diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-29 15:19:36 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-29 15:19:36 +0200 |
commit | f73aa32549743b2967160d38c1622199c41524a4 (patch) | |
tree | 0fa290fbf9b14d7bfd3803f8cc4618c6c9829330 /ui | |
parent | f62c7f2a8cc143454779dc99334ca9fc80ddabd5 (diff) | |
download | jellything-f73aa32549743b2967160d38c1622199c41524a4.tar jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.bz2 jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.zst |
aaaaaaa
Diffstat (limited to 'ui')
-rw-r--r-- | ui/Cargo.toml | 1 | ||||
-rw-r--r-- | ui/src/account/mod.rs | 27 | ||||
-rw-r--r-- | ui/src/admin/log.rs | 117 | ||||
-rw-r--r-- | ui/src/admin/mod.rs | 74 | ||||
-rw-r--r-- | ui/src/admin/user.rs | 31 | ||||
-rw-r--r-- | ui/src/lib.rs | 1 |
6 files changed, 249 insertions, 2 deletions
diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 3868b1f..0a2fb5a 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -9,3 +9,4 @@ jellycommon = { path = "../common" } humansize = "2.1.3" serde = { version = "1.0.217", features = ["derive", "rc"] } serde_json = "1.0.140" +vte = "0.14.1" diff --git a/ui/src/account/mod.rs b/ui/src/account/mod.rs new file mode 100644 index 0000000..bc8d3ce --- /dev/null +++ b/ui/src/account/mod.rs @@ -0,0 +1,27 @@ +/* + 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::routes::u_account_login; + +markup::define! { + AccountRegister<'a>(lang: &'a Language) { + form.account[method="POST", action=""] { + h1 { @trs(&lang, "account.register") } + + label[for="inp-invitation"] { @trs(&lang, "account.register.invitation") } + input[type="text", id="inp-invitation", name="invitation"]; br; + + label[for="inp-username"] { @trs(&lang, "account.username") } + input[type="text", id="inp-username", name="username"]; br; + label[for="inp-password"] { @trs(&lang, "account.password") } + input[type="password", id="inp-password", name="password"]; br; + + input[type="submit", value=&*tr(**lang, "account.register.submit")]; + + p { @trs(&lang, "account.register.login") " " a[href=u_account_login()] { @trs(&lang, "account.register.login_here") } } + } + } +} diff --git a/ui/src/admin/log.rs b/ui/src/admin/log.rs new file mode 100644 index 0000000..a69bdfa --- /dev/null +++ b/ui/src/admin/log.rs @@ -0,0 +1,117 @@ +/* + 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 jellycommon::{ + api::{LogLevel, LogLine}, + routes::u_admin_log, +}; +use markup::raw; +use std::fmt::Write; + +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/admin/mod.rs b/ui/src/admin/mod.rs index 292e445..74a5e1a 100644 --- a/ui/src/admin/mod.rs +++ b/ui/src/admin/mod.rs @@ -5,3 +5,77 @@ */ pub mod user; +pub mod log; + +use crate::{Page, locale::Language, scaffold::FlashDisplay}; +use jellycommon::routes::{ + u_admin_import, u_admin_invite_create, u_admin_invite_remove, u_admin_log, + u_admin_transcode_posters, 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>(lang: &'a Language, busy: Option<&'static str>, last_import_err: &'a [String], flash: Option<Result<String, String>>, invites: &'a [String]) { + h1 { "Admin Panel" } + @FlashDisplay { flash: flash.clone() } + @if !last_import_err.is_empty() { + section.message.error { + details { + summary { p.error { @format!("The last import resulted in {} errors:", last_import_err.len()) } } + ol { @for e in *last_import_err { + li.error { pre.error { @e } } + }} + } + } + } + ul { + li{a[href=u_admin_log(true)] { "Server Log (Warnings only)" }} + li{a[href=u_admin_log(false)] { "Server Log (Full) " }} + } + h2 { "Library" } + @if let Some(text) = busy { + section.message { p.warn { @text } } + } + form[method="POST", action=u_admin_import(true)] { + input[type="submit", disabled=busy.is_some(), value="Start incremental import"]; + } + form[method="POST", action=u_admin_import(false)] { + input[type="submit", disabled=busy.is_some(), value="Start full import"]; + } + form[method="POST", action=u_admin_transcode_posters()] { + input[type="submit", disabled=busy.is_some(), value="Transcode all posters with low resolution"]; + } + form[method="POST", action=u_admin_update_search()] { + input[type="submit", value="Regenerate full-text search index"]; + } + h2 { "Users" } + p { a[href=u_admin_users()] "Manage Users" } + h2 { "Invitations" } + form[method="POST", action=u_admin_invite_create()] { + input[type="submit", value="Generate new invite code"]; + } + 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="Invalidate"]; + } + } + }} + + // h2 { "Database" } + // @match db_stats(&database) { + // Ok(s) => { @s } + // Err(e) => { pre.error { @format!("{e:?}") } } + // } + } +); diff --git a/ui/src/admin/user.rs b/ui/src/admin/user.rs index 9878803..613fc08 100644 --- a/ui/src/admin/user.rs +++ b/ui/src/admin/user.rs @@ -4,13 +4,40 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{locale::Language, scaffold::FlashDisplay}; +use crate::{Page, locale::Language, scaffold::FlashDisplay}; use jellycommon::{ - routes::{u_admin_user_permission, u_admin_user_remove, u_admin_users}, + routes::{u_admin_user, u_admin_user_permission, u_admin_user_remove, u_admin_users}, user::{PermissionSet, User, UserPermission}, }; +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: Option<Result<String, String>>) { + h1 { "User Management" } + @FlashDisplay { flash: flash.clone() } + h2 { "All Users" } + 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: Option<Result<String, String>>) { h1 { @format!("{:?}", user.display_name) " (" @user.name ")" } a[href=u_admin_users()] "Back to the User List" diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 2521054..8a4b950 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -16,6 +16,7 @@ pub mod settings; pub mod stats; pub mod items; pub mod admin; +pub mod account; use locale::Language; use markup::DynRender; |