From 4d9ba0effee22ecc68b6e407e77cff3e87309dd3 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Wed, 28 Feb 2024 11:13:37 +0200 Subject: [PATCH] WIP work on fullscreen player --- src/browser_ffi.mjs | 4 + src/elekf/web/authed_view.gleam | 22 +-- src/elekf/web/components/player.gleam | 56 +++++-- src/elekf/web/components/player/actions.gleam | 1 + .../web/components/player/full_screen.gleam | 108 ++++++++++++- src/elekf/web/components/player/model.gleam | 1 + src/elekf/web/components/player/small.gleam | 148 ++++++++++-------- src/elekf/web/components/thumbnail.gleam | 2 +- src/player_ffi.mjs | 42 +++++ style.css | 54 +++++-- 10 files changed, 324 insertions(+), 114 deletions(-) diff --git a/src/browser_ffi.mjs b/src/browser_ffi.mjs index 91635a5..d444623 100644 --- a/src/browser_ffi.mjs +++ b/src/browser_ffi.mjs @@ -1,3 +1,7 @@ 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 888210b..539f5a2 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -266,22 +266,12 @@ pub fn view(model: Model) { ]) }, ]), - div( - [ - attribute.id("authed-view-player"), - attribute.class("glass-bg glass-shadow glass-blur glass-border"), - ], - [ - case model.play_status { - HasTracks(PlayInfo(player: player, ..)) -> - div([attribute.id("player")], [ - player.view(player) - |> element.map(PlayerMsg), - ]) - NoTracks -> text("") - }, - ], - ), + case model.play_status { + HasTracks(PlayInfo(player: player, ..)) -> + player.view(player) + |> element.map(PlayerMsg) + NoTracks -> text("") + }, ]) } diff --git a/src/elekf/web/components/player.gleam b/src/elekf/web/components/player.gleam index 58223ba..73397d2 100644 --- a/src/elekf/web/components/player.gleam +++ b/src/elekf/web/components/player.gleam @@ -21,6 +21,7 @@ import elektrofoni import elekf/utils/lustre import elekf/web/common import elekf/web/volume +import elekf/web/components/player/full_screen import elekf/web/components/player/small import elekf/web/components/player/model.{ type AudioContext, type GainNode, type MediaElementAudioSourceNode, type Model, @@ -42,10 +43,10 @@ pub type Msg { UpdateRequestConfig(RequestConfig) BackwardSeekRequested(Float) ForwardSeekRequested(Float) - CreateMediaElementAudioSourceNode - FullScreenToggled + ComponentInitialised ActionTriggered(actions.Action) PositionSelected(Int) + FullScreenEscaped } pub fn init( @@ -92,9 +93,7 @@ pub fn init( let seek_to = option.unwrap(data.seek_time, 0.0) dispatch(PositionSelected(float.round(seek_to))) }) - lustre.after_next_render(fn() { - dispatch(CreateMediaElementAudioSourceNode) - }) + lustre.after_next_render(fn() { dispatch(ComponentInitialised) }) }) start_play(settings, artist, album, track) @@ -157,6 +156,22 @@ 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) -> { @@ -231,20 +246,15 @@ pub fn update(model: Model, msg) { skip_to_time(pos) #(Model(..model, position: pos), effect.none()) } - CreateMediaElementAudioSourceNode -> { + ComponentInitialised -> { let source = create_media_element_source(model.audio_context) connect_gain(model.gain_node, model.audio_context, source) #(Model(..model, audio_source: option.Some(source)), effect.none()) } - - FullScreenToggled -> { - let new_mode = case model.view_mode { - model.Small -> model.FullScreen - model.FullScreen -> model.Small - } - - #(Model(..model, view_mode: new_mode), effect.none()) + FullScreenEscaped -> { + unregister_back_preventor() + #(Model(..model, view_mode: model.Small), effect.none()) } } } @@ -271,9 +281,11 @@ pub fn view(model: Model) { model.Small -> small.view(model) |> element.map(ActionTriggered) + model.FullScreen -> + full_screen.view(model) + |> element.map(ActionTriggered) }, ]) - //FullScreen -> full_screen.view() } fn form_url( @@ -308,6 +320,14 @@ 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, @@ -383,3 +403,9 @@ 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 66bbda3..4adc2b8 100644 --- a/src/elekf/web/components/player/actions.gleam +++ b/src/elekf/web/components/player/actions.gleam @@ -12,4 +12,5 @@ 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 2237cff..211f450 100644 --- a/src/elekf/web/components/player/full_screen.gleam +++ b/src/elekf/web/components/player/full_screen.gleam @@ -1,3 +1,107 @@ -pub fn view() { - todo +import gleam/int +import gleam/option +import lustre/element.{text} +import lustre/element/html.{div, input, p} +import lustre/attribute +import lustre/event +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/thumbnail +import elekf/web/components/player/actions.{ + Commit, EndUserSkip, Ephemeral, NextTrack, Pause, Play, PrevTrack, + SelectPosition, StartUserSkip, ToggleFullScreen, +} +import elekf/web/components/player/model.{type Model} + +pub fn view(model: Model) { + let is_playing = model.state == model.Playing + let current_time_padding = case model.track.length > 3600 { + True -> track_length.Hours + False -> track_length.Auto + } + + div( + [ + attribute.id("full-player-wrapper"), + attribute.class("player-wrapper glass-bg glass-blur"), + ], + [ + thumbnail.maybe_view( + option.Some(model.settings), + "full-player-artwork", + "full-player-thumbnail", + "full-player-thumbnail full-player-thumbnail-placeholder", + model.track.artwork_id, + model.album.name, + ), + p([attribute.id("full-player-track-name")], [text(model.track.title)]), + p([attribute.id("full-player-artist-name")], [text(model.artist.name)]), + p([attribute.id("full-player-album-name")], [text(model.album.name)]), + div([attribute.id("player-controls")], [ + button_group.view("", [attribute.id("player-controls-buttons")], [ + button.view( + "", + [attribute.id("player-previous"), event.on_click(PrevTrack)], + [icon("skip-backward-fill", Alt("Previous"))], + ), + case is_playing { + True -> + button.view( + "", + [attribute.id("player-play-pause"), event.on_click(Pause)], + [icon("pause-fill", Alt("Pause"))], + ) + False -> + button.view( + "", + [attribute.id("player-play-pause"), event.on_click(Play)], + [icon("play-fill", Alt("Play"))], + ) + }, + button.view( + "", + [attribute.id("player-next"), event.on_click(NextTrack)], + [icon("skip-forward-fill", Alt("Next"))], + ), + button.view( + "", + [ + attribute.id("player-fullscreen-toggle"), + event.on_click(ToggleFullScreen), + ], + [icon("arrows-angle-contract", Alt("Small player"))], + ), + ]), + p( + [ + attribute.id("player-time-elapsed"), + attribute.class("player-time"), + attribute.attribute("aria-label", "Time elapsed"), + ], + [track_length(model.position, current_time_padding)], + ), + p( + [ + attribute.id("player-time-total"), + attribute.class("player-time"), + attribute.attribute("aria-label", "Total time"), + ], + [track_length(model.track.length, track_length.Auto)], + ), + input([ + attribute.id("player-track"), + attribute.type_("range"), + attribute.min("0"), + attribute.max(int.to_string(model.track.length)), + attribute.attribute("aria-label", "Track position"), + event.on("change", fn(_) { Ok(SelectPosition(Commit)) }), + event.on_mouse_down(StartUserSkip), + event.on_mouse_up(EndUserSkip), + event.on_input(fn(_) { SelectPosition(Ephemeral) }), + ]), + ]), + ], + ) } diff --git a/src/elekf/web/components/player/model.gleam b/src/elekf/web/components/player/model.gleam index fb1f484..3200085 100644 --- a/src/elekf/web/components/player/model.gleam +++ b/src/elekf/web/components/player/model.gleam @@ -1,5 +1,6 @@ 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 9cc53f3..7282aa3 100644 --- a/src/elekf/web/components/player/small.gleam +++ b/src/elekf/web/components/player/small.gleam @@ -11,7 +11,7 @@ import elekf/web/components/button import elekf/web/components/thumbnail import elekf/web/components/player/actions.{ Commit, EndUserSkip, Ephemeral, NextTrack, Pause, Play, PrevTrack, - SelectPosition, StartUserSkip, + SelectPosition, StartUserSkip, ToggleFullScreen, } import elekf/web/components/player/model.{type Model} @@ -22,72 +22,88 @@ pub fn view(model: Model) { False -> track_length.Auto } - div([attribute.id("small-player-wrapper")], [ - thumbnail.maybe_view( - option.Some(model.settings), - "small-player-artwork", - "small-player-thumbnail", - "small-player-thumbnail small-player-thumbnail-placeholder", - model.track.artwork_id, - model.album.name, - ), - p([attribute.id("small-player-track-name")], [text(model.track.title)]), - p([attribute.id("small-player-artist-name")], [text(model.artist.name)]), - p([attribute.id("small-player-album-name")], [text(model.album.name)]), - div([attribute.id("player-controls")], [ - button_group.view("", [attribute.id("player-controls-buttons")], [ - button.view( - "button-small", - [attribute.id("player-previous"), event.on_click(PrevTrack)], - [icon("skip-backward-fill", Alt("Previous"))], - ), - case is_playing { - True -> - button.view( - "button-small", - [attribute.id("player-play-pause"), event.on_click(Pause)], - [icon("pause-fill", Alt("Pause"))], - ) - False -> - button.view( - "button-small", - [attribute.id("player-play-pause"), event.on_click(Play)], - [icon("play-fill", Alt("Play"))], - ) - }, - button.view( - "button-small", - [attribute.id("player-next"), event.on_click(NextTrack)], - [icon("skip-forward-fill", Alt("Next"))], - ), - ]), - p( - [ - attribute.id("player-time-elapsed"), - attribute.class("player-time"), - attribute.attribute("aria-label", "Time elapsed"), - ], - [track_length(model.position, current_time_padding)], + div( + [ + attribute.id("small-player-wrapper"), + attribute.class( + "player-wrapper glass-bg glass-shadow glass-blur glass-border", ), - p( - [ - attribute.id("player-time-total"), - attribute.class("player-time"), - attribute.attribute("aria-label", "Total time"), - ], - [track_length(model.track.length, track_length.Auto)], + ], + [ + thumbnail.maybe_view( + option.Some(model.settings), + "small-player-artwork", + "small-player-thumbnail", + "small-player-thumbnail small-player-thumbnail-placeholder", + model.track.artwork_id, + model.album.name, ), - input([ - attribute.id("player-track"), - attribute.type_("range"), - attribute.min("0"), - attribute.max(int.to_string(model.track.length)), - attribute.attribute("aria-label", "Track position"), - event.on("change", fn(_) { Ok(SelectPosition(Commit)) }), - event.on_mouse_down(StartUserSkip), - event.on_mouse_up(EndUserSkip), - event.on_input(fn(_) { SelectPosition(Ephemeral) }), + p([attribute.id("small-player-track-name")], [text(model.track.title)]), + p([attribute.id("small-player-artist-name")], [text(model.artist.name)]), + p([attribute.id("small-player-album-name")], [text(model.album.name)]), + div([attribute.id("player-controls")], [ + button_group.view("", [attribute.id("player-controls-buttons")], [ + button.view( + "", + [attribute.id("player-previous"), event.on_click(PrevTrack)], + [icon("skip-backward-fill", Alt("Previous"))], + ), + case is_playing { + True -> + button.view( + "", + [attribute.id("player-play-pause"), event.on_click(Pause)], + [icon("pause-fill", Alt("Pause"))], + ) + False -> + button.view( + "", + [attribute.id("player-play-pause"), event.on_click(Play)], + [icon("play-fill", Alt("Play"))], + ) + }, + button.view( + "", + [attribute.id("player-next"), event.on_click(NextTrack)], + [icon("skip-forward-fill", Alt("Next"))], + ), + button.view( + "", + [ + attribute.id("player-fullscreen-toggle"), + event.on_click(ToggleFullScreen), + ], + [icon("arrows-angle-expand", Alt("Full screen player"))], + ), + ]), + p( + [ + attribute.id("player-time-elapsed"), + attribute.class("player-time"), + attribute.attribute("aria-label", "Time elapsed"), + ], + [track_length(model.position, current_time_padding)], + ), + p( + [ + attribute.id("player-time-total"), + attribute.class("player-time"), + attribute.attribute("aria-label", "Total time"), + ], + [track_length(model.track.length, track_length.Auto)], + ), + input([ + attribute.id("player-track"), + attribute.type_("range"), + attribute.min("0"), + attribute.max(int.to_string(model.track.length)), + attribute.attribute("aria-label", "Track position"), + event.on("change", fn(_) { Ok(SelectPosition(Commit)) }), + event.on_mouse_down(StartUserSkip), + event.on_mouse_up(EndUserSkip), + event.on_input(fn(_) { SelectPosition(Ephemeral) }), + ]), ]), - ]), - ]) + ], + ) } diff --git a/src/elekf/web/components/thumbnail.gleam b/src/elekf/web/components/thumbnail.gleam index 155e17b..d6c62e8 100644 --- a/src/elekf/web/components/thumbnail.gleam +++ b/src/elekf/web/components/thumbnail.gleam @@ -17,7 +17,7 @@ pub fn view(s: Settings, image_id: String, html_id: String, class: String) { [ img([ attribute.id(html_id), - attribute.alt(""), + attribute.attribute("aria-hidden", "true"), attribute.src(artwork.url(s.artwork_server, image_id, artwork.S300)), attribute.attribute("loading", "lazy"), attribute.width(300), diff --git a/src/player_ffi.mjs b/src/player_ffi.mjs index e71b79b..bfd8ce7 100644 --- a/src/player_ffi.mjs +++ b/src/player_ffi.mjs @@ -1,3 +1,5 @@ +import { preventPopstate } from "./browser_ffi.mjs"; + const player_id = "player-elem"; const track_id = "player-track"; @@ -11,6 +13,21 @@ 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); @@ -68,6 +85,31 @@ 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); diff --git a/style.css b/style.css index c7d5e07..0dbf4f3 100644 --- a/style.css +++ b/style.css @@ -167,27 +167,25 @@ single-album-view { overflow-y: auto; } -#authed-view-player { +#authed-view-wrapper[data-player-status="closed"] .player-wrapper { + display: none; +} + +#small-player-wrapper { position: absolute; bottom: 0; left: 0; width: 100%; padding: var(--side-margin); - border-radius: 10px 10px 0 0; - border-bottom: none; -} - -#authed-view-wrapper[data-player-status="closed"] #authed-view-player { - display: none; -} - -#small-player-wrapper { display: grid; grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr; gap: var(--side-margin); font-weight: 100; + + border-radius: 10px 10px 0 0; + border-bottom: none; } #small-player-track-name, @@ -198,24 +196,52 @@ single-album-view { overflow-x: hidden; } -#small-player-track-name { +#small-player-track-name, +#full-player-track-name { grid-area: track; font-weight: normal; } -#small-player-artist-name { +#small-player-artist-name, +#full-player-artist-name { grid-area: artist; } -#small-player-album-name { +#small-player-album-name, +#full-player-album-name { grid-area: album; } -#small-player-artwork-wrapper { +#small-player-artwork-wrapper, +#full-player-artwork-wrapper { grid-area: art; } +#full-player-wrapper { + position: absolute; + bottom: 0; + left: 0; + top: 0; + right: 0; + width: 100%; + height: 100%; + padding: var(--double-margin); + + display: grid; + grid-template: "art art" "track track" "artist artist" "album album" ". ." 1fr "controls controls" auto / 1fr 1fr; + gap: var(--side-margin); + + text-align: center; + font-weight: 100; + font-size: 125%; +} + +#full-player-artwork-wrapper img { + margin: 0 auto; + padding-bottom: var(--side-margin); +} + #player-controls { grid-area: controls;