feat: init

This commit is contained in:
zhenyi
2026-06-07 11:30:56 +08:00
commit 563381c1ca
361 changed files with 41327 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
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<String>,
pub color_scheme: Option<String>,
pub density: Option<String>,
pub font_size: Option<String>,
pub editor_theme: Option<String>,
pub markdown_preview: Option<bool>,
pub reduced_motion: Option<bool>,
}
impl UserService {
pub async fn user_appearance(&self, ctx: &Session) -> Result<UserAppearance, AppError> {
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<UserAppearance, AppError> {
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<UserAppearance, AppError> {
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<Option<UserAppearance>, 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)
}
}