WIP work on fullscreen player

This commit is contained in:
Mikko Ahlroth 2024-02-28 11:13:37 +02:00
parent 9f1af761e5
commit 4d9ba0effe
10 changed files with 324 additions and 114 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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