From 3b3387f6365512070a89fd4cf49f62716dc22094 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Wed, 28 Feb 2024 21:39:31 +0200 Subject: [PATCH] Refactor fullscreen player to not rely on hacks, use real router --- src/browser_ffi.mjs | 4 - src/elekf/web/authed_view.gleam | 62 ++++++++++---- .../components/library_views/album_item.gleam | 2 +- .../library_views/artists_view.gleam | 2 +- src/elekf/web/components/player.gleam | 39 ++------- src/elekf/web/components/player/actions.gleam | 1 - .../web/components/player/full_screen.gleam | 11 ++- src/elekf/web/components/player/model.gleam | 1 - src/elekf/web/components/player/small.gleam | 14 ++-- src/elekf/web/router.gleam | 81 ++++++++++++++++--- src/player_ffi.mjs | 42 ---------- 11 files changed, 138 insertions(+), 121 deletions(-) diff --git a/src/browser_ffi.mjs b/src/browser_ffi.mjs index d444623..91635a5 100644 --- a/src/browser_ffi.mjs +++ b/src/browser_ffi.mjs @@ -1,7 +1,3 @@ export function requestAnimationFrame(callback) { globalThis.requestAnimationFrame(callback); } - -export function preventPopstate() { - globalThis.history.pushState({}, ""); -} diff --git a/src/elekf/web/authed_view.gleam b/src/elekf/web/authed_view.gleam index 539f5a2..66c9a05 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -7,6 +7,7 @@ import gleam/option import gleam/float import gleam/int import gleam/result +import gleam/set import gleam/javascript/promise import lustre/element.{text} import lustre/element/html.{div, nav, p} @@ -90,8 +91,10 @@ pub fn init(auth_data: common.AuthData) { effect.from(fn(dispatch) { router.init(dispatch) }) |> effect.map(Router) + let #(path, _query) = router.get_current_path() + let initial_view = - router.get_current_path() + path |> router.parse() |> result.unwrap(router.TrackList) |> route_to_view() @@ -194,12 +197,33 @@ pub fn update(model: Model, msg) { ) }) } - Router(router.RouteChanged(route)) -> { - case route_to_view(route) { - Ok(view) -> #(Model(..model, view: view), effect.none()) + Router(router.RouteChanged(route_and_query)) -> { + case route_to_view(route_and_query.route) { + Ok(view) -> { + let view_updated = Model(..model, view: view) + if_player(view_updated, fn(info) { + let msg = case + set.contains(route_and_query.query, router.FullScreen) + { + True -> player.FullScreenOpened + False -> player.FullScreenClosed + } + + let #(player_model, e) = + utils.update_child(info.player, msg, player.update, PlayerMsg) + + #( + Model( + ..view_updated, + play_status: HasTracks(PlayInfo(..info, player: player_model)), + ), + e, + ) + }) + } Error(_) -> { io.println_error("Unable to change to route:") - io.debug(route) + io.debug(route_and_query) #(model, effect.none()) } } @@ -223,15 +247,24 @@ pub fn view(model: Model) { div([attribute.id("authed-view-library")], [ nav([attribute.id("library-top-nav")], [ button_group.view("", [], [ - link.button(router.to_hash(router.TrackList), "", [], [ - icon("music-note-beamed", Alt("Tracks")), - ]), - link.button(router.to_hash(router.ArtistList), "", [], [ - icon("file-person", Alt("Artists")), - ]), - link.button(router.to_hash(router.AlbumList), "", [], [ - icon("disc", Alt("Albums")), - ]), + link.button( + router.to_hash(router.queryless(router.TrackList)), + "", + [], + [icon("music-note-beamed", Alt("Tracks"))], + ), + link.button( + router.to_hash(router.queryless(router.ArtistList)), + "", + [], + [icon("file-person", Alt("Artists"))], + ), + link.button( + router.to_hash(router.queryless(router.AlbumList)), + "", + [], + [icon("disc", Alt("Albums"))], + ), ]), ]), case model.view { @@ -397,7 +430,6 @@ fn route_to_view(route: router.Route) { router.ArtistList -> Ok(library_view.Artists) router.AlbumList -> Ok(library_view.Albums) router.Artist(id) -> Ok(library_view.SingleArtist(id)) - // TODO router.Album(id) -> Ok(library_view.SingleAlbum(id)) // TODO router.Settings -> Error(Nil) diff --git a/src/elekf/web/components/library_views/album_item.gleam b/src/elekf/web/components/library_views/album_item.gleam index dadb1ac..c1ed472 100644 --- a/src/elekf/web/components/library_views/album_item.gleam +++ b/src/elekf/web/components/library_views/album_item.gleam @@ -31,7 +31,7 @@ pub fn view( [ link.link( - router.to_hash(router.Album(album_id)), + router.to_hash(router.queryless(router.Album(album_id))), "library-item album-item", [attribute.id("album-list-" <> album_id_str)], [ diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index 7586f2c..49f9db2 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -72,7 +72,7 @@ fn item_view( let artist_id_str = int.to_string(artist_id) [ link.link( - router.to_hash(router.Artist(artist_id)), + router.to_hash(router.queryless(router.Artist(artist_id))), "library-item artist-item", [attribute.id("artist-list-" <> artist_id_str)], [ diff --git a/src/elekf/web/components/player.gleam b/src/elekf/web/components/player.gleam index 73397d2..dfde3c9 100644 --- a/src/elekf/web/components/player.gleam +++ b/src/elekf/web/components/player.gleam @@ -46,7 +46,8 @@ pub type Msg { ComponentInitialised ActionTriggered(actions.Action) PositionSelected(Int) - FullScreenEscaped + FullScreenOpened + FullScreenClosed } pub fn init( @@ -156,22 +157,6 @@ pub fn update(model: Model, msg) { let pos = current_track_position() #(Model(..model, position: pos), effect.none()) } - - actions.ToggleFullScreen -> { - let #(new_mode, toggle_effect) = case model.view_mode { - model.Small -> #( - model.FullScreen, - effect.from(fn(dispatch) { register_back_preventor(dispatch) }), - ) - - model.FullScreen -> { - unregister_back_preventor() - #(model.Small, effect.none()) - } - } - - #(Model(..model, view_mode: new_mode), toggle_effect) - } } } StartPlay(track_id, track) -> { @@ -252,8 +237,10 @@ pub fn update(model: Model, msg) { #(Model(..model, audio_source: option.Some(source)), effect.none()) } - FullScreenEscaped -> { - unregister_back_preventor() + FullScreenOpened -> { + #(Model(..model, view_mode: model.FullScreen), effect.none()) + } + FullScreenClosed -> { #(Model(..model, view_mode: model.Small), effect.none()) } } @@ -320,14 +307,6 @@ fn event_timeupdate(_: a) { Ok(UpdateTime(current_time())) } -fn register_back_preventor(dispatch: fn(Msg) -> Nil) { - enable_prevent_popstate(fn() { dispatch(FullScreenEscaped) }) -} - -fn unregister_back_preventor() { - disable_prevent_popstate() -} - fn start_play( settings: common.Settings, artist: Artist, @@ -403,9 +382,3 @@ fn connect_gain( @external(javascript, "../../../player_ffi.mjs", "linearRampToValue") fn linear_ramp_to_value(node: GainNode, gain: Float, at: Float) -> Nil - -@external(javascript, "../../../player_ffi.mjs", "enablePreventPopstate") -fn enable_prevent_popstate(callback: fn() -> Nil) -> Nil - -@external(javascript, "../../../player_ffi.mjs", "disablePreventPopstate") -fn disable_prevent_popstate() -> Nil diff --git a/src/elekf/web/components/player/actions.gleam b/src/elekf/web/components/player/actions.gleam index 4adc2b8..66bbda3 100644 --- a/src/elekf/web/components/player/actions.gleam +++ b/src/elekf/web/components/player/actions.gleam @@ -12,5 +12,4 @@ pub type Action { StartUserSkip EndUserSkip SelectPosition(type_: PositionSelection) - ToggleFullScreen } diff --git a/src/elekf/web/components/player/full_screen.gleam b/src/elekf/web/components/player/full_screen.gleam index 211f450..7d6436e 100644 --- a/src/elekf/web/components/player/full_screen.gleam +++ b/src/elekf/web/components/player/full_screen.gleam @@ -8,10 +8,11 @@ import elekf/web/components/icon.{Alt, icon} import elekf/web/components/track_length.{track_length} import elekf/web/components/button_group import elekf/web/components/button +import elekf/web/components/link import elekf/web/components/thumbnail import elekf/web/components/player/actions.{ Commit, EndUserSkip, Ephemeral, NextTrack, Pause, Play, PrevTrack, - SelectPosition, StartUserSkip, ToggleFullScreen, + SelectPosition, StartUserSkip, } import elekf/web/components/player/model.{type Model} @@ -65,12 +66,10 @@ pub fn view(model: Model) { [attribute.id("player-next"), event.on_click(NextTrack)], [icon("skip-forward-fill", Alt("Next"))], ), - button.view( + link.button( + "javascript:history.back()", "", - [ - attribute.id("player-fullscreen-toggle"), - event.on_click(ToggleFullScreen), - ], + [attribute.id("player-fullscreen-toggle")], [icon("arrows-angle-contract", Alt("Small player"))], ), ]), diff --git a/src/elekf/web/components/player/model.gleam b/src/elekf/web/components/player/model.gleam index 3200085..fb1f484 100644 --- a/src/elekf/web/components/player/model.gleam +++ b/src/elekf/web/components/player/model.gleam @@ -1,6 +1,5 @@ import gleam/uri import gleam/option -import plinth/browser/event import ibroadcast/authed_request.{type RequestConfig} import elekf/web/common import elekf/library.{type Library} diff --git a/src/elekf/web/components/player/small.gleam b/src/elekf/web/components/player/small.gleam index 7282aa3..502078c 100644 --- a/src/elekf/web/components/player/small.gleam +++ b/src/elekf/web/components/player/small.gleam @@ -8,12 +8,14 @@ import elekf/web/components/icon.{Alt, icon} import elekf/web/components/track_length.{track_length} import elekf/web/components/button_group import elekf/web/components/button +import elekf/web/components/link import elekf/web/components/thumbnail import elekf/web/components/player/actions.{ Commit, EndUserSkip, Ephemeral, NextTrack, Pause, Play, PrevTrack, - SelectPosition, StartUserSkip, ToggleFullScreen, + SelectPosition, StartUserSkip, } import elekf/web/components/player/model.{type Model} +import elekf/web/router pub fn view(model: Model) { let is_playing = model.state == model.Playing @@ -67,12 +69,12 @@ pub fn view(model: Model) { [attribute.id("player-next"), event.on_click(NextTrack)], [icon("skip-forward-fill", Alt("Next"))], ), - button.view( + link.button( + router.to_hash( + router.current_with_query(router.fullscreen_player()), + ), "", - [ - attribute.id("player-fullscreen-toggle"), - event.on_click(ToggleFullScreen), - ], + [attribute.id("player-fullscreen-toggle")], [icon("arrows-angle-expand", Alt("Full screen player"))], ), ]), diff --git a/src/elekf/web/router.gleam b/src/elekf/web/router.gleam index dd78131..05d264a 100644 --- a/src/elekf/web/router.gleam +++ b/src/elekf/web/router.gleam @@ -1,6 +1,9 @@ import gleam/int import gleam/string import gleam/result +import gleam/set +import gleam/uri +import gleam/list import plinth/browser/window const artists = "artists" @@ -9,10 +12,17 @@ const albums = "albums" const settings = "settings" -pub type Msg { - RouteChanged(route: Route) +const fullscreen_query = "fullscreen" + +pub const default_route = TrackList + +pub type Option { + FullScreen } +pub type Query = + set.Set(Option) + pub type Route { TrackList ArtistList @@ -22,6 +32,14 @@ pub type Route { Settings } +pub type RouteWithQuery { + RouteWithQuery(route: Route, query: Query) +} + +pub type Msg { + RouteChanged(route: RouteWithQuery) +} + pub fn init(dispatch: fn(Msg) -> Nil) { window.add_event_listener("hashchange", fn(_e) { let _ = run(dispatch) @@ -30,13 +48,14 @@ pub fn init(dispatch: fn(Msg) -> Nil) { } pub fn run(dispatch: fn(Msg) -> Nil) { - let hash = get_current_path() + let #(hash, query) = get_current_path() + let query = parse_query(query) use route <- result.try(parse(hash)) - Ok(dispatch(RouteChanged(route))) + Ok(dispatch(RouteChanged(RouteWithQuery(route, query)))) } -pub fn to_hash(route: Route) { - "#" <> to_string(route) +pub fn to_hash(route_with_query: RouteWithQuery) { + "#" <> to_string(route_with_query) } pub fn parse(route: String) { @@ -62,12 +81,40 @@ pub fn parse(route: String) { } } -pub fn get_current_path() { - result.unwrap(window.get_hash(), "/") +pub fn queryless(route: Route) { + RouteWithQuery(route, set.new()) } -fn to_string(route: Route) { - let parts = case route { +pub fn current_with_query(query: Query) { + let #(path, _) = get_current_path() + let route = result.unwrap(parse(path), default_route) + RouteWithQuery(route, query) +} + +pub fn fullscreen_player() { + set.new() + |> set.insert(FullScreen) +} + +pub fn parse_query(query: List(#(String, String))) { + list.fold(query, set.new(), fn(acc, item) { + let #(key, _val) = item + case key { + q if q == fullscreen_query -> set.insert(acc, FullScreen) + _ -> acc + } + }) +} + +pub fn get_current_path() { + let hash = result.unwrap(window.get_hash(), "/") + let #(path, query) = result.unwrap(string.split_once(hash, "?"), #(hash, "")) + let query = result.unwrap(uri.parse_query(query), []) + #(path, query) +} + +fn to_string(route_with_query: RouteWithQuery) { + let parts = case route_with_query.route { TrackList -> [] ArtistList -> [artists] AlbumList -> [albums] @@ -75,6 +122,18 @@ fn to_string(route: Route) { Album(id) -> [albums, int.to_string(id)] Settings -> [settings] } + let query_str = query_to_string(route_with_query.query) - "/" <> string.join(parts, "/") + "/" <> string.join(parts, "/") <> "?" <> query_str +} + +fn query_to_string(query: Query) { + let query_parts = + set.fold(query, [], fn(acc, item) { + case item { + FullScreen -> [#(fullscreen_query, ""), ..acc] + } + }) + + uri.query_to_string(query_parts) } diff --git a/src/player_ffi.mjs b/src/player_ffi.mjs index bfd8ce7..e71b79b 100644 --- a/src/player_ffi.mjs +++ b/src/player_ffi.mjs @@ -1,5 +1,3 @@ -import { preventPopstate } from "./browser_ffi.mjs"; - const player_id = "player-elem"; const track_id = "player-track"; @@ -13,21 +11,6 @@ let player; */ let track; -/** - * @type {boolean} - */ -let popstatePreventEnabled = false; - -/** - * @type {boolean} - */ -let popstateListenerAdded = false; - -/** - * @type {(function(): undefined) | null} - */ -let popstateListenerCallback = null; - export function registerCallback(event, callback) { getPlayer(); player.addEventListener(event, callback); @@ -85,31 +68,6 @@ export function linearRampToValue(node, gain, at) { node.gain.linearRampToValueAtTime(gain, at); } -export function enablePreventPopstate(callback) { - if (!popstateListenerAdded) { - window.addEventListener("popstate", popstateListener); - popstateListenerAdded = true; - } - - popstatePreventEnabled = true; - popstateListenerCallback = callback; -} - -export function disablePreventPopstate() { - popstatePreventEnabled = false; - popstateListenerCallback = null; -} - -function popstateListener() { - if (popstatePreventEnabled) { - preventPopstate(); - - if (popstateListenerCallback) { - popstateListenerCallback(); - } - } -} - function getPlayer() { if (player === undefined) { player = document.getElementById(player_id);