aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-23 17:25:28 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-23 17:25:28 +0100
commit064a7906f6c6e18ad4ce5fb5a19e5e3d02144358 (patch)
treec7e62f961ab5638aa36b1c669bc49d03939bcc41
parent807cde3a096a6e87d13a98d2ee78578732b1fb45 (diff)
downloadjellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar
jellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar.bz2
jellything-064a7906f6c6e18ad4ce5fb5a19e5e3d02144358.tar.zst
user settings page
-rw-r--r--common/src/api.rs1
-rw-r--r--common/src/user.rs3
-rw-r--r--locale/en.ini19
-rw-r--r--server/src/routes.rs9
-rw-r--r--server/src/ui/account/mod.rs2
-rw-r--r--server/src/ui/account/settings.rs115
-rw-r--r--ui/src/components/mod.rs7
-rw-r--r--ui/src/components/node_page.rs39
-rw-r--r--ui/src/components/user.rs53
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")];
+ // }
+ }
+}