aboutsummaryrefslogtreecommitdiff
path: root/ui/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-28 18:27:03 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-28 18:27:03 +0200
commit51761cbdefa39107b9e1f931f1aa8df6aebb2a94 (patch)
tree957ca180786ece777e6e1153ada91da741d845ec /ui/src
parent80d28b764c95891551e28c395783f5ff9d065743 (diff)
downloadjellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar
jellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar.bz2
jellything-51761cbdefa39107b9e1f931f1aa8df6aebb2a94.tar.zst
many much more generic refactor
Diffstat (limited to 'ui/src')
-rw-r--r--ui/src/filter_sort.rs24
-rw-r--r--ui/src/format.rs5
-rw-r--r--ui/src/home.rs24
-rw-r--r--ui/src/lib.rs27
-rw-r--r--ui/src/node_page.rs7
-rw-r--r--ui/src/props.rs4
-rw-r--r--ui/src/scaffold.rs4
-rw-r--r--ui/src/search.rs38
-rw-r--r--ui/src/settings.rs76
-rw-r--r--ui/src/stats.rs45
10 files changed, 192 insertions, 62 deletions
diff --git a/ui/src/filter_sort.rs b/ui/src/filter_sort.rs
index 53d4ea3..ec83f6f 100644
--- a/ui/src/filter_sort.rs
+++ b/ui/src/filter_sort.rs
@@ -5,6 +5,7 @@
*/
use jellycommon::api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty};
+use markup::RenderAttributeValue;
use crate::locale::{Language, trs};
@@ -79,11 +80,11 @@ markup::define! {
fieldset.filter {
legend { "Filter" }
.categories {
- @for (cname, cat) in FilterProperty::CATS {
+ @for (cname, cat) in FILTER_CATS {
.category {
h3 { @trs(lang, cname) }
@for (value, label) in *cat {
- label { input[type="checkbox", name="filter_kind", value=value, checked=f.filter_kind.as_ref().map(|k|k.contains(value)).unwrap_or(true)]; @trs(lang, label) } br;
+ label { input[type="checkbox", name="filter_kind", value=A(*value), checked=f.filter_kind.as_ref().map(|k|k.contains(value)).unwrap_or(true)]; @trs(lang, label) } br;
}
}
}
@@ -92,11 +93,11 @@ markup::define! {
fieldset.sortby {
legend { "Sort" }
.categories {
- @for (cname, cat) in SortProperty::CATS {
+ @for (cname, cat) in SORT_CATS {
.category {
h3 { @trs(lang, cname) }
@for (value, label) in *cat {
- label { input[type="radio", name="sort_by", value=value, checked=Some(value)==f.sort_by.as_ref()]; @trs(lang, label) } br;
+ label { input[type="radio", name="sort_by", value=A(*value), checked=Some(value)==f.sort_by.as_ref()]; @trs(lang, label) } br;
}
}
}
@@ -106,7 +107,7 @@ markup::define! {
legend { "Sort Order" }
@use SortOrder::*;
@for (value, label) in [(Ascending, "filter_sort.order.asc"), (Descending, "filter_sort.order.desc")] {
- label { input[type="radio", name="sort_order", value=value, checked=Some(value)==f.sort_order]; @trs(lang, label) } br;
+ label { input[type="radio", name="sort_order", value=A(value), checked=Some(value)==f.sort_order]; @trs(lang, label) } br;
}
}
input[type="submit", value="Apply"]; a[href="?"] { "Clear" }
@@ -115,21 +116,22 @@ markup::define! {
}
}
-impl markup::Render for SortProperty {
+struct A<T>(pub T);
+impl markup::Render for A<SortProperty> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
}
}
-impl markup::Render for SortOrder {
+impl markup::Render for A<SortOrder> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
}
}
-impl markup::Render for FilterProperty {
+impl markup::Render for A<FilterProperty> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
}
}
-impl RenderAttributeValue for SortOrder {}
-impl RenderAttributeValue for FilterProperty {}
-impl RenderAttributeValue for SortProperty {}
+impl RenderAttributeValue for A<SortOrder> {}
+impl RenderAttributeValue for A<FilterProperty> {}
+impl RenderAttributeValue for A<SortProperty> {}
diff --git a/ui/src/format.rs b/ui/src/format.rs
index a374850..84e4c27 100644
--- a/ui/src/format.rs
+++ b/ui/src/format.rs
@@ -6,6 +6,7 @@
use crate::locale::{Language, TrString, tr, trs};
use jellycommon::{Chapter, MediaInfo, NodeKind, SourceTrackKind};
+use std::fmt::Write;
pub fn format_duration(d: f64) -> String {
format_duration_mode(d, false, Language::English)
@@ -66,10 +67,10 @@ pub fn format_kind(k: NodeKind, lang: Language) -> TrString<'static> {
)
}
-trait MediaInfoExt {
+pub trait MediaInfoExt {
fn resolution_name(&self) -> &'static str;
}
-impl MediaInfoExt for MediaInfo {
+impl MediaInfoExt for &MediaInfo {
fn resolution_name(&self) -> &'static str {
let mut maxdim = 0;
for t in &self.tracks {
diff --git a/ui/src/home.rs b/ui/src/home.rs
index 7b58179..ec0c634 100644
--- a/ui/src/home.rs
+++ b/ui/src/home.rs
@@ -3,20 +3,21 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-
use crate::{
+ Page,
locale::{Language, tr, trs},
node_card::NodeCard,
- scaffold::LayoutPage,
};
+use jellycommon::api::ApiHomeResponse;
+use markup::DynRender;
markup::define! {
- HomePage<'a>(lang: &'a Language) {
- h2 { @tr(lang, "home.bin.root").replace("{title}", &CONF.brand) }
- ul.children.hlist {@for (node, udata) in &toplevel {
+ HomePage<'a>(lang: &'a Language, r: &'a ApiHomeResponse) {
+ h2 { @trs(lang, "home.bin.root") } //.replace("{title}", &CONF.brand) }
+ ul.children.hlist {@for (node, udata) in &r.toplevel {
li { @NodeCard { node, udata, lang: &lang } }
}}
- @for (name, nodes) in &categories {
+ @for (name, nodes) in &r.categories {
@if !nodes.is_empty() {
h2 { @trs(&lang, &name) }
ul.children.hlist {@for (node, udata) in nodes {
@@ -27,10 +28,11 @@ markup::define! {
}
}
-pub fn home_page() {
- LayoutPage {
- title: tr(lang, "home").to_string(),
- content: HomePage { lang: &lang },
- ..Default::default()
+impl Page for HomePage<'_> {
+ fn title(&self) -> String {
+ tr(*self.lang, "home").to_string()
+ }
+ fn to_render(&self) -> DynRender {
+ markup::new!(@self)
}
}
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index 40d43dd..4298623 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -1,15 +1,34 @@
+use markup::DynRender;
+
/*
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) 2025 metamuffin <metamuffin.org>
*/
+pub mod filter_sort;
pub mod format;
+pub mod home;
pub mod locale;
-pub mod node_page;
pub mod node_card;
-pub mod scaffold;
+pub mod node_page;
pub mod props;
-pub mod filter_sort;
+pub mod scaffold;
pub mod search;
+pub mod settings;
pub mod stats;
-pub mod home;
+
+/// render as supertrait would be possible but is not
+/// dyn compatible and I really dont want to expose generics
+/// that generate rendering code because of compile speed.
+pub trait Page {
+ fn title(&self) -> String;
+ fn to_render(&self) -> DynRender;
+ fn class(&self) -> Option<&'static str> {
+ None
+ }
+}
+
+pub fn render_page(page: &dyn Page) -> String {
+ // page.render()
+ "a".to_string()
+}
diff --git a/ui/src/node_page.rs b/ui/src/node_page.rs
index 8848202..b48fca2 100644
--- a/ui/src/node_page.rs
+++ b/ui/src/node_page.rs
@@ -5,6 +5,7 @@
*/
use crate::{
+ Page,
filter_sort::NodeFilterSortForm,
format::format_chapter,
locale::{Language, trs},
@@ -18,6 +19,12 @@ use jellycommon::{
};
use std::sync::Arc;
+impl Page for NodePage<'_> {
+ fn title(&self) -> String {
+ self.node.title.clone().unwrap_or_default()
+ }
+}
+
markup::define! {
NodePage<'a>(
node: &'a Node,
diff --git a/ui/src/props.rs b/ui/src/props.rs
index 7dbc0de..fbeddca 100644
--- a/ui/src/props.rs
+++ b/ui/src/props.rs
@@ -4,8 +4,8 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
use crate::{
- format::format_duration,
- locale::{Language, trs},
+ format::{MediaInfoExt, format_count, format_duration},
+ locale::{Language, tr, trs},
};
use jellycommon::{
Node, Rating, Visibility,
diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs
index ffd5fdf..cc5886b 100644
--- a/ui/src/scaffold.rs
+++ b/ui/src/scaffold.rs
@@ -4,8 +4,8 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-use crate::locale::{tr, trs, Language};
-use markup::{DynRender, Render};
+use crate::locale::{Language, escape, tr, trs};
+use markup::{DynRender, Render, raw};
use std::sync::LazyLock;
static LOGO_ENABLED: LazyLock<bool> = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists());
diff --git a/ui/src/search.rs b/ui/src/search.rs
index 092ad57..f252620 100644
--- a/ui/src/search.rs
+++ b/ui/src/search.rs
@@ -4,28 +4,40 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
+use crate::{
+ Page,
+ locale::{Language, tr, trs},
+ node_card::NodeCard,
+};
+use jellycommon::api::ApiSearchResponse;
+use markup::DynRender;
+
+impl Page for SearchPage<'_> {
+ fn title(&self) -> String {
+ tr(*self.lang, "search.title").to_string()
+ }
+ fn class(&self) -> Option<&'static str> {
+ Some("search")
+ }
+ fn to_render(&self) -> DynRender {
+ markup::new!(@self)
+ }
+}
+
markup::define! {
- SearchPage {
+ SearchPage<'a>(lang: &'a Language, r: Option<ApiSearchResponse>, query: &'a Option<String>) {
h1 { @trs(&lang, "search.title") }
form[action="", method="GET"] {
- input[type="text", name="query", placeholder=&*tr(lang, "search.placeholder"), value=&query];
+ input[type="text", name="query", placeholder=&*tr(**lang, "search.placeholder"), value=&query];
input[type="submit", value="Search"];
}
- @if let Some((count, results, search_dur)) = &results {
+ @if let Some(r) = &r {
h2 { @trs(&lang, "search.results.title") }
- p.stats { @tr(lang, "search.results.stats").replace("{count}", &count.to_string()).replace("{dur}", &format!("{search_dur:?}")) }
- ul.children {@for (node, udata) in results.iter() {
+ p.stats { @tr(**lang, "search.results.stats").replace("{count}", &r.count.to_string()).replace("{dur}", &format!("{:?}", r.duration)) }
+ ul.children {@for (node, udata) in r.results.iter() {
li { @NodeCard { node, udata, lang: &lang } }
}}
// TODO pagination
}
}
}
-
-pub fn search_page() {
- LayoutPage {
- title: tr(lang, "search.title").to_string(),
- class: Some("search"),
- content: SearchPage,
- }
-} \ No newline at end of file
diff --git a/ui/src/settings.rs b/ui/src/settings.rs
new file mode 100644
index 0000000..9bc4b1d
--- /dev/null
+++ b/ui/src/settings.rs
@@ -0,0 +1,76 @@
+/*
+ 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) 2025 metamuffin <metamuffin.org>
+*/
+use crate::locale::{Language, tr, trs};
+use jellycommon::user::{PlayerKind, Theme};
+use markup::RenderAttributeValue;
+
+markup::define! {
+ Settings<'a>(flash: Option<Result<String, String>>, lang: &'a Language) {
+ h1 { "Settings" }
+ @if let Some(flash) = &flash {
+ @match flash {
+ Ok(mesg) => { section.message { p.success { @mesg } } }
+ Err(err) => { section.message { p.error { @format!("{err}") } } }
+ }
+ }
+ h2 { @trs(&lang, "account") }
+ a.switch_account[href=uri!(r_account_login())] { "Switch Account" }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ label[for="username"] { @trs(&lang, "account.username") }
+ input[type="text", id="username", disabled, value=&session.user.name];
+ input[type="submit", disabled, value=&*tr(**lang, "settings.immutable")];
+ }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ label[for="display_name"] { @trs(lang, "account.display_name") }
+ input[type="text", id="display_name", name="display_name", value=&session.user.display_name];
+ input[type="submit", value=&*tr(**lang, "settings.update")];
+ }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ label[for="password"] { @trs(lang, "account.password") }
+ input[type="password", id="password", name="password"];
+ input[type="submit", value=&*tr(**lang, "settings.update")];
+ }
+ h2 { @trs(&lang, "settings.appearance") }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ fieldset {
+ legend { @trs(&lang, "settings.appearance.theme") }
+ @for (t, tlabel) in Theme::LIST {
+ label { input[type="radio", name="theme", value=A(*t), checked=session.user.theme==*t]; @tlabel } br;
+ }
+ }
+ input[type="submit", value=&*tr(**lang, "settings.apply")];
+ }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ fieldset {
+ legend { @trs(&lang, "settings.player_preference") }
+ @for (t, tlabel) in PlayerKind::LIST {
+ label { input[type="radio", name="player_preference", value=A(*t), checked=session.user.player_preference==*t]; @tlabel } br;
+ }
+ }
+ input[type="submit", value=&*tr(**lang, "settings.apply")];
+ }
+ form[method="POST", action=uri!(r_account_settings_post())] {
+ label[for="native_secret"] { "Native Secret" }
+ input[type="password", id="native_secret", name="native_secret"];
+ input[type="submit", value=&*tr(**lang, "settings.update")];
+ p { "The secret can be found in " code{"$XDG_CONFIG_HOME/jellynative_secret"} " or by clicking " a.button[href="jellynative://show-secret-v1"] { "Show Secret" } "." }
+ }
+ }
+}
+
+struct A<T>(pub T);
+impl markup::Render for A<Theme> {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ }
+}
+impl markup::Render for A<PlayerKind> {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ }
+}
+impl RenderAttributeValue for A<Theme> {}
+impl RenderAttributeValue for A<PlayerKind> {}
diff --git a/ui/src/stats.rs b/ui/src/stats.rs
index b4a2e23..3655245 100644
--- a/ui/src/stats.rs
+++ b/ui/src/stats.rs
@@ -7,24 +7,24 @@
use crate::{
format::{format_duration, format_duration_long, format_kind, format_size},
locale::{Language, tr, trs},
- scaffold::LayoutPage,
};
+use jellycommon::api::{ApiStatsResponse, StatsBin};
use markup::raw;
markup::define! {
- StatsPage<'a>(lang: &'a Language) {
+ StatsPage<'a>(lang: &'a Language, r: ApiStatsResponse) {
.page.stats {
h1 { @trs(&lang, "stats.title") }
- p { @raw(tr(lang, "stats.count")
- .replace("{count}", &format!("<b>{}</b>", all.count))
+ p { @raw(tr(**lang, "stats.count")
+ .replace("{count}", &format!("<b>{}</b>", r.total.count))
)}
- p { @raw(tr(lang, "stats.runtime")
- .replace("{dur}", &format!("<b>{}</b>", format_duration_long(all.runtime, lang)))
- .replace("{size}", &format!("<b>{}</b>", format_size(all.size)))
+ p { @raw(tr(**lang, "stats.runtime")
+ .replace("{dur}", &format!("<b>{}</b>", format_duration_long(r.total.runtime, **lang)))
+ .replace("{size}", &format!("<b>{}</b>", format_size(r.total.size)))
)}
- p { @raw(tr(lang, "stats.average")
- .replace("{dur}", &format!("<b>{}</b>", format_duration(all.average_runtime())))
- .replace("{size}", &format!("<b>{}</b>", format_size(all.average_size() as u64)))
+ p { @raw(tr(**lang, "stats.average")
+ .replace("{dur}", &format!("<b>{}</b>", format_duration(r.total.average_runtime())))
+ .replace("{size}", &format!("<b>{}</b>", format_size(r.total.average_size() as u64)))
)}
h2 { @trs(&lang, "stats.by_kind.title") }
@@ -39,8 +39,8 @@ markup::define! {
th { @trs(&lang, "stats.by_kind.max_size") }
th { @trs(&lang, "stats.by_kind.max_runtime") }
}
- @for (k,b) in &kinds { tr {
- td { @format_kind(*k, lang) }
+ @for (k,b) in &r.kinds { tr {
+ td { @format_kind(*k, **lang) }
td { @b.count }
td { @format_size(b.size) }
td { @format_duration(b.runtime) }
@@ -54,10 +54,21 @@ markup::define! {
}
}
-pub fn stats_page() {
- LayoutPage {
- title: tr(lang, "stats.title").to_string(),
- content: StatsPage { lang: &lang },
- ..Default::default()
+impl StatsPage<'_> {
+ pub fn title(&self) -> String {
+ tr(*self.lang, "stats.title").to_string()
+ }
+}
+
+trait BinExt {
+ fn average_runtime(&self) -> f64;
+ fn average_size(&self) -> f64;
+}
+impl BinExt for StatsBin {
+ fn average_runtime(&self) -> f64 {
+ self.runtime / self.count as f64
+ }
+ fn average_size(&self) -> f64 {
+ self.size as f64 / self.count as f64
}
}