use serde::{Deserialize, Serialize}; use crate::error::AppError; use crate::models::common::{ColorScheme, Density, FontSize, Theme}; use crate::models::users::UserAppearance; use crate::service::UserService; use crate::session::Session; use super::util::{merge_optional_text, parse_enum}; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct UpdateUserAppearanceParams { pub theme: Option, pub color_scheme: Option, pub density: Option, pub font_size: Option, pub editor_theme: Option, pub markdown_preview: Option, pub reduced_motion: Option, } impl UserService { pub async fn user_appearance(&self, ctx: &Session) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; self.ensure_user_appearance(user_uid).await } pub async fn user_update_appearance( &self, ctx: &Session, params: UpdateUserAppearanceParams, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let current = self.ensure_user_appearance(user_uid).await?; let theme = parse_enum(params.theme, current.theme, Theme::Unknown, "theme")?; let color_scheme = parse_enum( params.color_scheme, current.color_scheme, ColorScheme::Unknown, "color_scheme", )?; let density = parse_enum(params.density, current.density, Density::Unknown, "density")?; let font_size = parse_enum( params.font_size, current.font_size, FontSize::Unknown, "font_size", )?; sqlx::query_as::<_, UserAppearance>( "UPDATE user_appearance SET theme = $1, color_scheme = $2, density = $3, font_size = $4, \ editor_theme = $5, markdown_preview = $6, reduced_motion = $7, updated_at = $8 \ WHERE user_id = $9 RETURNING user_id, theme, color_scheme, density, font_size, \ editor_theme, markdown_preview, reduced_motion, created_at, updated_at", ) .bind(theme) .bind(color_scheme) .bind(density) .bind(font_size) .bind(merge_optional_text(params.editor_theme, current.editor_theme)) .bind(params.markdown_preview.unwrap_or(current.markdown_preview)) .bind(params.reduced_motion.unwrap_or(current.reduced_motion)) .bind(chrono::Utc::now()) .bind(user_uid) .fetch_one(self.ctx.db.writer()) .await .map_err(AppError::Database) } async fn ensure_user_appearance( &self, user_uid: uuid::Uuid, ) -> Result { if let Some(appearance) = self.find_user_appearance(user_uid).await? { return Ok(appearance); } let now = chrono::Utc::now(); sqlx::query( "INSERT INTO user_appearance (user_id, theme, color_scheme, density, font_size, \ markdown_preview, reduced_motion, created_at, updated_at) \ VALUES ($1, 'system', 'system', 'comfortable', 'medium', true, false, $2, $2) ON CONFLICT (user_id) DO NOTHING", ) .bind(user_uid) .bind(now) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; self.find_user_appearance(user_uid) .await? .ok_or(AppError::UserNotFound) } async fn find_user_appearance( &self, user_uid: uuid::Uuid, ) -> Result, AppError> { sqlx::query_as::<_, UserAppearance>( "SELECT user_id, theme, color_scheme, density, font_size, editor_theme, \ markdown_preview, reduced_motion, created_at, updated_at \ FROM user_appearance WHERE user_id = $1", ) .bind(user_uid) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database) } }