Refactor fullscreen player to not rely on hacks, use real router
This commit is contained in:
parent
34ccbefa72
commit
3b3387f636
11 changed files with 138 additions and 121 deletions
|
@ -1,7 +1,3 @@
|
|||
export function requestAnimationFrame(callback) {
|
||||
globalThis.requestAnimationFrame(callback);
|
||||
}
|
||||
|
||||
export function preventPopstate() {
|
||||
globalThis.history.pushState({}, "");
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)],
|
||||
[
|
||||
|
|
|
@ -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)],
|
||||
[
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,5 +12,4 @@ pub type Action {
|
|||
StartUserSkip
|
||||
EndUserSkip
|
||||
SelectPosition(type_: PositionSelection)
|
||||
ToggleFullScreen
|
||||
}
|
||||
|
|
|
@ -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"))],
|
||||
),
|
||||
]),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"))],
|
||||
),
|
||||
]),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue