aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock58
-rw-r--r--Cargo.toml1
-rw-r--r--common/src/api.rs3
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/api.rs8
-rw-r--r--server/src/compat/youtube.rs74
-rw-r--r--server/src/logic/playersync.rs6
-rw-r--r--server/src/logic/stream.rs133
-rw-r--r--server/src/logic/userdata.rs101
-rw-r--r--server/src/main.rs2
-rw-r--r--server/src/request_info.rs9
-rw-r--r--server/src/routes.rs92
-rw-r--r--server/src/ui/account/mod.rs22
-rw-r--r--server/src/ui/assets.rs77
-rw-r--r--server/src/ui/error.rs36
-rw-r--r--server/src/ui/mod.rs79
-rw-r--r--server/src/ui/node.rs44
-rw-r--r--server/src/ui/search.rs41
-rw-r--r--server/src/ui/stats.rs20
-rw-r--r--ui/Cargo.toml2
-rw-r--r--ui/src/components/login.rs43
-rw-r--r--ui/src/components/mod.rs16
-rw-r--r--ui/src/old/account/mod.rs103
23 files changed, 392 insertions, 580 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7150d19..2b2f706 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -46,20 +46,6 @@ dependencies = [
]
[[package]]
-name = "aes-gcm"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
-dependencies = [
- "aead",
- "aes",
- "cipher",
- "ctr",
- "ghash",
- "subtle",
-]
-
-[[package]]
name = "aes-gcm-siv"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -666,13 +652,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
- "aes-gcm",
- "base64",
- "hkdf",
"percent-encoding",
- "rand 0.8.5",
- "sha2",
- "subtle",
"time",
"version_check",
]
@@ -1257,16 +1237,6 @@ dependencies = [
]
[[package]]
-name = "ghash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
-dependencies = [
- "opaque-debug",
- "polyval",
-]
-
-[[package]]
name = "gif"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1337,24 +1307,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "hkdf"
-version = "0.12.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
-dependencies = [
- "hmac",
-]
-
-[[package]]
-name = "hmac"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
-dependencies = [
- "digest",
-]
-
-[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2292,16 +2244,18 @@ dependencies = [
[[package]]
name = "markup"
-version = "0.15.0"
-source = "git+https://github.com/metamuffin/markup.rs?rev=2ee9aee#2ee9aeeb7654ede4dbdd9c9bc7f57cab888ef12f"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be10fa8085360758d551c6577b0692c9e7614d2d743036cf2fa452476a3e0569"
dependencies = [
"markup-proc-macro",
]
[[package]]
name = "markup-proc-macro"
-version = "0.15.0"
-source = "git+https://github.com/metamuffin/markup.rs?rev=2ee9aee#2ee9aeeb7654ede4dbdd9c9bc7f57cab888ef12f"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee486981a2fe832d4deb4d23a35508c111ef7070758177ce3580eb2a384fab2f"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index 42aac47..8a5d846 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ members = [
"cache/tools",
]
resolver = "3"
+default-members = ["server"]
[workspace.dependencies]
log = "0.4.28"
diff --git a/common/src/api.rs b/common/src/api.rs
index 9040852..415ef76 100644
--- a/common/src/api.rs
+++ b/common/src/api.rs
@@ -20,6 +20,9 @@ fields! {
VIEW_PLAYER: u64 = 2028 "player";
VIEW_STATGROUP: Object = 2041 "statgroup";
VIEW_STATTEXT: Object = 2042 "stattext";
+ VIEW_ACCOUNT_LOGIN: () = 2043 "account_login";
+ VIEW_ACCOUNT_LOGOUT: () = 2043 "account_logout";
+ VIEW_ACCOUNT_SET_PASSWORD: &str = 2044 "account_set_password";
NKU_NODE: Object = 2025 "node";
NKU_UDATA: Object = 2026 "udata";
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 534dce5..f12b8fe 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -25,7 +25,7 @@ env_logger = "0.11.8"
futures = "0.3.31"
log = { workspace = true }
rand = "0.9.2"
-rocket = { version = "0.5", features = ["secrets", "json"] }
+rocket = { version = "0.5", features = ["json"] }
rocket_ws = "0.1"
serde = { version = "1.0.228", features = ["derive", "rc"] }
serde_json = "1.0.145"
diff --git a/server/src/api.rs b/server/src/api.rs
index 2b3d016..9d16433 100644
--- a/server/src/api.rs
+++ b/server/src/api.rs
@@ -4,6 +4,7 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use super::ui::error::MyResult;
+use crate::request_info::RequestInfo;
use rocket::{get, response::Redirect, serde::json::Json};
#[get("/api")]
@@ -38,7 +39,8 @@ pub fn r_version() -> &'static str {
// }
#[get("/nodes_modified?<since>")]
-pub fn r_nodes_modified_since(session: A<Session>, since: u64) -> MyResult<Json<Vec<NodeID>>> {
- let nodes = get_nodes_modified_since(&session.0, since)?;
- Ok(Json(nodes))
+pub fn r_nodes_modified_since(ri: RequestInfo<'_>, since: u64) -> MyResult<Json<Vec<String>>> {
+ // let nodes = get_nodes_modified_since(&session.0, since)?;
+ // Ok(Json(nodes))
+ todo!()
}
diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs
index 5e86014..e511d9b 100644
--- a/server/src/compat/youtube.rs
+++ b/server/src/compat/youtube.rs
@@ -3,53 +3,47 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{request_info::A, ui::error::MyResult};
-use anyhow::anyhow;
-use jellycommon::{
- routes::{u_node_slug, u_node_slug_player},
- IdentifierType,
-};
-use jellylogic::{
- node::{get_node_by_eid, node_id_to_slug},
- session::Session,
-};
+use crate::{request_info::RequestInfo, ui::error::MyResult};
use rocket::{get, response::Redirect};
#[get("/watch?<v>")]
-pub fn r_youtube_watch(session: A<Session>, v: &str) -> MyResult<Redirect> {
- if v.len() != 11 {
- Err(anyhow!("video id length incorrect"))?
- }
- let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else {
- Err(anyhow!("element not found"))?
- };
- let slug = node_id_to_slug(&session.0, id)?;
- Ok(Redirect::to(u_node_slug_player(&slug)))
+pub fn r_youtube_watch(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> {
+ // if v.len() != 11 {
+ // Err(anyhow!("video id length incorrect"))?
+ // }
+ // let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else {
+ // Err(anyhow!("element not found"))?
+ // };
+ // let slug = node_id_to_slug(&session.0, id)?;
+ // Ok(Redirect::to(u_node_slug_player(&slug)))
+ todo!()
}
#[get("/channel/<id>")]
-pub fn r_youtube_channel(session: A<Session>, id: &str) -> MyResult<Redirect> {
- let Some(id) = (if id.starts_with("UC") {
- get_node_by_eid(&session.0, IdentifierType::YoutubeChannel, id)?
- } else if id.starts_with("@") {
- get_node_by_eid(&session.0, IdentifierType::YoutubeChannelHandle, id)?
- } else {
- Err(anyhow!("unknown channel id format"))?
- }) else {
- Err(anyhow!("channel not found"))?
- };
- let slug = node_id_to_slug(&session.0, id)?;
- Ok(Redirect::to(u_node_slug(&slug)))
+pub fn r_youtube_channel(ri: RequestInfo<'_>, id: &str) -> MyResult<Redirect> {
+ // let Some(id) = (if id.starts_with("UC") {
+ // get_node_by_eid(&session.0, IdentifierType::YoutubeChannel, id)?
+ // } else if id.starts_with("@") {
+ // get_node_by_eid(&session.0, IdentifierType::YoutubeChannelHandle, id)?
+ // } else {
+ // Err(anyhow!("unknown channel id format"))?
+ // }) else {
+ // Err(anyhow!("channel not found"))?
+ // };
+ // let slug = node_id_to_slug(&session.0, id)?;
+ // Ok(Redirect::to(u_node_slug(&slug)))
+ todo!()
}
#[get("/embed/<v>")]
-pub fn r_youtube_embed(session: A<Session>, v: &str) -> MyResult<Redirect> {
- if v.len() != 11 {
- Err(anyhow!("video id length incorrect"))?
- }
- let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else {
- Err(anyhow!("element not found"))?
- };
- let slug = node_id_to_slug(&session.0, id)?;
- Ok(Redirect::to(u_node_slug_player(&slug)))
+pub fn r_youtube_embed(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> {
+ // if v.len() != 11 {
+ // Err(anyhow!("video id length incorrect"))?
+ // }
+ // let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else {
+ // Err(anyhow!("element not found"))?
+ // };
+ // let slug = node_id_to_slug(&session.0, id)?;
+ // Ok(Redirect::to(u_node_slug_player(&slug)))
+ todo!()
}
diff --git a/server/src/logic/playersync.rs b/server/src/logic/playersync.rs
index 6c1f9f4..71e2809 100644
--- a/server/src/logic/playersync.rs
+++ b/server/src/logic/playersync.rs
@@ -2,12 +2,12 @@ use anyhow::bail;
use chashmap::CHashMap;
use futures::{SinkExt, StreamExt};
use log::warn;
-use rocket::{get, State};
-use rocket_ws::{stream::DuplexStream, Channel, Message, WebSocket};
+use rocket::{State, get};
+use rocket_ws::{Channel, Message, WebSocket, stream::DuplexStream};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::{self, Sender};
-use crate::request_info::cors::Cors;
+use crate::responders::cors::Cors;
#[derive(Default)]
pub struct PlayersyncChannels {
diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs
index 55d6850..430c10c 100644
--- a/server/src/logic/stream.rs
+++ b/server/src/logic/stream.rs
@@ -3,18 +3,16 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{request_info::A, ui::error::MyError};
-use anyhow::{anyhow, Result};
-use jellycommon::{api::NodeFilterSort, stream::StreamSpec, NodeID, TrackSource};
-use jellylogic::{node::get_node, session::Session};
+use crate::{request_info::RequestInfo, ui::error::MyError};
+use anyhow::{Result, anyhow};
+use jellycommon::stream::StreamSpec;
use jellystream::SMediaInfo;
use log::{info, warn};
use rocket::{
- get, head,
+ Either, Request, Response, get, head,
http::{Header, Status},
request::{self, FromRequest},
response::{self, Redirect, Responder},
- Either, Request, Response,
};
use std::{
collections::{BTreeMap, BTreeSet},
@@ -22,14 +20,14 @@ use std::{
sync::Arc,
};
use tokio::{
- io::{duplex, DuplexStream},
+ io::{DuplexStream, duplex},
task::spawn_blocking,
};
use tokio_util::io::SyncIoBridge;
#[head("/n/<_id>/stream?<spec..>")]
pub async fn r_stream_head(
- _sess: A<Session>,
+ _sess: RequestInfo<'_>,
_id: &str,
spec: BTreeMap<String, String>,
) -> Result<Either<StreamResponse, Redirect>, MyError> {
@@ -45,27 +43,27 @@ pub async fn r_stream_head(
#[get("/n/<id>/stream?<spec..>")]
pub async fn r_stream(
- session: A<Session>,
+ session: RequestInfo<'_>,
id: &str,
range: Option<RequestRange>,
spec: BTreeMap<String, String>,
) -> Result<Either<StreamResponse, RedirectResponse>, MyError> {
let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?;
- // TODO perm
- let node = get_node(
- &session.0,
- NodeID::from_slug(id),
- false,
- false,
- NodeFilterSort::default(),
- )?
- .node;
+ // // TODO perm
+ // let node = get_node(
+ // &session.0,
+ // NodeID::from_slug(id),
+ // false,
+ // false,
+ // NodeFilterSort::default(),
+ // )?
+ // .node;
- let media = Arc::new(
- node.media
- .clone()
- .ok_or(anyhow!("item does not contain media"))?,
- );
+ // let media = Arc::new(
+ // node.media
+ // .clone()
+ // .ok_or(anyhow!("item does not contain media"))?,
+ // );
// TODO its unclear how requests with multiple tracks should be handled.
// if spec.track.len() == 1 {
@@ -115,55 +113,56 @@ pub async fn r_stream(
// }
// }
- info!(
- "stream request (range={})",
- range
- .as_ref()
- .map(|r| r.to_cr_hv())
- .unwrap_or("none".to_string())
- );
+ // info!(
+ // "stream request (range={})",
+ // range
+ // .as_ref()
+ // .map(|r| r.to_cr_hv())
+ // .unwrap_or("none".to_string())
+ // );
- let urange = match &range {
- Some(r) => {
- let r = r.0.first().unwrap_or(&(None..None));
- r.start.unwrap_or(0)..r.end.unwrap_or(u64::MAX)
- }
- None => 0..u64::MAX,
- };
+ // let urange = match &range {
+ // Some(r) => {
+ // let r = r.0.first().unwrap_or(&(None..None));
+ // r.start.unwrap_or(0)..r.end.unwrap_or(u64::MAX)
+ // }
+ // None => 0..u64::MAX,
+ // };
- let head = jellystream::stream_head(&spec);
+ // let head = jellystream::stream_head(&spec);
- let mut sources = BTreeSet::new();
- for t in &media.tracks {
- if let TrackSource::Local(path, _) = &t.source {
- sources.insert(path.to_owned());
- }
- }
- let media = Arc::new(SMediaInfo {
- files: sources,
- title: node.title.clone(),
- });
+ // let mut sources = BTreeSet::new();
+ // for t in &media.tracks {
+ // if let TrackSource::Local(path, _) = &t.source {
+ // sources.insert(path.to_owned());
+ // }
+ // }
+ // let media = Arc::new(SMediaInfo {
+ // files: sources,
+ // title: node.title.clone(),
+ // });
- // TODO cleaner solution needed
- let mut reader = match spawn_blocking(move || jellystream::stream(media, spec, urange))
- .await
- .unwrap()
- {
- Ok(o) => o,
- Err(e) => {
- warn!("stream error: {e:?}");
- Err(e)?
- }
- };
- let (stream_write, stream_read) = duplex(4096);
- spawn_blocking(move || std::io::copy(&mut reader, &mut SyncIoBridge::new(stream_write)));
+ // // TODO cleaner solution needed
+ // let mut reader = match spawn_blocking(move || jellystream::stream(media, spec, urange))
+ // .await
+ // .unwrap()
+ // {
+ // Ok(o) => o,
+ // Err(e) => {
+ // warn!("stream error: {e:?}");
+ // Err(e)?
+ // }
+ // };
+ // let (stream_write, stream_read) = duplex(4096);
+ // spawn_blocking(move || std::io::copy(&mut reader, &mut SyncIoBridge::new(stream_write)));
- Ok(Either::Left(StreamResponse {
- stream: stream_read,
- range,
- advertise_range: head.range_supported,
- content_type: head.content_type,
- }))
+ // Ok(Either::Left(StreamResponse {
+ // stream: stream_read,
+ // range,
+ // advertise_range: head.range_supported,
+ // content_type: head.content_type,
+ // }))
+ todo!()
}
pub struct RedirectResponse(String);
diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs
index 104de4a..ea96507 100644
--- a/server/src/logic/userdata.rs
+++ b/server/src/logic/userdata.rs
@@ -3,23 +3,10 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{request_info::A, ui::error::MyResult};
-use jellycommon::{
- api::NodeFilterSort,
- routes::u_node_id,
- user::{NodeUserData, WatchedState},
- NodeID,
-};
-use jellylogic::{
- node::{
- get_node, update_node_userdata_rating, update_node_userdata_watched,
- update_node_userdata_watched_progress,
- },
- session::Session,
-};
+use crate::ui::error::MyResult;
use rocket::{
- form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField,
- UriDisplayQuery,
+ FromForm, FromFormField, UriDisplayQuery, form::Form, get, post, response::Redirect,
+ serde::json::Json,
};
#[derive(Debug, FromFormField, UriDisplayQuery)]
@@ -29,48 +16,48 @@ pub enum UrlWatchedState {
Pending,
}
-#[get("/n/<id>/userdata")]
-pub fn r_node_userdata(session: A<Session>, id: A<NodeID>) -> MyResult<Json<NodeUserData>> {
- let u = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?.userdata;
- Ok(Json(u))
-}
+// #[get("/n/<id>/userdata")]
+// pub fn r_node_userdata(session: A<Session>, id: A<NodeID>) -> MyResult<Json<NodeUserData>> {
+// let u = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?.userdata;
+// Ok(Json(u))
+// }
-#[post("/n/<id>/watched?<state>")]
-pub async fn r_node_userdata_watched(
- session: A<Session>,
- id: A<NodeID>,
- state: UrlWatchedState,
-) -> MyResult<Redirect> {
- update_node_userdata_watched(
- &session.0,
- id.0,
- match state {
- UrlWatchedState::None => WatchedState::None,
- UrlWatchedState::Watched => WatchedState::Watched,
- UrlWatchedState::Pending => WatchedState::Pending,
- },
- )?;
- Ok(Redirect::found(u_node_id(id.0)))
-}
+// #[post("/n/<id>/watched?<state>")]
+// pub async fn r_node_userdata_watched(
+// session: A<Session>,
+// id: A<NodeID>,
+// state: UrlWatchedState,
+// ) -> MyResult<Redirect> {
+// update_node_userdata_watched(
+// &session.0,
+// id.0,
+// match state {
+// UrlWatchedState::None => WatchedState::None,
+// UrlWatchedState::Watched => WatchedState::Watched,
+// UrlWatchedState::Pending => WatchedState::Pending,
+// },
+// )?;
+// Ok(Redirect::found(u_node_id(id.0)))
+// }
-#[derive(FromForm)]
-pub struct UpdateRating {
- #[field(validate = range(-10..=10))]
- rating: i32,
-}
+// #[derive(FromForm)]
+// pub struct UpdateRating {
+// #[field(validate = range(-10..=10))]
+// rating: i32,
+// }
-#[post("/n/<id>/update_rating", data = "<form>")]
-pub async fn r_node_userdata_rating(
- session: A<Session>,
- id: A<NodeID>,
- form: Form<UpdateRating>,
-) -> MyResult<Redirect> {
- update_node_userdata_rating(&session.0, id.0, form.rating)?;
- Ok(Redirect::found(u_node_id(id.0)))
-}
+// #[post("/n/<id>/update_rating", data = "<form>")]
+// pub async fn r_node_userdata_rating(
+// session: A<Session>,
+// id: A<NodeID>,
+// form: Form<UpdateRating>,
+// ) -> MyResult<Redirect> {
+// update_node_userdata_rating(&session.0, id.0, form.rating)?;
+// Ok(Redirect::found(u_node_id(id.0)))
+// }
-#[post("/n/<id>/progress?<t>")]
-pub async fn r_node_userdata_progress(session: A<Session>, id: A<NodeID>, t: f64) -> MyResult<()> {
- update_node_userdata_watched_progress(&session.0, id.0, t)?;
- Ok(())
-}
+// #[post("/n/<id>/progress?<t>")]
+// pub async fn r_node_userdata_progress(session: A<Session>, id: A<NodeID>, t: f64) -> MyResult<()> {
+// update_node_userdata_watched_progress(&session.0, id.0, t)?;
+// Ok(())
+// }
diff --git a/server/src/main.rs b/server/src/main.rs
index bd9901a..aab335a 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -48,7 +48,7 @@ async fn main() {
}
}
-pub(crate) struct State {
+pub struct State {
pub config: Config,
pub cache: Cache,
diff --git a/server/src/request_info.rs b/server/src/request_info.rs
index 3468c58..0f2fd3a 100644
--- a/server/src/request_info.rs
+++ b/server/src/request_info.rs
@@ -9,7 +9,8 @@ use crate::{
auth::token_to_user,
ui::error::{MyError, MyResult},
};
-use jellycommon::jellyobject::ObjectBuffer;
+use anyhow::anyhow;
+use jellycommon::jellyobject::{Object, ObjectBuffer};
use jellyui::RenderInfo;
use rocket::{
Request, async_trait,
@@ -46,6 +47,12 @@ impl<'a> RequestInfo<'a> {
state: state.clone(),
})
}
+ pub fn require_user(&'a self) -> MyResult<Object<'a>> {
+ self.user
+ .as_ref()
+ .map(|u| u.as_object())
+ .ok_or(MyError(anyhow!("user required")))
+ }
pub fn render_info(&'a self) -> RenderInfo<'a> {
RenderInfo {
lang: self.lang,
diff --git a/server/src/routes.rs b/server/src/routes.rs
index 19c76b2..01d0081 100644
--- a/server/src/routes.rs
+++ b/server/src/routes.rs
@@ -10,32 +10,12 @@ use crate::{
logic::{
playersync::{PlayersyncChannels, r_playersync},
stream::r_stream,
- userdata::{
- r_node_userdata, r_node_userdata_progress, r_node_userdata_rating,
- r_node_userdata_watched,
- },
},
ui::{
- account::{
- r_account_login, r_account_login_post, r_account_logout, r_account_logout_post,
- r_account_register, r_account_register_post,
- settings::{r_account_settings, r_account_settings_post},
- },
- admin::{
- import::{r_admin_import, r_admin_import_post, r_admin_import_stream},
- log::{r_admin_log, r_admin_log_stream},
- r_admin_dashboard, r_admin_invite, r_admin_remove_invite, r_admin_update_search,
- user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users},
- },
- assets::{r_image, r_item_poster, r_node_thumbnail},
+ assets::r_image,
error::{r_api_catch, r_catch},
- home::r_home,
- items::r_items,
node::r_node,
- player::r_player,
- r_favicon, r_index,
- search::r_search,
- stats::r_stats,
+ r_favicon,
style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style},
},
};
@@ -52,7 +32,7 @@ macro_rules! uri {
};
}
-pub fn build_rocket(state: Arc<State>) -> Rocket<Build> {
+pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> {
rocket::build()
.configure(Config {
address: std::env::var("BIND_ADDR")
@@ -84,47 +64,47 @@ pub fn build_rocket(state: Arc<State>) -> Rocket<Build> {
"/",
routes![
// Frontend
- r_account_login_post,
- r_account_login,
- r_account_logout_post,
- r_account_logout,
- r_account_register_post,
- r_account_register,
- r_account_settings_post,
- r_account_settings,
- r_admin_dashboard,
- r_admin_import,
- r_admin_import_post,
- r_admin_import_stream,
- r_admin_invite,
- r_admin_log_stream,
- r_admin_log,
- r_admin_remove_invite,
- r_admin_remove_user,
- r_admin_update_search,
- r_admin_user_permission,
- r_admin_user,
- r_admin_users,
- r_items,
+ // r_account_login_post,
+ // r_account_login,
+ // r_account_logout_post,
+ // r_account_logout,
+ // r_account_register_post,
+ // r_account_register,
+ // r_account_settings_post,
+ // r_account_settings,
+ // r_admin_dashboard,
+ // r_admin_import,
+ // r_admin_import_post,
+ // r_admin_import_stream,
+ // r_admin_invite,
+ // r_admin_log_stream,
+ // r_admin_log,
+ // r_admin_remove_invite,
+ // r_admin_remove_user,
+ // r_admin_update_search,
+ // r_admin_user_permission,
+ // r_admin_user,
+ // r_admin_users,
+ // r_items,
r_image,
r_assets_font,
r_assets_js_map,
r_assets_js,
r_assets_style,
r_favicon,
- r_home,
- r_index,
- r_item_poster,
+ // r_home,
+ // r_index,
+ // r_item_poster,
r_node,
- r_node_thumbnail,
- r_node_userdata_progress,
- r_node_userdata_rating,
- r_node_userdata_watched,
- r_node_userdata,
- r_player,
+ // r_node_thumbnail,
+ // r_node_userdata_progress,
+ // r_node_userdata_rating,
+ // r_node_userdata_watched,
+ // r_node_userdata,
+ // r_player,
r_playersync,
- r_search,
- r_stats,
+ // r_search,
+ // r_stats,
r_stream,
// API
r_nodes_modified_since,
diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs
index 429b70a..ec8bd49 100644
--- a/server/src/ui/account/mod.rs
+++ b/server/src/ui/account/mod.rs
@@ -3,12 +3,17 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-pub mod settings;
+// pub mod settings;
use super::error::MyError;
-use crate::ui::{error::MyResult, home::rocket_uri_macro_r_home};
+use crate::{
+ request_info::RequestInfo,
+ ui::{error::MyResult, home::rocket_uri_macro_r_home},
+};
use anyhow::anyhow;
+use jellycommon::{VIEW_ACCOUNT_LOGIN, jellyobject::Object};
use jellyimport::is_importing;
+use jellyui::render_view;
use rocket::{
FromForm,
form::{Contextual, Form},
@@ -30,16 +35,9 @@ pub struct RegisterForm {
}
#[get("/account/register")]
-pub async fn r_account_register(lang: AcceptLanguage) -> RawHtml<String> {
- let AcceptLanguage(lang) = lang;
- RawHtml(render_page(
- &AccountRegister { lang: &lang },
- RenderInfo {
- importing: false,
- session: None,
- lang,
- },
- ))
+pub async fn r_account_register(ri: RequestInfo<'_>) -> RawHtml<String> {
+ let ob = Object::EMPTY.insert(VIEW_ACCOUNT_LOGIN, ());
+ RawHtml(render_view(ri.render_info(), ob.as_object()))
}
#[derive(FromForm, Serialize, Deserialize)]
diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs
index 8f3fb4a..c956672 100644
--- a/server/src/ui/assets.rs
+++ b/server/src/ui/assets.rs
@@ -4,58 +4,57 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use super::error::MyResult;
-use anyhow::{Context, anyhow};
-use rocket::{get, http::ContentType, response::Redirect};
-use std::str::FromStr;
+use crate::{request_info::RequestInfo, responders::cache::CacheControlImage};
+use anyhow::Context;
+use rocket::{get, http::ContentType};
+use std::path::PathBuf;
pub const AVIF_QUALITY: u32 = 70;
pub const AVIF_SPEED: u8 = 5;
-#[get("/image/<key..>?<size>")]
+#[get("/image/<path..>?<size>")]
pub async fn r_image(
- _session: A<Session>,
- key: A<Asset>,
+ ri: RequestInfo<'_>,
+ path: PathBuf,
size: Option<usize>,
) -> MyResult<(ContentType, CacheControlImage)> {
let size = size.unwrap_or(2048);
-
- if !key.0.0.ends_with(".image") {
- Err(anyhow!("request to non-image"))?
- }
+ let path = path.to_string_lossy().to_string();
// fit the resolution into a finite set so the maximum cache is finite too.
let width = 2usize.pow(size.clamp(128, 2048).ilog2());
- let encoded = jellytranscoder::image::transcode(&key.0.0, AVIF_QUALITY, AVIF_SPEED, width)
- .context("transcoding asset")?;
+ let encoded =
+ jellytranscoder::image::transcode(&ri.state.cache, &path, AVIF_QUALITY, AVIF_SPEED, width)
+ .context("transcoding asset")?;
Ok((ContentType::AVIF, CacheControlImage(encoded)))
}
-#[get("/n/<id>/image/<slot>?<size>")]
-pub async fn r_item_poster(
- session: A<Session>,
- id: A<NodeID>,
- slot: &str,
- size: Option<usize>,
-) -> MyResult<Redirect> {
- let slot = PictureSlot::from_str(slot).map_err(|_| anyhow!("slot invalid"))?;
- let node = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?;
- let picture = node
- .node
- .pictures
- .get(&slot)
- .cloned()
- .ok_or(anyhow!("no pic todo"))?;
- Ok(Redirect::permanent(rocket::uri!(r_image(picture, size))))
-}
+// #[get("/n/<id>/image/<slot>?<size>")]
+// pub async fn r_item_poster(
+// session: A<Session>,
+// id: A<NodeID>,
+// slot: &str,
+// size: Option<usize>,
+// ) -> MyResult<Redirect> {
+// let slot = PictureSlot::from_str(slot).map_err(|_| anyhow!("slot invalid"))?;
+// let node = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?;
+// let picture = node
+// .node
+// .pictures
+// .get(&slot)
+// .cloned()
+// .ok_or(anyhow!("no pic todo"))?;
+// Ok(Redirect::permanent(rocket::uri!(r_image(picture, size))))
+// }
-#[get("/n/<id>/thumbnail?<t>&<size>")]
-pub async fn r_node_thumbnail(
- session: A<Session>,
- id: A<NodeID>,
- t: f64,
- size: Option<usize>,
-) -> MyResult<Redirect> {
- let picture = get_node_thumbnail(&session.0, id.0, t).await?;
- Ok(Redirect::permanent(rocket::uri!(r_image(picture, size))))
-}
+// #[get("/n/<id>/thumbnail?<t>&<size>")]
+// pub async fn r_node_thumbnail(
+// session: A<Session>,
+// id: A<NodeID>,
+// t: f64,
+// size: Option<usize>,
+// ) -> MyResult<Redirect> {
+// let picture = get_node_thumbnail(&session.0, id.0, t).await?;
+// Ok(Redirect::permanent(rocket::uri!(r_image(picture, size))))
+// }
diff --git a/server/src/ui/error.rs b/server/src/ui/error.rs
index 0f279fc..578d841 100644
--- a/server/src/ui/error.rs
+++ b/server/src/ui/error.rs
@@ -3,38 +3,20 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use log::info;
use rocket::{
- catch,
- http::{ContentType, Status},
- response::{self, content::RawHtml, Responder},
- Request,
+ Request, catch,
+ http::Status,
+ response::{self, Responder, content::RawHtml},
};
-use serde_json::{json, Value};
-use std::{fmt::Display, fs::File, io::Read, sync::LazyLock};
-
-static ERROR_IMAGE: LazyLock<Vec<u8>> = LazyLock::new(|| {
- info!("loading error image");
- let mut f = File::open(CONF.asset_path.join("error.avif"))
- .expect("please create error.avif in the asset dir");
- let mut o = Vec::new();
- f.read_to_end(&mut o).unwrap();
- o
-});
+use serde_json::{Value, json};
+use std::fmt::Display;
#[catch(default)]
pub fn r_catch(status: Status, _request: &Request) -> RawHtml<String> {
catch_with_message(format!("{status}"))
}
fn catch_with_message(message: String) -> RawHtml<String> {
- RawHtml(render_page(
- &ErrorPage { status: message },
- RenderInfo {
- importing: false,
- session: None,
- lang: Language::English,
- },
- ))
+ RawHtml(message) // TODO
}
#[catch(default)]
@@ -52,9 +34,9 @@ impl<'r> Responder<'r, 'static> for MyError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
match req.accept().map(|a| a.preferred()) {
Some(x) if x.is_json() => json!({ "error": format!("{}", self.0) }).respond_to(req),
- Some(x) if x.is_avif() || x.is_png() || x.is_jpeg() => {
- (ContentType::AVIF, ERROR_IMAGE.as_slice()).respond_to(req)
- }
+ // Some(x) if x.is_avif() || x.is_png() || x.is_jpeg() => {
+ // (ContentType::AVIF, ERROR_IMAGE.as_slice()).respond_to(req)
+ // }
_ => catch_with_message(format!("{:#}", self.0)).respond_to(req),
}
}
diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs
index 92b93fe..55fad6a 100644
--- a/server/src/ui/mod.rs
+++ b/server/src/ui/mod.rs
@@ -4,58 +4,51 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use error::MyResult;
-use home::rocket_uri_macro_r_home;
-use rocket::{
- Either,
- futures::FutureExt,
- get,
- response::{Redirect, content::RawHtml},
-};
-use std::{future::Future, pin::Pin};
-use tokio::{
- fs::{File, read_to_string},
- io::AsyncRead,
-};
+use rocket::{futures::FutureExt, get};
+use std::{future::Future, pin::Pin, sync::Arc};
+use tokio::{fs::File, io::AsyncRead};
+
+use crate::State;
pub mod account;
-pub mod admin;
+// pub mod admin;
pub mod assets;
pub mod error;
-pub mod home;
-pub mod items;
+// pub mod home;
+// pub mod items;
pub mod node;
-pub mod player;
-pub mod search;
-pub mod stats;
+// pub mod player;
+// pub mod search;
+// pub mod stats;
pub mod style;
-#[get("/")]
-pub async fn r_index(
- lang: AcceptLanguage,
- sess: Option<A<Session>>,
-) -> MyResult<Either<Redirect, RawHtml<String>>> {
- let AcceptLanguage(lang) = lang;
- if sess.is_some() {
- Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home()))))
- } else {
- let front = read_to_string(CONF.asset_path.join("front.htm")).await?;
- Ok(Either::Right(RawHtml(render_page(
- &CustomPage {
- title: "Jellything".to_string(),
- body: front,
- },
- RenderInfo {
- importing: false,
- session: None,
- lang,
- },
- ))))
- }
-}
+// #[get("/")]
+// pub async fn r_index(
+// lang: AcceptLanguage,
+// sess: Option<A<Session>>,
+// ) -> MyResult<Either<Redirect, RawHtml<String>>> {
+// let AcceptLanguage(lang) = lang;
+// if sess.is_some() {
+// Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home()))))
+// } else {
+// let front = read_to_string(CONF.asset_path.join("front.htm")).await?;
+// Ok(Either::Right(RawHtml(render_page(
+// &CustomPage {
+// title: "Jellything".to_string(),
+// body: front,
+// },
+// RenderInfo {
+// importing: false,
+// session: None,
+// lang,
+// },
+// ))))
+// }
+// }
#[get("/favicon.ico")]
-pub async fn r_favicon() -> MyResult<File> {
- Ok(File::open(CONF.asset_path.join("favicon.ico")).await?)
+pub async fn r_favicon(s: &rocket::State<Arc<State>>) -> MyResult<File> {
+ Ok(File::open(s.config.asset_path.join("favicon.ico")).await?)
}
pub struct Defer(Pin<Box<dyn Future<Output = String> + Send>>);
diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs
index 85beac6..cf5c793 100644
--- a/server/src/ui/node.rs
+++ b/server/src/ui/node.rs
@@ -5,42 +5,12 @@
*/
use super::error::MyResult;
use crate::request_info::RequestInfo;
-use rocket::{Either, get, response::content::RawHtml, serde::json::Json};
+use jellycommon::jellyobject::Object;
+use jellyui::render_view;
+use rocket::{get, response::content::RawHtml};
-#[get("/n/<id>?<parents>&<children>&<filter..>")]
-pub async fn r_node(
- ri: RequestInfo<'_>,
- id: A<NodeID>,
- filter: Option<ANodeFilterSort>,
- parents: bool,
- children: bool,
-) -> MyResult<Either<RawHtml<String>, Json<ApiNodeResponse>>> {
- let filter: Option<NodeFilterSort> = filter.map(Into::into);
- let filter = filter.unwrap_or_default();
-
- let r = get_node(
- &ri.session,
- id.0,
- !ri.accept.is_json() || children,
- !ri.accept.is_json() || parents,
- filter.clone(),
- )?;
-
- Ok(if ri.accept.is_json() {
- Either::Right(Json(r))
- } else {
- Either::Left(RawHtml(render_page(
- &NodePage {
- node: &r.node,
- udata: &r.userdata,
- children: &r.children,
- parents: &r.parents,
- similar: &[],
- filter: &filter,
- lang: &ri.lang,
- player: false,
- },
- ri.render_info(),
- )))
- })
+#[get("/n/<slug>")]
+pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<RawHtml<String>> {
+ ri.require_user()?;
+ Ok(RawHtml(render_view(ri.render_info(), Object::EMPTY)))
}
diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs
index fce79e3..8ec2697 100644
--- a/server/src/ui/search.rs
+++ b/server/src/ui/search.rs
@@ -10,27 +10,28 @@ use rocket::{Either, get, response::content::RawHtml, serde::json::Json};
#[get("/search?<query>&<page>")]
pub async fn r_search(
- ri: RequestInfo,
+ ri: RequestInfo<'_>,
query: Option<&str>,
page: Option<usize>,
-) -> MyResult<Either<RawHtml<String>, Json<ApiSearchResponse>>> {
- let r = query
- .map(|query| search(&ri.session, query, page))
- .transpose()?;
+) -> MyResult<RawHtml<String>> {
+ // let r = query
+ // .map(|query| search(&ri.session, query, page))
+ // .transpose()?;
- Ok(if ri.accept.is_json() {
- let Some(r) = r else {
- Err(anyhow!("no query"))?
- };
- Either::Right(Json(r))
- } else {
- Either::Left(RawHtml(render_page(
- &SearchPage {
- lang: &ri.lang,
- query: &query.map(|s| s.to_string()),
- r,
- },
- ri.render_info(),
- )))
- })
+ // Ok(if ri.accept.is_json() {
+ // let Some(r) = r else {
+ // Err(anyhow!("no query"))?
+ // };
+ // Either::Right(Json(r))
+ // } else {
+ // Either::Left(RawHtml(render_page(
+ // &SearchPage {
+ // lang: &ri.lang,
+ // query: &query.map(|s| s.to_string()),
+ // r,
+ // },
+ // ri.render_info(),
+ // )))
+ // })
+ todo!()
}
diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs
index fc4ae64..387ca63 100644
--- a/server/src/ui/stats.rs
+++ b/server/src/ui/stats.rs
@@ -3,22 +3,10 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use super::error::MyError;
-use crate::request_info::RequestInfo;
-use rocket::{Either, get, response::content::RawHtml, serde::json::Json};
+use crate::{request_info::RequestInfo, ui::error::MyResult};
+use rocket::{get, response::content::RawHtml};
#[get("/stats")]
-pub fn r_stats(
- ri: RequestInfo,
-) -> Result<Either<RawHtml<String>, Json<ApiStatsResponse>>, MyError> {
- let r = stats(&ri.session)?;
-
- Ok(if ri.accept.is_json() {
- Either::Right(Json(r))
- } else {
- Either::Left(RawHtml(render_page(
- &StatsPage { lang: &ri.lang, r },
- ri.render_info(),
- )))
- })
+pub fn r_stats(ri: RequestInfo) -> MyResult<RawHtml<String>> {
+ todo!()
}
diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index 9bf5082..55ff42f 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
-markup = { git = "https://github.com/metamuffin/markup.rs", rev = "2ee9aee" }
+markup = "0.16.0"
jellycommon = { path = "../common" }
humansize = "2.1.3"
serde = { version = "1.0.228", features = ["derive", "rc"] }
diff --git a/ui/src/components/login.rs b/ui/src/components/login.rs
new file mode 100644
index 0000000..c54a541
--- /dev/null
+++ b/ui/src/components/login.rs
@@ -0,0 +1,43 @@
+/*
+ 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 jellyui_locale::tr;
+
+use crate::RenderInfo;
+
+markup::define! {
+ AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, session: &'a str) {
+ form.account[method="POST", action=""] {
+ h1 { @tr(ri.lang, "account.set_password") }
+ input[type="text", name="session", hidden, value=session]; 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")];
+
+
+ }
+ }
+ AccountLogin<'a>(ri: &'a RenderInfo<'a>) {
+ form.account[method="POST", action=""] {
+ h1 { @tr(ri.lang, "account.login") }
+
+ 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 ri.user.is_some() { "account.login.submit.switch" } else { "account.login.submit" })];
+ }
+ }
+ 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/components/mod.rs b/ui/src/components/mod.rs
index 07b050b..792894e 100644
--- a/ui/src/components/mod.rs
+++ b/ui/src/components/mod.rs
@@ -4,6 +4,7 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
+pub mod login;
pub mod message;
pub mod node_page;
pub mod props;
@@ -11,7 +12,11 @@ pub mod stats;
use crate::{
RenderInfo,
- components::{message::Message, node_page::NodePage},
+ components::{
+ login::{AccountLogin, AccountLogout, AccountSetPassword},
+ message::Message,
+ node_page::NodePage,
+ },
};
use jellycommon::{jellyobject::Object, *};
use markup::define;
@@ -24,5 +29,14 @@ define! {
@if let Some(nku) = view.get(VIEW_NODE_PAGE) {
@NodePage { ri, nku }
}
+ @if let Some(()) = view.get(VIEW_ACCOUNT_LOGIN) {
+ @AccountLogin { ri }
+ }
+ @if let Some(()) = view.get(VIEW_ACCOUNT_LOGOUT) {
+ @AccountLogout{ ri }
+ }
+ @if let Some(session) = view.get(VIEW_ACCOUNT_SET_PASSWORD) {
+ @AccountSetPassword { ri, session }
+ }
}
}
diff --git a/ui/src/old/account/mod.rs b/ui/src/old/account/mod.rs
deleted file mode 100644
index e7da26f..0000000
--- a/ui/src/old/account/mod.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- 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")];
- }
- }
-}