diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-02-23 17:25:28 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-02-23 17:25:28 +0100 |
| commit | 064a7906f6c6e18ad4ce5fb5a19e5e3d02144358 (patch) | |
| tree | c7e62f961ab5638aa36b1c669bc49d03939bcc41 | |
| parent | 807cde3a096a6e87d13a98d2ee78578732b1fb45 (diff) | |
| download | jellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar jellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar.bz2 jellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar.zst | |
user settings page
| -rw-r--r-- | common/src/api.rs | 1 | ||||
| -rw-r--r-- | common/src/user.rs | 3 | ||||
| -rw-r--r-- | locale/en.ini | 19 | ||||
| -rw-r--r-- | server/src/routes.rs | 9 | ||||
| -rw-r--r-- | server/src/ui/account/mod.rs | 2 | ||||
| -rw-r--r-- | server/src/ui/account/settings.rs | 115 | ||||
| -rw-r--r-- | ui/src/components/mod.rs | 7 | ||||
| -rw-r--r-- | ui/src/components/node_page.rs | 39 | ||||
| -rw-r--r-- | ui/src/components/user.rs | 53 |
9 files changed, 146 insertions, 102 deletions
diff --git a/common/src/api.rs b/common/src/api.rs index beccaa5..bec33ba 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -27,6 +27,7 @@ fields! { VIEW_ADMIN_IMPORT: Object = b"adim"; VIEW_ADMIN_INFO: Object = b"adin"; VIEW_ADMIN_LOG: Object = b"adlo"; + VIEW_USER_SETTINGS: Object = b"uset"; ADMIN_IMPORT_BUSY: () = b"busy"; ADMIN_IMPORT_ERROR: &str = b"erro"; // multi diff --git a/common/src/user.rs b/common/src/user.rs index fcb8eea..ccd9c5c 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -4,12 +4,13 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use jellyobject::fields; +use jellyobject::{enums, fields}; fields! { USER_LOGIN: &str = b"Ulgn"; USER_PASSWORD: &[u8] = b"Upwd"; USER_NAME: &str = b"Unam"; + USER_THEME: &str = b"Uthm"; USER_ADMIN: () = b"Uadm"; UDATA_WATCHED: () = b"Dwat"; diff --git a/locale/en.ini b/locale/en.ini index b49273c..15658ae 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -8,8 +8,6 @@ nav.admin=Administration nav.settings=Settings nav.logout=Log out nav.login=Log in -nav.register=Register -nav.importing=Library database is being updated... stats.title=Library Statistics stats.count=There is a total of {count} nodes in the library. @@ -56,18 +54,6 @@ node.tags=Tags node.similar=Similar Media node.external_ids=External Identifiers -filter_sort=Filter and Sort -filter_sort.filter.kind=By Kind -filter_sort.filter.federation=By Federation -filter_sort.filter.watched=By Watched -filter_sort.sort.general=General -filter_sort.sort.media=Media -filter_sort.sort.rating=By Rating -filter_sort.sort.rating.user=Your Rating -filter_sort.sort.rating.likes_div_views=Likes per view -filter_sort.order.asc=Ascending -filter_sort.order.desc=Descending - tag.cred.kind.arra=Arranger tag.cred.kind.art1=Art tag.cred.kind.came=Camera @@ -158,6 +144,10 @@ tag.sbtl=Subtitle tag.stsz=Size on Filesystem tag.tgln=Tagline tag.titl=Title +tag.Uadm=Admin +tag.Ulgn=Login +tag.Unam=Name +tag.Upwd=Password theme.dark=Dark theme.light=Light @@ -255,6 +245,7 @@ account.register.invitation=Invite Code account.display_name=Display Name settings=Settings +settings.account=Account settings.account.display_name.changed=Display name updated. settings.account.password.changed=Password updated. settings.appearance=Appearance diff --git a/server/src/routes.rs b/server/src/routes.rs index 334fb39..2b7fed9 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -12,7 +12,10 @@ use crate::{ stream::r_stream, }, ui::{ - account::{r_account_login, r_account_login_post, r_account_logout, r_account_logout_post}, + account::{ + r_account_login, r_account_login_post, r_account_logout, r_account_logout_post, + settings::{r_account_settings, r_account_settings_post}, + }, admin::{ import::{r_admin_import, r_admin_import_post, r_admin_import_stream}, log::{r_admin_log, r_admin_log_stream}, @@ -78,8 +81,8 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { r_account_logout, // r_account_register_post, // r_account_register, - // r_account_settings_post, - // r_account_settings, + r_account_settings_post, + r_account_settings, r_admin_dashboard, r_admin_import, r_admin_import_post, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 8c60a2d..448f91e 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -5,6 +5,8 @@ */ // pub mod settings; +pub mod settings; + use super::error::MyError; use crate::{ auth::login, request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse, diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index 167731f..2e9fcbe 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -5,24 +5,23 @@ */ use super::format_form_error; use crate::{ - request_info::{RequestInfo, A}, - ui::error::MyResult, + auth::hash_password, request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse, }; +use anyhow::anyhow; use jellycommon::{ + MESSAGE_KIND, MESSAGE_TEXT, USER_LOGIN, USER_NAME, USER_PASSWORD, VIEW_MESSAGE, + VIEW_USER_SETTINGS, + jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder, Path}, routes::u_account_settings, - user::{PlayerKind, Theme}, }; -use jellylogic::account::{ - update_user_display_name, update_user_native_secret, update_user_password, - update_user_player_preference, update_user_theme, -}; -use jellyui::{account::settings::SettingsPage, locale::tr, render_page, scaffold::SessionInfo}; +use jellydb::{Filter, Query, Sort}; +use jellyui::tr; use rocket::{ - form::{self, validate::len, Contextual, Form}, + FromForm, + form::{self, Contextual, Form, validate::len}, get, post, request::FlashMessage, - response::{content::RawHtml, Flash, Redirect}, - FromForm, + response::{Flash, Redirect}, }; use std::ops::Range; @@ -31,10 +30,7 @@ pub struct SettingsForm { #[field(validate = option_len(4..64))] password: Option<String>, #[field(validate = option_len(4..32))] - display_name: Option<String>, - theme: Option<A<Theme>>, - player_preference: Option<A<PlayerKind>>, - native_secret: Option<String>, + name: Option<String>, } fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<'v, ()> { @@ -42,17 +38,21 @@ fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<' } #[get("/account/settings")] -pub fn r_account_settings(ri: RequestInfo, flash: Option<FlashMessage>) -> RawHtml<String> { - RawHtml(render_page( - &SettingsPage { - flash: &flash.map(FlashMessage::into_inner), - session: &SessionInfo { - user: ri.session.user.clone(), - }, - lang: &ri.lang, - }, - ri.render_info(), - )) +pub fn r_account_settings(ri: RequestInfo, flash: Option<FlashMessage>) -> MyResult<UiResponse> { + let user = ri.require_user()?; + let mut view = ObjectBufferBuilder::default(); + view.push(VIEW_USER_SETTINGS, user); + if let Some(flash) = flash { + view.push( + VIEW_MESSAGE, + ObjectBuffer::new(&mut [ + (MESSAGE_KIND.0, &flash.kind()), + (MESSAGE_TEXT.0, &flash.message()), + ]) + .as_object(), + ); + } + Ok(ri.respond_ui(view.finish())) } #[post("/account/settings", data = "<form>")] @@ -73,30 +73,35 @@ pub fn r_account_settings_post( let mut out = String::new(); if let Some(password) = &form.password { - update_user_password(&ri.session, password)?; + let login = ri + .require_user()? + .get(USER_LOGIN) + .ok_or(anyhow!("user has no login"))?; + let password = hash_password(login, password); + update_user(&ri, |user| user.insert(USER_PASSWORD, &password))?; out += &*tr(ri.lang, "settings.account.password.changed"); out += "\n"; } - if let Some(display_name) = &form.display_name { - update_user_display_name(&ri.session, display_name)?; + if let Some(name) = &form.name { + update_user(&ri, |user| user.insert(USER_NAME, name))?; out += &*tr(ri.lang, "settings.account.display_name.changed"); out += "\n"; } - if let Some(theme) = form.theme { - update_user_theme(&ri.session, theme.0)?; - out += &*tr(ri.lang, "settings.account.theme.changed"); - out += "\n"; - } - if let Some(player_preference) = form.player_preference { - update_user_player_preference(&ri.session, player_preference.0)?; - out += &*tr(ri.lang, "settings.player_preference.changed"); - out += "\n"; - } - if let Some(native_secret) = &form.native_secret { - update_user_native_secret(&ri.session, native_secret)?; - out += &*tr(ri.lang, "settings.native_secret.changed"); - out += "\n"; - } + // if let Some(theme) = form.theme { + // update_user_theme(&ri.session, theme.0)?; + // out += &*tr(ri.lang, "settings.account.theme.changed"); + // out += "\n"; + // } + // if let Some(player_preference) = form.player_preference { + // update_user_player_preference(&ri.session, player_preference.0)?; + // out += &*tr(ri.lang, "settings.player_preference.changed"); + // out += "\n"; + // } + // if let Some(native_secret) = &form.native_secret { + // update_user_native_secret(&ri.session, native_secret)?; + // out += &*tr(ri.lang, "settings.native_secret.changed"); + // out += "\n"; + // } let out = if out.is_empty() { tr(ri.lang, "settings.no_change").to_string() } else { @@ -105,3 +110,25 @@ pub fn r_account_settings_post( Ok(Flash::success(Redirect::to(u_account_settings()), out)) } + +fn update_user(ri: &RequestInfo, update: impl Fn(Object) -> ObjectBuffer) -> MyResult<()> { + let login = ri + .require_user()? + .get(USER_LOGIN) + .ok_or(anyhow!("user has no login"))?; + ri.state.database.transaction(&mut |txn| { + let user_row = txn + .query_single(Query { + filter: Filter::Match(Path(vec![USER_LOGIN.0]), login.into()), + sort: Sort::None, + })? + .ok_or(anyhow!("user vanished"))?; + + let user = txn.get(user_row)?.unwrap(); + let new_user = update(user.as_object()); + txn.update(user_row, new_user)?; + + Ok(()) + })?; + Ok(()) +} diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index 15b2ae2..dde77b0 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -5,6 +5,7 @@ */ pub mod admin; +pub mod admin_log; pub mod login; pub mod message; pub mod node_card; @@ -12,7 +13,7 @@ pub mod node_list; pub mod node_page; pub mod props; pub mod stats; -pub mod admin_log; +pub mod user; use crate::{ RenderInfo, @@ -22,6 +23,7 @@ use crate::{ message::Message, node_list::NodeList, node_page::{NodePage, Player}, + user::UserSettings, }, }; use jellycommon::{jellyobject::Object, *}; @@ -59,5 +61,8 @@ define! { @if let Some(data) = view.get(VIEW_ADMIN_INFO) { @AdminInfo { ri, data } } + @if let Some(user) = view.get(VIEW_USER_SETTINGS) { + @UserSettings { ri, user } + } } } diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs index 5823933..53534cf 100644 --- a/ui/src/components/node_page.rs +++ b/ui/src/components/node_page.rs @@ -79,32 +79,6 @@ markup::define! { // }} // }} // } - // @if !node.credits.is_empty() { - // h2 { @trs(lang, "node.people") } - // @for (group, people) in &node.credits { - // details[open=group==&CreditCategory::Cast] { - // summary { h3 { @format!("{}", group) } } - // ul.children.hlist { @for (i, pe) in people.iter().enumerate() { - // li { .card."aspect-port" { - // .poster { - // a[href="#"] { - // img[src=u_node_slug_person_asset(&node.slug, *group, i, 1024), loading="lazy"]; - // } - // } - // .title { - // // TODO span { @pe.person.name } br; - // @if let Some(c) = pe.characters.first() { - // span.subtitle { @c } - // } - // @if let Some(c) = pe.jobs.first() { - // span.subtitle { @c } - // } - // } - // }} - // }} - // } - // } - // } @if node.has(NO_TRACK.0) { details { summary { @tr(ri.lang, "media.tracks") } @@ -165,19 +139,6 @@ markup::define! { // li { @NodeCard { node, udata, lang } } // }} // } - // @match node.kind { - // NodeKind::Show | NodeKind::Series | NodeKind::Season => { - // ol { @for (node, udata) in children.iter() { - // li { @NodeCardWide { node, udata, lang } } - // }} - // } - // NodeKind::Collection | NodeKind::Channel | _ => { - // ul.children {@for (node, udata) in children.iter() { - // li { @NodeCard { node, udata, lang } } - // }} - // } - // } - // } } Player<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) { diff --git a/ui/src/components/user.rs b/ui/src/components/user.rs new file mode 100644 index 0000000..815c555 --- /dev/null +++ b/ui/src/components/user.rs @@ -0,0 +1,53 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use crate::RenderInfo; +use jellycommon::{ + jellyobject::Object, + routes::{u_account_login, u_account_settings}, + *, +}; +use jellyui_locale::tr; + +markup::define! { + UserSettings<'a>(ri: &'a RenderInfo<'a>, user: Object<'a>) { + h1 { @tr(ri.lang, "settings") } + + h2 { @tr(ri.lang, "settings.account") } + a.switch_account[href=u_account_login()] { "Switch Account" } + p { @tr(ri.lang, "tag.Ulgn") ": " @user.get(USER_LOGIN) } + form[method="POST", action=u_account_settings()] { + label[for="name"] { @tr(ri.lang, "tag.Unam") } + input[type="text", id="name", name="name", value=user.get(USER_NAME)]; + input[type="submit", value=tr(ri.lang, "settings.update")]; + } + form[method="POST", action=u_account_settings()] { + label[for="password"] { @tr(ri.lang, "tag.Upwd") } + input[type="password", id="password", name="password"]; + input[type="submit", value=tr(ri.lang, "settings.update")]; + } + + // h2 { @tr(ri.lang, "settings.appearance") } + // form[method="POST", action=u_account_settings()] { + // fieldset { + // legend { @tr(ri.lang, "tag.Uthm") } + // @for theme in [] { + // label { input[type="radio", name="theme", value=A(*theme), checked=session.user.theme==*theme]; @tr(ri.lang, &format!("theme.{theme}")) } br; + // } + // } + // input[type="submit", value=tr(ri.lang, "settings.apply")]; + // } + // form[method="POST", action=u_account_settings()] { + // fieldset { + // legend { @tr(ri.lang, "settings.player_preference") } + // @for kind in PlayerKind::ALL { + // label { input[type="radio", name="player_preference", value=A(*kind), checked=session.user.player_preference==*kind]; @tr(ri.lang, &format!("player_kind.{kind}")) } br; + // } + // } + // input[type="submit", value=tr(ri.lang, "settings.apply")]; + // } + } +} |