342 lines
10 KiB
Rust
342 lines
10 KiB
Rust
use askama::Template;
|
|
use axum::{
|
|
extract::{Form, Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Redirect},
|
|
};
|
|
use http::HeaderValue;
|
|
use julid::Julid;
|
|
use serde::Deserialize;
|
|
use sqlx::{query, query_as, query_scalar, SqlitePool};
|
|
|
|
use super::{
|
|
templates::{AddNewWatchPage, GetWatchPage, WatchStatusMenus},
|
|
AddError, AddErrorKind, EditError, EditErrorKind, WatchesError,
|
|
};
|
|
use crate::{
|
|
misc_util::empty_string_as_none, AuthSession, MyWatchesPage, ShowKind, Watch, WatchError,
|
|
WatchQuest, Wender,
|
|
};
|
|
|
|
//-************************************************************************
|
|
// Constants
|
|
//-************************************************************************
|
|
|
|
const GET_SAVED_WATCHES_QUERY: &str = "select * from watches inner join watch_quests quests on quests.user = $1 and quests.watch = watches.id";
|
|
const GET_QUEST_QUERY: &str = "select * from watch_quests where user = ? and watch = ?";
|
|
|
|
const GET_WATCH_QUERY: &str = "select * from watches where id = $1";
|
|
|
|
const ADD_WATCH_QUERY: &str = "insert into watches (title, kind, release_date, metadata_url, added_by, length) values ($1, $2, $3, $4, $5, $6) returning id";
|
|
const ADD_WATCH_QUEST_QUERY: &str =
|
|
"insert into watch_quests (user, watch, public, watched) values ($1, $2, $3, $4)";
|
|
|
|
const CHECKMARK: &str = "✓";
|
|
|
|
//-************************************************************************
|
|
// Types for receiving arguments from forms
|
|
//-************************************************************************
|
|
|
|
// kinda the main form?
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct PostAddNewWatch {
|
|
pub title: String,
|
|
#[serde(default)]
|
|
pub private: bool,
|
|
pub kind: ShowKind,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub year: Option<String>, // need a date-picker or something
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub metadata_url: Option<String>,
|
|
#[serde(default)]
|
|
pub watched_already: bool,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct PostAddExistingWatch {
|
|
pub watch: String,
|
|
pub public: bool,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct PostEditQuest {
|
|
pub watch: String,
|
|
pub act: String,
|
|
}
|
|
|
|
//-************************************************************************
|
|
// handlers
|
|
//-************************************************************************
|
|
|
|
pub async fn get_add_new_watch(auth: AuthSession) -> impl IntoResponse {
|
|
AddNewWatchPage { user: auth.user }.render().wender()
|
|
}
|
|
|
|
/// Add a Watch to your watchlist (side effects system-add)
|
|
pub async fn post_add_new_watch(
|
|
auth: AuthSession,
|
|
State(pool): State<SqlitePool>,
|
|
Form(form): Form<PostAddNewWatch>,
|
|
) -> Result<impl IntoResponse, WatchesError> {
|
|
if let Some(user) = auth.user {
|
|
{
|
|
let watch = Watch {
|
|
title: form.title,
|
|
kind: form.kind,
|
|
metadata_url: form.metadata_url,
|
|
release_date: form.year,
|
|
added_by: user.id,
|
|
..Default::default()
|
|
};
|
|
|
|
let watch_id = add_new_watch_impl(&pool, &watch).await?;
|
|
let quest = WatchQuest {
|
|
user: user.id,
|
|
public: !form.private,
|
|
watched: false,
|
|
watch: watch_id,
|
|
};
|
|
add_watch_quest_impl(&pool, &quest).await?;
|
|
|
|
let location = format!("/watch/{watch_id}");
|
|
Ok(Redirect::to(&location))
|
|
}
|
|
} else {
|
|
let e: AddError = AddErrorKind::NotSignedIn.into();
|
|
Err(e.into())
|
|
}
|
|
}
|
|
|
|
async fn add_new_watch_impl(db_pool: &SqlitePool, watch: &Watch) -> Result<Julid, AddError> {
|
|
let watch_id: Julid = query_scalar(ADD_WATCH_QUERY)
|
|
.bind(&watch.title)
|
|
.bind(watch.kind)
|
|
.bind(&watch.release_date)
|
|
.bind(&watch.metadata_url)
|
|
.bind(watch.added_by)
|
|
.bind(watch.length)
|
|
.fetch_one(db_pool)
|
|
.await
|
|
.map_err(|err| {
|
|
tracing::error!("Got error: {err}");
|
|
AddErrorKind::DBError
|
|
})?;
|
|
Ok(watch_id)
|
|
}
|
|
|
|
/// Add a Watch to your watchlist by selecting it with a checkbox
|
|
pub async fn post_add_watch_quest(
|
|
auth: AuthSession,
|
|
State(pool): State<SqlitePool>,
|
|
Form(form): Form<PostAddExistingWatch>,
|
|
) -> Result<impl IntoResponse, WatchesError> {
|
|
if let Some(user) = auth.user {
|
|
let quest = WatchQuest {
|
|
user: user.id,
|
|
watch: Julid::from_str(&form.watch).unwrap(),
|
|
public: form.public,
|
|
watched: false,
|
|
};
|
|
add_watch_quest_impl(&pool, &quest).await?;
|
|
|
|
let resp = checkmark(form.public);
|
|
Ok(resp.into_response())
|
|
} else {
|
|
let resp = Redirect::to("/login");
|
|
let mut resp = resp.into_response();
|
|
resp.headers_mut()
|
|
.insert("HX-Redirect", HeaderValue::from_str("/login").unwrap());
|
|
Ok(resp)
|
|
}
|
|
}
|
|
|
|
async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Result<(), AddError> {
|
|
query(ADD_WATCH_QUEST_QUERY)
|
|
.bind(quest.user)
|
|
.bind(quest.watch)
|
|
.bind(quest.public)
|
|
.bind(quest.watched)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|err| {
|
|
tracing::error!("Got error: {err}");
|
|
AddErrorKind::DBError
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
pub async fn edit_watch_quest(
|
|
auth: AuthSession,
|
|
State(pool): State<SqlitePool>,
|
|
Form(form): Form<PostEditQuest>,
|
|
) -> Result<impl IntoResponse, WatchesError> {
|
|
if let Some(user) = auth.user {
|
|
let watch = Julid::from_str(form.watch.trim()).map_err(|_| EditErrorKind::NotFound)?;
|
|
Ok(
|
|
edit_watch_quest_impl(&pool, form.act.trim(), user.id, watch)
|
|
.await?
|
|
.render()
|
|
.wender(),
|
|
)
|
|
} else {
|
|
Err(EditErrorKind::NotSignedIn.into())
|
|
}
|
|
}
|
|
|
|
async fn edit_watch_quest_impl(
|
|
pool: &SqlitePool,
|
|
action: &str,
|
|
user: Julid,
|
|
watch: Julid,
|
|
) -> Result<WatchStatusMenus, EditError> {
|
|
let quest: Option<WatchQuest> = query_as(GET_QUEST_QUERY)
|
|
.bind(user)
|
|
.bind(watch)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Got error from checking watch status: {e:?}");
|
|
EditErrorKind::DBError
|
|
})?;
|
|
if let Some(quest) = quest {
|
|
match action {
|
|
"remove" => {
|
|
sqlx::query!(
|
|
"delete from watch_quests where user = ? and watch = ?",
|
|
user,
|
|
watch
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Error removing quest: {e}");
|
|
EditErrorKind::DBError
|
|
})?;
|
|
Ok(WatchStatusMenus { watch, quest: None })
|
|
}
|
|
"watched" => {
|
|
let watched = !quest.watched;
|
|
sqlx::query!(
|
|
"update watch_quests set watched = ? where user = ? and watch = ?",
|
|
watched,
|
|
user,
|
|
watch
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Error updating quest: {e}");
|
|
EditErrorKind::DBError
|
|
})?;
|
|
let quest = WatchQuest { watched, ..quest };
|
|
Ok(WatchStatusMenus {
|
|
watch,
|
|
quest: Some(quest),
|
|
})
|
|
}
|
|
"viz" => {
|
|
let public = !quest.public;
|
|
sqlx::query!(
|
|
"update watch_quests set public = ? where user = ? and watch = ?",
|
|
public,
|
|
user,
|
|
watch
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Error updating quest: {e}");
|
|
EditErrorKind::DBError
|
|
})?;
|
|
let quest = WatchQuest { public, ..quest };
|
|
Ok(WatchStatusMenus {
|
|
watch,
|
|
quest: Some(quest),
|
|
})
|
|
}
|
|
_ => Ok(WatchStatusMenus {
|
|
watch,
|
|
quest: Some(quest),
|
|
}),
|
|
}
|
|
} else {
|
|
Err(EditErrorKind::NotFound.into())
|
|
}
|
|
}
|
|
|
|
/// A single Watch
|
|
pub async fn get_watch(
|
|
auth: AuthSession,
|
|
watch: Option<Path<String>>,
|
|
State(pool): State<SqlitePool>,
|
|
) -> impl IntoResponse {
|
|
let id = if let Some(Path(id)) = watch {
|
|
id
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
let id = id.trim();
|
|
let id = Julid::from_str(id).unwrap_or_default();
|
|
let q = query_as(GET_WATCH_QUERY).bind(id);
|
|
let watch: Option<Watch> = q.fetch_one(&pool).await.ok();
|
|
|
|
GetWatchPage {
|
|
watch,
|
|
user: auth.user,
|
|
}
|
|
.render()
|
|
.map_err(|e| {
|
|
tracing::error!("could not render watch page, got {e}");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})
|
|
.into_response()
|
|
}
|
|
|
|
/// everything the user has saved
|
|
pub async fn get_watches(auth: AuthSession, State(pool): State<SqlitePool>) -> impl IntoResponse {
|
|
let user = auth.user;
|
|
let watches: Vec<Watch> = if let Some(ref user) = user {
|
|
query_as(GET_SAVED_WATCHES_QUERY)
|
|
.bind(user.id)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap_or_default()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
MyWatchesPage { watches, user }
|
|
.render()
|
|
.wender()
|
|
.into_response()
|
|
}
|
|
|
|
pub async fn get_watch_status(
|
|
auth: AuthSession,
|
|
State(pool): State<SqlitePool>,
|
|
Path(watch): Path<String>,
|
|
) -> Result<impl IntoResponse, ()> {
|
|
if let Some(user) = auth.user {
|
|
let watch = Julid::from_str(&watch).unwrap();
|
|
let quest: Option<WatchQuest> = query_as(GET_QUEST_QUERY)
|
|
.bind(user.id)
|
|
.bind(watch)
|
|
.fetch_optional(&pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Got error from checking watch status: {e:?}");
|
|
})?;
|
|
Ok(WatchStatusMenus { watch, quest }
|
|
.render()
|
|
.wender()
|
|
.into_response())
|
|
} else {
|
|
Ok("<a href='/login'>Login to add</a>".into_response())
|
|
}
|
|
}
|
|
|
|
fn checkmark(public: bool) -> String {
|
|
let public = if public { "public" } else { "private" };
|
|
format!("{CHECKMARK} ({public})")
|
|
}
|