diff options
Diffstat (limited to 'ui/src')
-rw-r--r-- | ui/src/filter_sort.rs | 24 | ||||
-rw-r--r-- | ui/src/format.rs | 5 | ||||
-rw-r--r-- | ui/src/home.rs | 24 | ||||
-rw-r--r-- | ui/src/lib.rs | 27 | ||||
-rw-r--r-- | ui/src/node_page.rs | 7 | ||||
-rw-r--r-- | ui/src/props.rs | 4 | ||||
-rw-r--r-- | ui/src/scaffold.rs | 4 | ||||
-rw-r--r-- | ui/src/search.rs | 38 | ||||
-rw-r--r-- | ui/src/settings.rs | 76 | ||||
-rw-r--r-- | ui/src/stats.rs | 45 |
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 } } |