aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/src/user.rs25
-rw-r--r--server/src/main.rs12
-rw-r--r--server/src/routes/ui/account/mod.rs6
-rw-r--r--server/src/routes/ui/account/settings.rs22
-rw-r--r--server/src/routes/ui/player.rs44
-rw-r--r--web/script/transition.ts18
-rw-r--r--web/style/js-transition.css9
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;