diff options
-rw-r--r-- | Cargo.lock | 3 | ||||
-rw-r--r-- | base/src/permission.rs | 2 | ||||
-rw-r--r-- | common/Cargo.toml | 1 | ||||
-rw-r--r-- | common/src/lib.rs | 4 | ||||
-rw-r--r-- | import/src/infojson.rs | 21 | ||||
-rw-r--r-- | import/src/main.rs | 16 | ||||
-rw-r--r-- | server/src/routes/ui/layout.rs | 6 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 41 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 1 | ||||
-rw-r--r-- | server/src/routes/ui/sort.rs | 7 | ||||
-rw-r--r-- | server/src/routes/ui/style.rs | 36 | ||||
-rw-r--r-- | transcoder/Cargo.toml | 3 | ||||
-rw-r--r-- | web/js-transition.css | 34 | ||||
-rw-r--r-- | web/nodecard.css | 27 | ||||
-rw-r--r-- | web/script/backbutton.ts | 8 | ||||
m--------- | web/script/jshelper | 0 | ||||
-rw-r--r-- | web/script/main.ts | 2 | ||||
-rw-r--r-- | web/script/player/mediacaps.ts | 66 | ||||
-rw-r--r-- | web/script/player/mod.ts | 3 | ||||
-rw-r--r-- | web/script/player/player.ts | 3 | ||||
-rw-r--r-- | web/script/player/profiles.ts | 13 | ||||
-rw-r--r-- | web/script/player/track.ts | 2 | ||||
-rw-r--r-- | web/script/transition.ts (renamed from web/script/transition.js) | 59 |
23 files changed, 266 insertions, 92 deletions
@@ -1260,7 +1260,7 @@ dependencies = [ [[package]] name = "image" version = "0.24.7" -source = "git+https://github.com/image-rs/image#f1e34f365133a33a27dac5bc59c93d1cfb34a37e" +source = "git+https://github.com/metamuffin/image-rs#89633ba32f9a9b9e44abc80de88040b91595f992" dependencies = [ "bytemuck", "byteorder", @@ -1415,6 +1415,7 @@ name = "jellycommon" version = "0.1.0" dependencies = [ "bincode 2.0.0-rc.3", + "chrono", "rocket", "serde", ] diff --git a/base/src/permission.rs b/base/src/permission.rs index cc0e32c..275fe92 100644 --- a/base/src/permission.rs +++ b/base/src/permission.rs @@ -56,6 +56,6 @@ fn check_node_permission(perms: &PermissionSet, node: &Node) -> bool { return v; } } - return false; + return true; } } diff --git a/common/Cargo.toml b/common/Cargo.toml index b38b962..437fb0b 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" serde = { version = "1.0.188", features = ["derive"] } bincode = { version = "2.0.0-rc.3", features = ["derive"] } rocket = { workspace = true, optional = true } +chrono = { version = "0.4.31", features = ["serde"] } [features] rocket = ["dep:rocket"] diff --git a/common/src/lib.rs b/common/src/lib.rs index 2bde0b9..8292e87 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -11,6 +11,9 @@ pub mod seek_index; pub mod stream; pub mod user; +pub use chrono; + +use chrono::{DateTime, Utc}; #[cfg(feature = "rocket")] use rocket::{FromFormField, UriDisplayQuery}; use serde::{Deserialize, Serialize}; @@ -42,6 +45,7 @@ pub struct NodePublic { #[serde(default)] pub children: Vec<String>, #[serde(default)] pub tagline: Option<String>, #[serde(default)] pub description: Option<String>, + #[serde(default)] pub release_date: Option<DateTime<Utc>>, #[serde(default)] pub index: Option<usize>, #[serde(default)] pub media: Option<MediaInfo>, #[serde(default)] pub ratings: BTreeMap<Rating, f64>, diff --git a/import/src/infojson.rs b/import/src/infojson.rs index ca02551..dd2151b 100644 --- a/import/src/infojson.rs +++ b/import/src/infojson.rs @@ -4,6 +4,8 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ +use anyhow::Context; +use jellycommon::chrono::{format::Parsed, DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -120,3 +122,22 @@ pub struct YHeatmapSample { pub end_time: f64, pub value: f64, } + +pub fn parse_upload_date(d: &str) -> anyhow::Result<DateTime<Utc>> { + let (year, month, day) = (&d[0..4], &d[4..6], &d[6..8]); + let (year, month, day) = ( + year.parse().context("parsing year")?, + month.parse().context("parsing month")?, + day.parse().context("parsing day")?, + ); + + let mut p = Parsed::new(); + p.year = Some(year); + p.month = Some(month); + p.day = Some(day); + p.hour_div_12 = Some(0); + p.hour_mod_12 = Some(0); + p.minute = Some(0); + p.second = Some(0); + Ok(p.to_datetime_with_timezone(&Utc)?) +} diff --git a/import/src/main.rs b/import/src/main.rs index 57e6b99..c274e54 100644 --- a/import/src/main.rs +++ b/import/src/main.rs @@ -11,7 +11,7 @@ pub mod tmdb; use anyhow::Context; use base64::Engine; use clap::{Parser, Subcommand}; -use infojson::YVideo; +use infojson::{parse_upload_date, YVideo}; use jellycommon::{ config::GlobalConfig, AssetLocation, LocalTrack, MediaInfo, MediaSource, Node, NodeKind, NodePrivate, NodePublic, Rating, @@ -22,6 +22,7 @@ use log::{info, warn}; use rand::random; use std::{ collections::BTreeMap, + fmt::Debug, fs::{remove_file, File}, io::{stdin, BufReader, Write}, path::PathBuf, @@ -354,6 +355,9 @@ fn main() -> anyhow::Result<()> { duration: m.duration, tracks: m.tracks.clone(), }), + release_date: infojson + .as_ref() + .and_then(|j| ok_or_warn(parse_upload_date(&j.upload_date))), ..Default::default() }, }; @@ -399,3 +403,13 @@ fn make_ident(s: &str) -> String { } out } + +fn ok_or_warn<T, E: Debug>(r: Result<T, E>) -> Option<T> { + match r { + Ok(t) => Some(t), + Err(e) => { + warn!("{e:?}"); + None + } + } +} diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs index 1c47247..cdac09c 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/routes/ui/layout.rs @@ -27,7 +27,7 @@ use rocket::{ use std::io::Cursor; markup::define! { - Layout<'a, Main: Render>(title: String, main: Main, class: Option<&'a str>, session: Option<Session>, show_back: bool) { + Layout<'a, Main: Render>(title: String, main: Main, class: Option<&'a str>, session: Option<Session>) { @markup::doctype() html { head { @@ -37,7 +37,6 @@ markup::define! { } body[class=class.unwrap_or("")] { nav { - @if *show_back { a[href="javascript:history.back()"] { "<- Back" } } " " h1 { a[href="/"] { @CONF.brand } } " " @if let Some(_) = session { a[href=uri!(r_library_node("library"))] { "My Library" } " " @@ -80,7 +79,6 @@ pub type DynLayoutPage<'a> = LayoutPage<markup::DynRender<'a>>; pub struct LayoutPage<T> { pub title: String, pub class: Option<&'static str>, - pub show_back: bool, pub content: T, } @@ -90,7 +88,6 @@ impl Default for LayoutPage<DynRender<'_>> { class: None, content: markup::new!(), title: String::new(), - show_back: false, } } } @@ -103,7 +100,6 @@ impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage<Main> { let session = block_on(req.guard::<Option<Session>>()).unwrap(); let mut out = String::new(); Layout { - show_back: self.show_back, main: self.content, title: self.title, class: self.class.as_deref(), diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index e4d53e6..bb97146 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -72,7 +72,6 @@ pub async fn r_library_node_filter<'a>( Ok(Either::Left(LayoutPage { title: node.title.to_string(), - show_back: true, //- !matches!(node.kind, NodeKind::Collection), content: markup::new! { @NodePage { node: &node, id: &id, children: &children, filter: &filter } }, @@ -82,24 +81,37 @@ pub async fn r_library_node_filter<'a>( markup::define! { NodeCard<'a>(id: &'a str, node: &'a NodePublic) { - @let cls = format!("node card {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"}); + @let cls = format!("node card poster {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"}); div[class=cls] { .poster { - .inner { - a[href=uri!(r_library_node(id))] { - img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; + a[href=uri!(r_library_node(id))] { + img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; + } + @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { + .cardhover.open { + a[href=&uri!(r_library_node(id))] { "Open" } + @Props { node } } - div.details { - h3 { @node.title } + } else { + .cardhover.item { + a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "▶" } @Props { node } - p.description { @node.description } - @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { - a[href=&uri!(r_library_node(id))] { "Open" } - } else { - a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" } - } } } + // .inner { + // a[href=uri!(r_library_node(id))] { + // img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; + // } + // div.details { + // h3 { @node.title } + // p.descriptioüwn { @node.description } + // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { + // a[href=&uri!(r_library_node(id))] { "Open" } + // } else { + // a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" } + // } + // } + // } } div.title { a[href=uri!(r_library_node(id))] { @@ -167,6 +179,9 @@ markup::define! { Rating::Imdb => { "IMDb Rating: " @value } } } } + @if let Some(d) = &node.release_date { + p { "Released " @d.to_rfc2822() } + } } } } diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 4a636e6..d3a3342 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -70,7 +70,6 @@ pub fn r_player<'a>( } @conf }, - show_back: true, ..Default::default() }) } diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs index da5061d..e1af9d5 100644 --- a/server/src/routes/ui/sort.rs +++ b/server/src/routes/ui/sort.rs @@ -53,11 +53,16 @@ pub fn filter_and_sort_nodes(f: &NodeFilterSort, nodes: &mut Vec<(String, NodePu if let Some(kind) = &f.filter_kind { o &= kind.contains(&node.kind) } + if let Some(SortProperty::ReleaseDate) = &f.sort_by { + o &= node.release_date.is_some() + } o }); if let Some(sort_prop) = &f.sort_by { match sort_prop { - SortProperty::ReleaseDate => nodes.sort_by_key(|(_, _n)| 0), // TODO + SortProperty::ReleaseDate => { + nodes.sort_by_key(|(_, n)| n.release_date.expect("asserted above")) + } SortProperty::Title => nodes.sort_by(|(_, a), (_, b)| a.title.cmp(&b.title)), SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(_, n)| { SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.)) diff --git a/server/src/routes/ui/style.rs b/server/src/routes/ui/style.rs index 1f92690..bdaddd2 100644 --- a/server/src/routes/ui/style.rs +++ b/server/src/routes/ui/style.rs @@ -4,7 +4,11 @@ Copyright (C) 2023 metamuffin <metamuffin.org> Copyright (C) 2023 tpart */ -use rocket::{get, http::ContentType}; +use rocket::{ + get, + http::{ContentType, Header}, + response::Responder, +}; macro_rules! concat_files { ([$base: expr], $($files:literal),*) => {{ @@ -37,10 +41,22 @@ fn css_bundle() -> String { "nodepage.css", "nodecard.css", "js-player.css", + "js-transition.css", "forms.css" ) } +pub struct CachedAsset<T>(pub T); +impl<'r, 'o: 'r, T: Responder<'r, 'o>> Responder<'r, 'o> for CachedAsset<T> { + fn respond_to(self, request: &'r rocket::Request<'_>) -> rocket::response::Result<'o> { + let mut res = self.0.respond_to(request)?; + if cfg!(not(debug_assertions)) { + res.set_header(Header::new("cache-control", "max-age=86400")); + } + Ok(res) + } +} + fn js_bundle() -> String { concat_files!([env!("OUT_DIR")], "bundle.js") } @@ -49,23 +65,23 @@ fn js_bundle_map() -> String { } #[get("/assets/style.css")] -pub fn r_assets_style() -> (ContentType, String) { - (ContentType::CSS, css_bundle()) +pub fn r_assets_style() -> CachedAsset<(ContentType, String)> { + CachedAsset((ContentType::CSS, css_bundle())) } #[get("/assets/cantarell.woff2")] -pub fn r_assets_font() -> (ContentType, &'static [u8]) { - ( +pub fn r_assets_font() -> CachedAsset<(ContentType, &'static [u8])> { + CachedAsset(( ContentType::WOFF2, include_bytes!("../../../../web/cantarell.woff2"), - ) + )) } #[get("/assets/bundle.js")] -pub fn r_assets_js() -> (ContentType, String) { - (ContentType::JavaScript, js_bundle()) +pub fn r_assets_js() -> CachedAsset<(ContentType, String)> { + CachedAsset((ContentType::JavaScript, js_bundle())) } #[get("/assets/bundle.js.map")] -pub fn r_assets_js_map() -> (ContentType, String) { - (ContentType::JSON, js_bundle_map()) +pub fn r_assets_js_map() -> CachedAsset<(ContentType, String)> { + CachedAsset((ContentType::JSON, js_bundle_map())) } diff --git a/transcoder/Cargo.toml b/transcoder/Cargo.toml index 16bd9d9..c4fa7e8 100644 --- a/transcoder/Cargo.toml +++ b/transcoder/Cargo.toml @@ -8,7 +8,8 @@ jellycommon = { path = "../common" } jellybase = { path = "../base" } jellyremuxer = { path = "../remuxer" } log = { workspace = true } -image = { git = "https://github.com/image-rs/image", features = [ +# TODO: change this back to crates.io when pr is merged +image = { git = "https://github.com/metamuffin/image-rs", features = [ "avif-decoder", ] } anyhow = "1.0.75" diff --git a/web/js-transition.css b/web/js-transition.css new file mode 100644 index 0000000..867e30c --- /dev/null +++ b/web/js-transition.css @@ -0,0 +1,34 @@ +@keyframes jst-fadein { + from { + background-color: transparent; + } + to { + background-color: black; + } +} +@keyframes jst-fadeout { + from { + background-color: black; + } + to { + background-color: transparent; + } +} + +.jst-fade { + position: fixed; + left: 0px; + top: 0px; + width: 100vw; + height: 100vh; + z-index: 100; +} +.jst-error { + position: fixed; + top: 50vh; + left: 50vw; + transform: translate(-50%, -50%); + color: rgb(247, 69, 69); + font-size: large; + z-index: 101; +} diff --git a/web/nodecard.css b/web/nodecard.css index 078f53e..d32d5f4 100644 --- a/web/nodecard.css +++ b/web/nodecard.css @@ -40,10 +40,6 @@ .node.card .poster, .node.card .poster img { height: var(--card-size); - transition: height 0.2s; -} -.node.card:hover .poster img { - height: calc(var(--card-size) + 10em); } .node.card.aspect-port { width: calc(var(--card-size) / var(--port-poster-aspect)); @@ -68,24 +64,6 @@ text-align: center; } -.node.card .inner { - transition: margin 0.2s, padding 0.2s; -} -.node.card:hover .inner { - background-color: rgba(0, 0, 0, 0.76); - backdrop-filter: blur(10px); - border-radius: 1em; - padding: 1.5em; - margin: -5em; -} -.node.card:not(:hover) .details { - display: none; -} -.node.card:hover .title { - display: none; -} - -/* .node.card .title { margin-top: 0.1em; text-align: center; @@ -96,9 +74,8 @@ } .node.card .poster a { grid-area: 1 / 1; -} */ +} -/* .node.card.poster .poster .cardhover.open { transition: opacity 0.3s, backdrop-filter 0.3s; opacity: 0; @@ -168,4 +145,4 @@ position: absolute; bottom: 0px; left: 0px; -} */ +} diff --git a/web/script/backbutton.ts b/web/script/backbutton.ts new file mode 100644 index 0000000..c1225c0 --- /dev/null +++ b/web/script/backbutton.ts @@ -0,0 +1,8 @@ +import { e } from "./jshelper/mod.ts"; + +globalThis.addEventListener("DOMContentLoaded", () => { + document.getElementsByTagName("nav").item(0)?.prepend( + e("a", "<- Back", { onclick() { history.back() } }) + ) +}) + diff --git a/web/script/jshelper b/web/script/jshelper -Subproject bd8b233316820cd35178085ef132f82279be050 +Subproject 2d36b0762459b8edfc1529827d0c15447edd266 diff --git a/web/script/main.ts b/web/script/main.ts index b59f7af..dd168d5 100644 --- a/web/script/main.ts +++ b/web/script/main.ts @@ -4,3 +4,5 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ import "./player/mod.ts" +import "./transition.ts" +import "./backbutton.ts" diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts new file mode 100644 index 0000000..ff143c0 --- /dev/null +++ b/web/script/player/mediacaps.ts @@ -0,0 +1,66 @@ +import { SourceTrack, SourceTrackKind } from "./jhls.d.ts"; + +const cache = new Map<string, boolean>() + +// TODO this testing method makes the assumption, that if the codec is supported on its own, it can be +// TODO arbitrarly combined with others that are supported. in reality this is true but the spec does not gurantee it. + +export async function test_media_capability(track: SourceTrack): Promise<boolean> { + const cache_key = `${get_track_kind(track.kind)};${track.codec}` + const cached = cache.get(cache_key); + if (cached !== undefined) return cached + const r = await test_media_capability_inner(track) + console.log(`${r ? "positive" : "negative"} media capability test finished for codec=${track.codec}`); + cache.set(cache_key, r) + return r +} +async function test_media_capability_inner(track: SourceTrack) { + if (track.kind.subtitles) { + return track.codec == "V_TEXT/WEBVTT" // TODO: actually implement it + } + let res; + const codec = MASTROSKA_CODEC_MAP[track.codec] + if (!codec) return console.warn(`unknown codec: ${track.codec}`), false + if (track.kind.audio) { + res = await navigator.mediaCapabilities.decodingInfo({ + type: "media-source", + audio: { + contentType: `audio/webm^; codecs=${codec}`, + samplerate: track.kind.audio.sample_rate, + channels: "" + track.kind.audio.channels, + bitrate: 128 * 1000, + } + }) + } + if (track.kind.video) { + res = await navigator.mediaCapabilities.decodingInfo({ + type: "media-source", + video: { + contentType: `video/webm; codecs=${codec}`, + framerate: track.kind.video.fps || 30, + width: track.kind.video.width, + height: track.kind.video.height, + bitrate: 5 * 1000 * 1000 // TODO we dont know this but we should in the future + } + }) + } + return res?.supported ?? false +} + +const MASTROSKA_CODEC_MAP: { [key: string]: string } = { + "V_VP9": "vp9", + "V_VP8": "vp8", + "V_AV1": "av1", + "V_MPEG4/ISO/AVC": "h264", + "V_MPEGH/ISO/HEVC": "h265", + "A_OPUS": "opus", + "A_VORBIS": "vorbis", + "S_TEXT/WEBVTT": "webvtt", +} + +export function get_track_kind(track: SourceTrackKind): "audio" | "video" | "subtitles" { + if (track.audio) return "audio" + if (track.video) return "video" + if (track.subtitles) return "subtitles" + throw new Error("invalid track"); +} diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index 8473280..ce3c113 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -9,12 +9,13 @@ import { Logger } from "../jshelper/src/log.ts"; import { EncodingProfile } from "./jhls.d.ts"; import { Player } from "./player.ts"; -document.addEventListener("DOMContentLoaded", () => { +globalThis.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { if (!globalThis.MediaSource) return alert("Media Source Extension API required") const node_id = globalThis.location.pathname.split("/")[2]; const main = document.getElementById("main")!; document.getElementsByTagName("footer")[0].remove() + globalThis.dispatchEvent(new Event("navigationrequiresreload")) initialize_player(main, node_id) } }) diff --git a/web/script/player/player.ts b/web/script/player/player.ts index 4c1d9fc..acf2a19 100644 --- a/web/script/player/player.ts +++ b/web/script/player/player.ts @@ -21,7 +21,7 @@ export class Player { private cancel_buffering_pers: undefined | (() => void) set_pers(s?: string) { - if (this.cancel_buffering_pers) this.cancel_buffering_pers() + if (this.cancel_buffering_pers) this.cancel_buffering_pers(), this.cancel_buffering_pers = undefined if (s) this.cancel_buffering_pers = this.logger?.log_persistent(s) } @@ -38,6 +38,7 @@ export class Player { } this.video.onwaiting = () => { console.log("waiting"); + if (this.video.currentTime > this.duration.value - 0.2) return this.set_pers("Playback finished") this.set_pers("Buffering...") this.canplay.value = false; } diff --git a/web/script/player/profiles.ts b/web/script/player/profiles.ts index a768d47..caa46bd 100644 --- a/web/script/player/profiles.ts +++ b/web/script/player/profiles.ts @@ -1,5 +1,6 @@ import { OVar } from "../jshelper/mod.ts"; import { EncodingProfile, JhlsMetadata } from "./jhls.d.ts"; +import { test_media_capability } from "./mediacaps.ts"; import { Player } from "./player.ts"; const PROFILE_UP_FAC = 0.6 @@ -33,19 +34,25 @@ export class ProfileSelector { if (i.subtitles) return this.profiles_subtitles return [] } - select_optimal_profile(track: number, profile: OVar<EncodingProfileExt | undefined>) { + async remux_supported(track: number): Promise<boolean> { + return await test_media_capability(this.metadata.tracks[track].info) + } + async select_optimal_profile(track: number, profile: OVar<EncodingProfileExt | undefined>) { const profs = this.profile_list_for_track(track) - const co = profile.value?.order ?? -1 + const sup_remux = await this.remux_supported(track); + const min_prof = sup_remux ? -1 : 0 + const co = profile.value?.order ?? min_prof const current_bitrate = profile_byterate(profs[co], 5000 * 1000) const next_bitrate = profile_byterate(profs[co - 1], 5000 * 1000) // console.log({ current_bitrate, next_bitrate, co, bandwidth: this.bandwidth.value * 8 }); + if (!sup_remux && !profile.value) profile.value = profs[co]; if (current_bitrate > this.bandwidth.value * PROFILE_DOWN_FAC && co + 1 < profs.length) { console.log("profile up"); profile.value = profs[co + 1] this.log_change(track, profile.value) } - if (next_bitrate < this.bandwidth.value * PROFILE_UP_FAC && co >= 0) { + if (next_bitrate < this.bandwidth.value * PROFILE_UP_FAC && co > min_prof) { console.log("profile down"); profile.value = profs[co - 1] this.log_change(track, profile.value) diff --git a/web/script/player/track.ts b/web/script/player/track.ts index e2d9d85..4173b12 100644 --- a/web/script/player/track.ts +++ b/web/script/player/track.ts @@ -81,7 +81,7 @@ export class PlayerTrack { async load(index: number) { this.loading.add(index) - this.player.profile_selector.select_optimal_profile(this.track_index, this.profile) + await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile) const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`; const buf = await this.player.downloader.download(url) await new Promise<void>(cb => { diff --git a/web/script/transition.js b/web/script/transition.ts index 7d39176..e0ee6f5 100644 --- a/web/script/transition.js +++ b/web/script/transition.ts @@ -5,14 +5,20 @@ */ /// <reference lib="dom" /> -const duration = 0.2 -globalThis.addEventListener("load", () => { +import { e } from "./jshelper/src/element.ts"; + +const duration = 200 +globalThis.addEventListener("DOMContentLoaded", () => { patch_page() }) -globalThis.addEventListener("popstate", (_e) => { - transition_to(window.location.href, true) - // transition_to(_e.state.href, true) +globalThis.addEventListener("popstate", async (_e) => { + await transition_to(window.location.href, true) +}) + +let disable_transition = false +globalThis.addEventListener("navigationrequiresreload", () => { + disable_transition = true }) function patch_page() { @@ -24,52 +30,51 @@ function patch_page() { }) } -async function transition_to(href, back) { +async function transition_to(href: string, back?: boolean) { + if (disable_transition) return window.location.href = href const trigger_load = prepare_load(href, back) await fade(false) trigger_load() + disable_transition = false; +} + +function show_error(mesg: string) { + document.body.append(e("span", { class: "jst-error" }, mesg)) } -function prepare_load(href, back) { +function prepare_load(href: string, back?: boolean) { const r_promise = fetch(href) return async () => { let rt = "" try { const r = await r_promise - if (!r.ok) return document.body.innerHTML = "<h1>error</h1>" + if (!r.ok) return show_error("Error response. Try again.") rt = await r.text() } catch (e) { - console.error(e) - return + if (e instanceof TypeError) return show_error("Navigation failed. Check your connection.") + return show_error("unknown error when fetching page") } const [head, body] = rt.split("<head>")[1].split("</head>") + if (!back) window.history.pushState({}, "", href) document.head.innerHTML = head document.body.outerHTML = body + globalThis.dispatchEvent(new Event("DOMContentLoaded")) fade(true) - // if (!back) window.history.pushState({href}, "", href) - if (!back) window.history.pushState({}, "", href) - patch_page() } } -function fade(dir) { +function fade(dir: boolean) { const overlay = document.createElement("div") - overlay.style.position = "absolute" - overlay.style.left = "0px" - overlay.style.top = "0px" - overlay.style.width = "100vw" - overlay.style.height = "100vh" + overlay.classList.add("jst-fade") overlay.style.backgroundColor = dir ? "black" : "transparent" - overlay.style.transition = `background-color ${duration}s` - overlay.style.zIndex = 99999; - setTimeout(() => { - overlay.style.backgroundColor = dir ? "transparent" : "black" - }, 0) + overlay.style.animationName = dir ? "jst-fadeout" : "jst-fadein" + overlay.style.animationFillMode = "forwards" + overlay.style.animationDuration = `${duration}ms` document.body.appendChild(overlay) - return new Promise(res => { + return new Promise<void>(res => { setTimeout(() => { if (dir) document.body.removeChild(overlay) res() - }, duration * 1000) + }, duration) }) -}
\ No newline at end of file +} |