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, // need a date-picker or something #[serde(default, deserialize_with = "empty_string_as_none")] pub metadata_url: Option, #[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, Form(form): Form, ) -> Result { 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 { 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, Form(form): Form, ) -> Result { 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, Form(form): Form, ) -> Result { 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 { let quest: Option = 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>, State(pool): State, ) -> 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 = 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) -> impl IntoResponse { let user = auth.user; let watches: Vec = 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, Path(watch): Path, ) -> Result { if let Some(user) = auth.user { let watch = Julid::from_str(&watch).unwrap(); let quest: Option = 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("Login to add".into_response()) } } fn checkmark(public: bool) -> String { let public = if public { "public" } else { "private" }; format!("{CHECKMARK} ({public})") }