Refactor fullscreen player to not rely on hacks, use real router

This commit is contained in:
Mikko Ahlroth 2024-02-28 21:39:31 +02:00
parent 34ccbefa72
commit 3b3387f636
11 changed files with 138 additions and 121 deletions

View file

@ -1,7 +1,3 @@
export function requestAnimationFrame(callback) {
globalThis.requestAnimationFrame(callback);
}
export function preventPopstate() {
globalThis.history.pushState({}, "");
}

View file

@ -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)

View file

@ -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)],
[

View file

@ -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)],
[

View file

@ -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

View file

@ -12,5 +12,4 @@ pub type Action {
StartUserSkip
EndUserSkip
SelectPosition(type_: PositionSelection)
ToggleFullScreen
}

View file

@ -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"))],
),
]),

View file

@ -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}

View file

@ -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"))],
),
]),

View file

@ -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)
}

View file

@ -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);