aboutsummaryrefslogtreecommitdiff
path: root/ui/src/old
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/old')
-rw-r--r--ui/src/old/account/mod.rs103
-rw-r--r--ui/src/old/account/settings.rs84
-rw-r--r--ui/src/old/admin/import.rs46
-rw-r--r--ui/src/old/admin/log.rs127
-rw-r--r--ui/src/old/admin/mod.rs64
-rw-r--r--ui/src/old/admin/user.rs81
-rw-r--r--ui/src/old/filter_sort.rs111
-rw-r--r--ui/src/old/home.rs33
-rw-r--r--ui/src/old/items.rs37
-rw-r--r--ui/src/old/node_card.rs59
-rw-r--r--ui/src/old/search.rs38
11 files changed, 783 insertions, 0 deletions
diff --git a/ui/src/old/account/mod.rs b/ui/src/old/account/mod.rs
new file mode 100644
index 0000000..e7da26f
--- /dev/null
+++ b/ui/src/old/account/mod.rs
@@ -0,0 +1,103 @@
+/*
+ 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>
+*/
+pub mod settings;
+
+use crate::{Page, locale::tr, scaffold::RenderInfo};
+use jellycommon::routes::{u_account_login, u_account_register};
+
+impl Page for AccountLogin<'_> {
+ fn title(&self) -> String {
+ tr(
+ self.ri.lang,
+ if self.logged_in {
+ "account.login.switch"
+ } else {
+ "account.login"
+ },
+ )
+ .to_string()
+ }
+
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+impl Page for AccountRegister<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "account.register").to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+impl Page for AccountRegisterSuccess<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "account.register").to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+impl Page for AccountLogout<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "account.logout").to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define! {
+ AccountRegister<'a>(ri: &'a RenderInfo<'a>) {
+ form.account[method="POST", action=""] {
+ h1 { @tr(ri.lang, "account.register") }
+
+ label[for="inp-invitation"] { @tr(ri.lang, "account.register.invitation") }
+ input[type="text", id="inp-invitation", name="invitation"]; br;
+
+ label[for="inp-username"] { @tr(ri.lang, "account.username") }
+ input[type="text", id="inp-username", name="username"]; br;
+ label[for="inp-password"] { @tr(ri.lang, "account.password") }
+ input[type="password", id="inp-password", name="password"]; br;
+
+ input[type="submit", value=tr(ri.lang, "account.register.submit")];
+
+ p { @tr(ri.lang, "account.register.login") " " a[href=u_account_login()] { @tr(ri.lang, "account.register.login_here") } }
+ }
+ }
+ AccountRegisterSuccess<'a>(ri: &'a RenderInfo<'a>, logged_in: bool) {
+ h1 { @tr(ri.lang, if *logged_in {
+ "account.register.success.switch"
+ } else {
+ "account.register.success"
+ })}
+ }
+ AccountLogin<'a>(ri: &'a RenderInfo<'a>, logged_in: bool) {
+ form.account[method="POST", action=""] {
+ h1 { @self.title() }
+
+ label[for="inp-username"] { @tr(ri.lang, "account.username") }
+ input[type="text", id="inp-username", name="username"]; br;
+ label[for="inp-password"] { @tr(ri.lang, "account.password") }
+ input[type="password", id="inp-password", name="password"]; br;
+
+ input[type="submit", value=tr(ri.lang, if *logged_in { "account.login.submit.switch" } else { "account.login.submit" })];
+
+ @if *logged_in {
+ p { @tr(ri.lang, "account.login.register.switch") " " a[href=u_account_register()] { @tr(ri.lang, "account.login.register_here") } }
+ } else {
+ p { @tr(ri.lang, "account.login.cookie_note") }
+ p { @tr(ri.lang, "account.login.register") " " a[href=u_account_register()] { @tr(ri.lang, "account.login.register_here") } }
+ }
+ }
+ }
+ AccountLogout<'a>(ri: &'a RenderInfo<'a>) {
+ form.account[method="POST", action=""] {
+ h1 { @tr(ri.lang, "account.logout") }
+ input[type="submit", value=tr(ri.lang, "account.logout.submit")];
+ }
+ }
+}
diff --git a/ui/src/old/account/settings.rs b/ui/src/old/account/settings.rs
new file mode 100644
index 0000000..83f72b0
--- /dev/null
+++ b/ui/src/old/account/settings.rs
@@ -0,0 +1,84 @@
+/*
+ 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::{
+ FlashM, Page,
+ locale::tr,
+ scaffold::{FlashDisplay, RenderInfo, SessionInfo},
+};
+use jellycommon::routes::{u_account_login, u_account_settings};
+use markup::RenderAttributeValue;
+
+impl Page for SettingsPage<'_> {
+ fn title(&self) -> String {
+ "Settings".to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define! {
+ SettingsPage<'a>(ri: &'a RenderInfo<'a>, session: &'a SessionInfo, flash: &'a FlashM) {
+ h1 { "Settings" }
+ @FlashDisplay {flash}
+ h2 { @tr(ri.lang, "account") }
+ a.switch_account[href=u_account_login()] { "Switch Account" }
+ form[method="POST", action=u_account_settings()] {
+ label[for="username"] { @tr(ri.lang, "account.username") }
+ input[type="text", id="username", disabled, value=&session.user.name];
+ input[type="submit", disabled, value=tr(ri.lang, "settings.immutable")];
+ }
+ form[method="POST", action=u_account_settings()] {
+ label[for="display_name"] { @tr(ri.lang, "account.display_name") }
+ input[type="text", id="display_name", name="display_name", value=&session.user.display_name];
+ input[type="submit", value=tr(ri.lang, "settings.update")];
+ }
+ form[method="POST", action=u_account_settings()] {
+ label[for="password"] { @tr(ri.lang, "account.password") }
+ 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, "settings.appearance.theme") }
+ @for theme in Theme::ALL {
+ 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")];
+ }
+ form[method="POST", action=u_account_settings()] {
+ label[for="native_secret"] { "Native Secret" }
+ input[type="password", id="native_secret", name="native_secret"];
+ input[type="submit", value=tr(ri.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_str(self.0.to_str())
+ }
+}
+impl markup::Render for A<PlayerKind> {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_str(self.0.to_str())
+ }
+}
+impl RenderAttributeValue for A<Theme> {}
+impl RenderAttributeValue for A<PlayerKind> {}
diff --git a/ui/src/old/admin/import.rs b/ui/src/old/admin/import.rs
new file mode 100644
index 0000000..805d787
--- /dev/null
+++ b/ui/src/old/admin/import.rs
@@ -0,0 +1,46 @@
+/*
+ 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::{FlashM, Page, locale::tr, scaffold::{FlashDisplay, RenderInfo}};
+use jellycommon::routes::u_admin_import_post;
+
+impl Page for AdminImportPage<'_> {
+ fn title(&self) -> String {
+ "Import".to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define!(
+ AdminImportPage<'a>(ri: &'a RenderInfo<'a>, busy: bool, last_import_err: &'a [String], flash: &'a FlashM) {
+ @FlashDisplay { flash }
+ @if *busy {
+ h1 { @tr(ri.lang, "admin.import.running") }
+ noscript { "Live import progress needs javascript." }
+ div[id="admin_import"] {}
+ } else {
+ h1 { @tr(ri.lang, "admin.import.title") }
+ @if !last_import_err.is_empty() {
+ section.message.error {
+ details {
+ summary { p.error { @tr(ri.lang, "admin.import_errors").replace("{n}", &last_import_err.len().to_string()) } }
+ ol { @for e in *last_import_err {
+ li.error { pre.error { @e } }
+ }}
+ }
+ }
+ }
+ form[method="POST", action=u_admin_import_post(true)] {
+ input[type="submit", value=tr(ri.lang, "admin.dashboard.import.inc").to_string()];
+ }
+ form[method="POST", action=u_admin_import_post(false)] {
+ input[type="submit", value=tr(ri.lang, "admin.dashboard.import.full").to_string()];
+ }
+ }
+ }
+);
diff --git a/ui/src/old/admin/log.rs b/ui/src/old/admin/log.rs
new file mode 100644
index 0000000..637158f
--- /dev/null
+++ b/ui/src/old/admin/log.rs
@@ -0,0 +1,127 @@
+/*
+ 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::Page;
+use jellycommon::routes::u_admin_log;
+use markup::raw;
+use std::fmt::Write;
+
+impl Page for ServerLogPage<'_> {
+ fn title(&self) -> String {
+ "Server Log".to_string()
+ }
+ fn class(&self) -> Option<&'static str> {
+ Some("admin_log")
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define! {
+ ServerLogPage<'a>(warnonly: bool, messages: &'a [String]) {
+ h1 { "Server Log" }
+ a[href=u_admin_log(!warnonly)] { @if *warnonly { "Show everything" } else { "Show only warnings" }}
+ code.log[id="log"] {
+ table { @for e in *messages {
+ @raw(e)
+ }}
+ }
+ }
+ ServerLogLine<'a>(e: &'a LogLine) {
+ tr[class=format!("level-{}", e.level).to_ascii_lowercase()] {
+ td.time { @e.time.to_rfc3339() }
+ td.loglevel { @format_level(e.level) }
+ td.module { @e.module }
+ td { @markup::raw(vt100_to_html(&e.message)) }
+ }
+ }
+}
+
+pub fn render_log_line(line: &LogLine) -> String {
+ ServerLogLine { e: line }.to_string()
+}
+
+fn vt100_to_html(s: &str) -> String {
+ let mut out = HtmlOut::default();
+ let mut st = vte::Parser::new();
+ st.advance(&mut out, s.as_bytes());
+ out.s
+}
+
+fn format_level(level: LogLevel) -> impl markup::Render {
+ let (s, c) = match level {
+ LogLevel::Debug => ("DEBUG", "blue"),
+ LogLevel::Error => ("ERROR", "red"),
+ LogLevel::Warn => ("WARN", "yellow"),
+ LogLevel::Info => ("INFO", "green"),
+ LogLevel::Trace => ("TRACE", "lightblue"),
+ };
+ markup::new! { span[style=format!("color:{c}")] {@s} }
+}
+
+#[derive(Default)]
+pub struct HtmlOut {
+ s: String,
+ color: bool,
+}
+impl HtmlOut {
+ pub fn set_color(&mut self, [r, g, b]: [u8; 3]) {
+ self.reset_color();
+ self.color = true;
+ write!(self.s, "<span style=color:#{:02x}{:02x}{:02x}>", r, g, b).unwrap()
+ }
+ pub fn reset_color(&mut self) {
+ if self.color {
+ write!(self.s, "</span>").unwrap();
+ self.color = false;
+ }
+ }
+}
+impl vte::Perform for HtmlOut {
+ fn print(&mut self, c: char) {
+ match c {
+ 'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => self.s.push(c),
+ x => write!(self.s, "&#{};", x as u32).unwrap(),
+ }
+ }
+ fn execute(&mut self, _byte: u8) {}
+ fn hook(&mut self, _params: &vte::Params, _i: &[u8], _ignore: bool, _a: char) {}
+ fn put(&mut self, _byte: u8) {}
+ fn unhook(&mut self) {}
+ fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
+ fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}
+ fn csi_dispatch(
+ &mut self,
+ params: &vte::Params,
+ _intermediates: &[u8],
+ _ignore: bool,
+ action: char,
+ ) {
+ let mut k = params.iter();
+ #[allow(clippy::single_match)]
+ match action {
+ 'm' => match k.next().unwrap_or(&[0]).first().unwrap_or(&0) {
+ c @ (30..=37 | 40..=47) => {
+ let c = if *c >= 40 { *c - 10 } else { *c };
+ self.set_color(match c {
+ 30 => [0, 0, 0],
+ 31 => [255, 0, 0],
+ 32 => [0, 255, 0],
+ 33 => [255, 255, 0],
+ 34 => [0, 0, 255],
+ 35 => [255, 0, 255],
+ 36 => [0, 255, 255],
+ 37 => [255, 255, 255],
+ _ => unreachable!(),
+ });
+ }
+ _ => (),
+ },
+ _ => (),
+ }
+ }
+}
diff --git a/ui/src/old/admin/mod.rs b/ui/src/old/admin/mod.rs
new file mode 100644
index 0000000..f42ba76
--- /dev/null
+++ b/ui/src/old/admin/mod.rs
@@ -0,0 +1,64 @@
+/*
+ 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>
+*/
+
+pub mod import;
+pub mod log;
+pub mod user;
+
+use crate::{FlashM, Page, locale::tr, scaffold::{FlashDisplay, RenderInfo}};
+use jellycommon::routes::{
+ u_admin_import, u_admin_invite_create, u_admin_invite_remove, u_admin_log,
+ u_admin_update_search, u_admin_users,
+};
+
+impl Page for AdminDashboardPage<'_> {
+ fn title(&self) -> String {
+ "Admin Dashboard".to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define!(
+ AdminDashboardPage<'a>(ri: &'a RenderInfo<'a>, busy: Option<&'static str>, flash: &'a FlashM, invites: &'a [String]) {
+ h1 { @tr(ri.lang, "admin.dashboard.title") }
+ @FlashDisplay { flash }
+ ul {
+ li{a[href=u_admin_log(true)] { @tr(ri.lang, "admin.log.warnonly") }}
+ li{a[href=u_admin_log(false)] { @tr(ri.lang, "admin.log.full") }}
+ }
+
+ a[href=u_admin_import()] { h2 { @tr(ri.lang, "admin.import.title") }}
+ @if let Some(text) = busy {
+ section.message { p.warn { @text } }
+ }
+ form[method="POST", action=u_admin_update_search()] {
+ input[type="submit", value=tr(ri.lang, "admin.dashboard.update_search").to_string()];
+ }
+ h2 { @tr(ri.lang, "admin.dashboard.users") }
+ p { a[href=u_admin_users()] { @tr(ri.lang, "admin.dashboard.manage_users") } }
+ h2 { @tr(ri.lang, "admin.dashboard.invites") }
+ form[method="POST", action=u_admin_invite_create()] {
+ input[type="submit", value=tr(ri.lang, "admin.dashboard.create_invite").to_string()];
+ }
+ ul { @for t in *invites {
+ li {
+ form[method="POST", action=u_admin_invite_remove()] {
+ span { @t }
+ input[type="text", name="invite", value=&t, hidden];
+ input[type="submit", value=tr(ri.lang, "admin.dashboard.create_invite").to_string()];
+ }
+ }
+ }}
+
+ // h2 { "Database" }
+ // @match db_stats(&database) {
+ // Ok(s) => { @s }
+ // Err(e) => { pre.error { @format!("{e:?}") } }
+ // }
+ }
+);
diff --git a/ui/src/old/admin/user.rs b/ui/src/old/admin/user.rs
new file mode 100644
index 0000000..e4a8975
--- /dev/null
+++ b/ui/src/old/admin/user.rs
@@ -0,0 +1,81 @@
+/*
+ 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::{FlashM, Page, scaffold::FlashDisplay};
+use jellycommon::routes::{
+ u_admin_user, u_admin_user_permission, u_admin_user_remove, u_admin_users,
+};
+
+impl Page for AdminUserPage<'_> {
+ fn title(&self) -> String {
+ "User Management".to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+impl Page for AdminUsersPage<'_> {
+ fn title(&self) -> String {
+ "User Management".to_string()
+ }
+ fn to_render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define! {
+ AdminUsersPage<'a>(lang: &'a Language, users: &'a [User], flash: &'a FlashM) {
+ h1 { @trs(lang, "admin.users.title") }
+ @FlashDisplay { flash }
+ h2 { @trs(lang, "admin.users.user_list") }
+ ul { @for u in *users {
+ li {
+ a[href=u_admin_user(&u.name)] { @format!("{:?}", u.display_name) " (" @u.name ")" }
+ }
+ }}
+ }
+ AdminUserPage<'a>(lang: &'a Language, user: &'a User, flash: &'a FlashM) {
+ h1 { @format!("{:?}", user.display_name) " (" @user.name ")" }
+ a[href=u_admin_users()] { @trs(lang, "admin.users.return_to_list") }
+ @FlashDisplay { flash }
+ form[method="POST", action=u_admin_user_remove(&user.name)] {
+ // input[type="text", name="name", value=&user.name, hidden];
+ input.danger[type="submit", value="Remove user(!)"];
+ }
+
+ h2 { "Permissions" }
+ @PermissionDisplay { perms: &user.permissions }
+
+ form[method="POST", action=u_admin_user_permission(&user.name)] {
+ // input[type="text", name="name", value=&user.name, hidden];
+ fieldset.perms {
+ legend { "Permission" }
+ @for p in UserPermission::ALL_ENUMERABLE {
+ label {
+ input[type="radio", name="permission", value=serde_json::to_string(p).unwrap()];
+ @format!("{p}")
+ } br;
+ }
+ }
+ fieldset.perms {
+ legend { "State" }
+ label { input[type="radio", name="action", value="unset"]; "Unset" } br;
+ label { input[type="radio", name="action", value="grant"]; "Grant" } br;
+ label { input[type="radio", name="action", value="revoke"]; "Revoke" } br;
+ }
+ input[type="submit", value="Update"];
+ }
+ }
+ PermissionDisplay<'a>(perms: &'a PermissionSet) {
+ ul { @for (perm,grant) in &perms.0 {
+ @if *grant {
+ li[class="perm-grant"] { @format!("Allow {}", perm) }
+ } else {
+ li[class="perm-revoke"] { @format!("Deny {}", perm) }
+ }
+ }}
+ }
+}
diff --git a/ui/src/old/filter_sort.rs b/ui/src/old/filter_sort.rs
new file mode 100644
index 0000000..55cb113
--- /dev/null
+++ b/ui/src/old/filter_sort.rs
@@ -0,0 +1,111 @@
+/*
+ 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::scaffold::RenderInfo;
+
+// const SORT_CATS: &[(&str, &[(SortProperty, &str)])] = {
+// &[
+// (
+// "filter_sort.sort.general",
+// &[(Title, "node.title"), (ReleaseDate, "node.release_date")],
+// ),
+// ("filter_sort.sort.media", &[(Duration, "media.duration")]),
+// (
+// "filter_sort.sort.rating",
+// &[
+// (RatingImdb, "rating.imdb"),
+// (RatingTmdb, "rating.tmdb"),
+// (RatingMetacritic, "rating.metacritic"),
+// (RatingRottenTomatoes, "rating.rotten_tomatoes"),
+// (RatingYoutubeFollowers, "rating.youtube_followers"),
+// (RatingYoutubeLikes, "rating.youtube_likes"),
+// (RatingYoutubeViews, "rating.youtube_views"),
+// (RatingUser, "filter_sort.sort.rating.user"),
+// (
+// RatingLikesDivViews,
+// "filter_sort.sort.rating.likes_div_views",
+// ),
+// ],
+// ),
+// ]
+// };
+// const FILTER_CATS: &[(&str, &[(FilterProperty, &str)])] = {
+// &[
+// (
+// "filter_sort.filter.kind",
+// &[
+// (KindMovie, "kind.movie"),
+// (KindVideo, "kind.video"),
+// (KindShortFormVideo, "kind.short_form_video"),
+// (KindMusic, "kind.music"),
+// (KindCollection, "kind.collection"),
+// (KindChannel, "kind.channel"),
+// (KindShow, "kind.show"),
+// (KindSeries, "kind.series"),
+// (KindSeason, "kind.season"),
+// (KindEpisode, "kind.episode"),
+// ],
+// ),
+// (
+// "filter_sort.filter.federation",
+// &[
+// (FederationLocal, "federation.local"),
+// (FederationRemote, "federation.remote"),
+// ],
+// ),
+// (
+// "filter_sort.filter.watched",
+// &[
+// (Watched, "watched.watched"),
+// (Unwatched, "watched.none"),
+// (WatchProgress, "watched.progress"),
+// ],
+// ),
+// ]
+// };
+
+markup::define! {
+ NodeFilterSortForm<'a>(ri: &'a RenderInfo<'a>, f: &'a NodeFilterSort) {
+ details.filtersort[open=f.filter_kind.is_some() || f.sort_by.is_some()] {
+ summary { "Filter and Sort" }
+ form[method="GET", action=""] {
+ fieldset.filter {
+ legend { "Filter" }
+ // .categories {
+ // @for (cname, cat) in FILTER_CATS {
+ // .category {
+ // h3 { @trs(lang, cname) }
+ // @for (value, label) in *cat {
+ // 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;
+ // }
+ // }
+ // }
+ // }
+ }
+ fieldset.sortby {
+ legend { "Sort" }
+ // .categories {
+ // @for (cname, cat) in SORT_CATS {
+ // .category {
+ // h3 { @trs(lang, cname) }
+ // @for (value, label) in *cat {
+ // label { input[type="radio", name="sort_by", value=A(*value), checked=Some(value)==f.sort_by.as_ref()]; @trs(lang, label) } br;
+ // }
+ // }
+ // }
+ // }
+ }
+ fieldset.sortorder {
+ legend { "Sort Order" }
+ // @for (value, label) in [(Ascending, "filter_sort.order.asc"), (Descending, "filter_sort.order.desc")] {
+ // 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" }
+ }
+ }
+ }
+}
diff --git a/ui/src/old/home.rs b/ui/src/old/home.rs
new file mode 100644
index 0000000..a3088c8
--- /dev/null
+++ b/ui/src/old/home.rs
@@ -0,0 +1,33 @@
+/*
+ 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::{CONF, Page, locale::tr, node_card::NodeCard, scaffold::RenderInfo};
+use markup::DynRender;
+
+markup::define! {
+ HomePage<'a>(ri: RenderInfo<'a>, r: ApiHomeResponse) {
+ h2 { @tr(ri.lang, "home.bin.root").replace("{title}", &CONF.brand) }
+ ul.children.hlist {@for nodeu in &r.toplevel {
+ li { @NodeCard { ri, nodeu } }
+ }}
+ @for (name, nodes) in &r.categories {
+ // @if !nodes.is_empty() {
+ // h2 { @trs(lang, name) }
+ // ul.children.hlist {@for (node, udata) in nodes {
+ // li { @NodeCard { node, udata, lang } }
+ // }}
+ // }
+ }
+ }
+}
+
+impl Page for HomePage<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "home").to_string()
+ }
+ fn to_render(&self) -> DynRender<'_> {
+ markup::new!(@self)
+ }
+}
diff --git a/ui/src/old/items.rs b/ui/src/old/items.rs
new file mode 100644
index 0000000..529a5d6
--- /dev/null
+++ b/ui/src/old/items.rs
@@ -0,0 +1,37 @@
+/*
+ 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::{Page, locale::tr, scaffold::RenderInfo};
+use markup::DynRender;
+
+markup::define! {
+ ItemsPage<'a>(ri: &'a RenderInfo<'a>, r: ApiItemsResponse, filter: &'a NodeFilterSort, page: usize) {
+ .page.dir {
+ h1 { "All Items" }
+ // @NodeFilterSortForm { f: filter, lang }
+ // ul.children { @for (node, udata) in &r.items {
+ // li {@NodeCard { node, udata, lang }}
+ // }}
+ // p.pagecontrols {
+ // span.current { @tr(**lang, "page.curr").replace("{cur}", &(page + 1).to_string()).replace("{max}", &r.pages.to_string()) " " }
+ // @if *page > 0 {
+ // a.prev[href=u_items_filter(page - 1, filter)] { @trs(lang, "page.prev") } " "
+ // }
+ // @if page + 1 < r.pages {
+ // a.next[href=u_items_filter(page + 1, filter)] { @trs(lang, "page.next") }
+ // }
+ // }
+ }
+ }
+}
+
+impl Page for ItemsPage<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "home").to_string()
+ }
+ fn to_render(&self) -> DynRender<'_> {
+ markup::new!(@self)
+ }
+}
diff --git a/ui/src/old/node_card.rs b/ui/src/old/node_card.rs
new file mode 100644
index 0000000..f87f490
--- /dev/null
+++ b/ui/src/old/node_card.rs
@@ -0,0 +1,59 @@
+/*
+ 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::{
+ node_page::{NodeUdata, aspect_class},
+ scaffold::RenderInfo,
+};
+use jellycommon::*;
+
+markup::define! {
+ NodeCard<'a>(ri: &'a RenderInfo<'a>, nodeu: NodeUdata<'a>) {
+ @let cls = format!("node card poster {}", aspect_class(nodeu.node.get(NO_KIND).unwrap_or(KIND_COLLECTION)));
+ div[class=cls] {
+ // .poster {
+ // a[href=u_node_slug(&node.slug)] {
+ // img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
+ // }
+ // .cardhover.item {
+ // @if node.media.is_some() {
+ // a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
+ // }
+ // @Props { node, udata, full: false, lang }
+ // }
+ // }
+ // div.title {
+ // a[href=u_node_slug(&node.slug)] {
+ // @node.title
+ // }
+ // }
+ // div.subtitle {
+ // span {
+ // @node.subtitle
+ // }
+ // }
+ }
+ }
+ NodeCardWide<'a>(ri: &'a RenderInfo<'a>, nodeu: NodeUdata<'a>) {
+ div[class="node card widecard poster"] {
+ // div[class=&format!("poster {}", aspect_class(node.kind))] {
+ // a[href=u_node_slug(&node.slug)] {
+ // img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
+ // }
+ // .cardhover.item {
+ // @if node.media.is_some() {
+ // a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
+ // }
+ // }
+ // }
+ // div.details {
+ // a.title[href=u_node_slug(&node.slug)] { @node.title }
+ // @Props { node, udata, full: false, lang }
+ // span.overview { @node.description }
+ // }
+ }
+ }
+}
diff --git a/ui/src/old/search.rs b/ui/src/old/search.rs
new file mode 100644
index 0000000..0eb34b9
--- /dev/null
+++ b/ui/src/old/search.rs
@@ -0,0 +1,38 @@
+/*
+ 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::{Page, locale::tr, node_card::NodeCard, scaffold::RenderInfo};
+use markup::DynRender;
+
+impl Page for SearchPage<'_> {
+ fn title(&self) -> String {
+ tr(self.ri.lang, "search.title").to_string()
+ }
+ fn class(&self) -> Option<&'static str> {
+ Some("search")
+ }
+ fn to_render(&self) -> DynRender<'_> {
+ markup::new!(@self)
+ }
+}
+
+markup::define! {
+ SearchPage<'a>(ri: &'a RenderInfo<'a>, r: Option<ApiSearchResponse>, query: &'a Option<String>) {
+ h1 { @tr(ri.lang, "search.title") }
+ form[action="", method="GET"] {
+ input[type="text", name="query", placeholder=tr(ri.lang, "search.placeholder"), value=&query];
+ input[type="submit", value="Search"];
+ }
+ @if let Some(r) = &r {
+ h2 { @tr(ri.lang, "search.results.title") }
+ p.stats { @tr(ri.lang, "search.results.stats").replace("{count}", &r.count.to_string()).replace("{dur}", &format!("{:?}", r.duration)) }
+ ul.children {@for nodeu in r.results.iter() {
+ li { @NodeCard { ri, nodeu } }
+ }}
+ // TODO pagination
+ }
+ }
+}