aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-29 15:19:36 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-29 15:19:36 +0200
commitf73aa32549743b2967160d38c1622199c41524a4 (patch)
tree0fa290fbf9b14d7bfd3803f8cc4618c6c9829330 /ui
parentf62c7f2a8cc143454779dc99334ca9fc80ddabd5 (diff)
downloadjellything-f73aa32549743b2967160d38c1622199c41524a4.tar
jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.bz2
jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.zst
aaaaaaa
Diffstat (limited to 'ui')
-rw-r--r--ui/Cargo.toml1
-rw-r--r--ui/src/account/mod.rs27
-rw-r--r--ui/src/admin/log.rs117
-rw-r--r--ui/src/admin/mod.rs74
-rw-r--r--ui/src/admin/user.rs31
-rw-r--r--ui/src/lib.rs1
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;