diff options
-rw-r--r-- | common/src/user.rs | 25 | ||||
-rw-r--r-- | server/src/main.rs | 12 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 6 | ||||
-rw-r--r-- | server/src/routes/ui/account/settings.rs | 22 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 44 | ||||
-rw-r--r-- | web/script/transition.ts | 18 | ||||
-rw-r--r-- | web/style/js-transition.css | 9 |
7 files changed, 105 insertions, 31 deletions
diff --git a/common/src/user.rs b/common/src/user.rs index 085f9db..4b39673 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -13,13 +13,16 @@ use std::{ fmt::Display, }; -#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)] pub struct User { pub name: String, pub display_name: String, pub password: Vec<u8>, pub admin: bool, + #[serde(default)] pub theme: Theme, + #[serde(default)] + pub player_preference: PlayerKind, pub permissions: PermissionSet, } @@ -45,15 +48,26 @@ pub struct CreateSessionParams { pub drop_permissions: Option<HashSet<UserPermission>>, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] #[serde(rename_all = "snake_case")] pub enum Theme { + #[default] Dark, Light, Purple, } +#[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] +#[serde(rename_all = "snake_case")] +pub enum PlayerKind { + #[default] + Browser, + Native, + NativeFullscreen, +} + impl Theme { pub const LIST: &'static [(Theme, &'static str)] = &[ (Theme::Dark, "Dark"), @@ -61,6 +75,13 @@ impl Theme { (Theme::Purple, "Purple"), ]; } +impl PlayerKind { + pub const LIST: &'static [(PlayerKind, &'static str)] = &[ + (PlayerKind::Browser, "In-Browser"), + (PlayerKind::Native, "Native"), + (PlayerKind::NativeFullscreen, "Native (Fullscreen)"), + ]; +} #[derive(Debug, Clone, Serialize, Deserialize, Default, Encode, Decode)] pub struct PermissionSet(pub HashMap<UserPermission, bool>); diff --git a/server/src/main.rs b/server/src/main.rs index a94fe32..919ba50 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,9 +10,11 @@ use crate::routes::ui::{account::hash_password, admin::log::enable_logging}; use database::DataAcid; use jellybase::{ - database::{ReadableTable, Ser, T_USER}, federation::Federation, CONF, SECRETS + database::{ReadableTable, Ser, T_USER}, + federation::Federation, + CONF, SECRETS, }; -use jellycommon::user::{PermissionSet, Theme, User}; +use jellycommon::user::User; use log::{error, info, warn}; use routes::build_rocket; use tokio::fs::create_dir_all; @@ -45,12 +47,8 @@ async fn main() { name: username.clone(), password: hash_password(&username, &password), ..admin.unwrap_or_else(|| User { - name: Default::default(), display_name: "Admin".to_string(), - password: Default::default(), - admin: Default::default(), - theme: Theme::Dark, - permissions: PermissionSet::default(), + ..Default::default() }) }), ) diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 5298f7c..144b1e0 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -22,7 +22,7 @@ use jellybase::{ database::{Ser, TableExt, T_INVITE, T_USER}, CONF, }; -use jellycommon::user::{PermissionSet, Theme, User, UserPermission}; +use jellycommon::user::{User, UserPermission}; use rocket::{ form::{Contextual, Form}, get, @@ -148,9 +148,7 @@ pub fn r_account_register_post<'a>( display_name: form.username.clone(), name: form.username.clone(), password: hash_password(&form.username, &form.password), - admin: false, - theme: Theme::Dark, - permissions: PermissionSet::default(), + ..Default::default() }), )? .map(|x| x.value().0); diff --git a/server/src/routes/ui/account/settings.rs b/server/src/routes/ui/account/settings.rs index 48e0e9f..e91bec0 100644 --- a/server/src/routes/ui/account/settings.rs +++ b/server/src/routes/ui/account/settings.rs @@ -18,7 +18,7 @@ use jellybase::{ database::{ReadableTable, Ser, T_USER}, permission::PermissionSetExt, }; -use jellycommon::user::{Theme, UserPermission}; +use jellycommon::user::{PlayerKind, Theme, UserPermission}; use markup::{Render, RenderAttributeValue}; use rocket::{ form::{self, validate::len, Contextual, Form}, @@ -35,6 +35,7 @@ pub struct SettingsForm { #[field(validate = option_len(4..32))] display_name: Option<String>, theme: Option<Theme>, + player_preference: Option<PlayerKind>, } fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<'v, ()> { @@ -80,14 +81,23 @@ fn settings_page(session: Session, flash: Option<MyResult<String>>) -> DynLayout } input[type="submit", value="Apply"]; } + form[method="POST", action=uri!(r_account_settings_post())] { + fieldset { + legend { "Preferred Media Player" } + @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="Apply"]; + } }, ..Default::default() } } -struct A(pub Theme); -impl RenderAttributeValue for A {} -impl Render for A { +struct A<T>(pub T); +impl<T: UriDisplay<Query>> RenderAttributeValue for A<T> {} +impl<T: UriDisplay<Query>> Render for A<T> { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay<Query>)) } @@ -137,6 +147,10 @@ pub fn r_account_settings_post( user.theme = theme; out += "Theme updated\n"; } + if let Some(player_preference) = form.player_preference { + user.player_preference = player_preference; + out += "Player preference changed.\n"; + } users.insert(&*session.user.name, Ser(user))?; drop(users); diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 233547e..be40d4c 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -17,13 +17,17 @@ use crate::{ uri, }; use anyhow::anyhow; -use jellybase::database::{TableExt, T_NODE}; +use jellybase::{ + database::{TableExt, T_NODE}, + CONF, +}; use jellycommon::{ stream::{StreamFormat, StreamSpec}, + user::PlayerKind, Node, SourceTrackKind, TrackID, }; use markup::DynRender; -use rocket::{get, FromForm, State, UriDisplayQuery}; +use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; #[derive(FromForm, Default, Clone, Debug, UriDisplayQuery)] pub struct PlayerConfig { @@ -42,15 +46,43 @@ impl PlayerConfig { } } +fn jellynative_url(action: &str, node: &str) -> String { + format!( + "jellynative://{action}/http://{}{}", + CONF.hostname, + uri!(r_stream( + node, + StreamSpec { + format: StreamFormat::HlsMaster, + ..Default::default() + } + )) + ) +} #[get("/n/<id>/player?<conf..>", rank = 4)] pub fn r_player<'a>( - _sess: Session, + sess: Session, db: &'a State<DataAcid>, id: &'a str, conf: PlayerConfig, -) -> MyResult<DynLayoutPage<'a>> { +) -> MyResult<Either<DynLayoutPage<'a>, Redirect>> { let item = T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?; + match sess.user.player_preference { + PlayerKind::Browser => (), + PlayerKind::Native => { + return Ok(Either::Right(Redirect::temporary(jellynative_url( + "player", id, + )))) + } + PlayerKind::NativeFullscreen => { + return Ok(Either::Right(Redirect::temporary(jellynative_url( + "player-fullscreen", + id, + )))) + } + } + let spec = StreamSpec { track: None .into_iter() @@ -66,7 +98,7 @@ pub fn r_player<'a>( let playing = !spec.track.is_empty(); let conf = player_conf(item.clone(), playing)?; - Ok(LayoutPage { + Ok(Either::Left(LayoutPage { title: item.public.title.to_owned().unwrap_or_default(), class: Some("player"), content: markup::new! { @@ -78,7 +110,7 @@ pub fn r_player<'a>( @conf }, ..Default::default() - }) + })) } pub fn player_conf<'a>(item: Node, playing: bool) -> anyhow::Result<DynRender<'a>> { diff --git a/web/script/transition.ts b/web/script/transition.ts index ce0cd94..a15a6b0 100644 --- a/web/script/transition.ts +++ b/web/script/transition.ts @@ -39,23 +39,29 @@ async function transition_to(href: string, back?: boolean) { disable_transition = false; } -function show_error(mesg: string) { +function show_message(mesg: string, mode: "error" | "success" = "error") { clear_spinner() disable_transition = true - document.body.append(e("span", { class: "jst-error" }, mesg)) + document.body.append(e("span", { class: ["jst-message", mode] }, mesg)) } function prepare_load(href: string, back?: boolean) { - const r_promise = fetch(href) + const r_promise = fetch(href, { headers: { accept: "text/html" }, redirect: "manual" }) return async () => { let rt = "" try { const r = await r_promise - if (!r.ok) return show_error("Error response. Try again.") + if (r.type == "opaqueredirect") { + window.location.href = href + show_message("Native Player Started.", "success") + setTimeout(() => window.location.reload(), 500) + return + } + if (!r.ok) return show_message("Error response. Try again.") rt = await r.text() } catch (e) { - if (e instanceof TypeError) return show_error("Navigation failed. Check your connection.") - return show_error("unknown error when fetching page") + if (e instanceof TypeError) return show_message("Navigation failed. Check your connection.") + return show_message("unknown error when fetching page") } const [head, body] = rt.split("<head>")[1].split("</head>") if (!back) window.history.pushState({}, "", href) diff --git a/web/style/js-transition.css b/web/style/js-transition.css index be9d34a..067fefc 100644 --- a/web/style/js-transition.css +++ b/web/style/js-transition.css @@ -36,15 +36,20 @@ height: 100vh; z-index: 10; } -.jst-error { +.jst-message { position: fixed; top: 50vh; left: 50vw; transform: translate(-50%, -50%); - color: var(--c-error); font-size: large; z-index: 11; } +.jst-message.error { + color: var(--c-error); +} +.jst-message.success { + color: var(--c-success); +} .jst-spinner { position: fixed; top: 50vh; |