Initial work for full screen player
This commit is contained in:
parent
773627bd5d
commit
9f1af761e5
10 changed files with 313 additions and 204 deletions
|
@ -25,6 +25,8 @@ import elekf/library/track.{type Track}
|
||||||
import elekf/transfer/library as library_transfer
|
import elekf/transfer/library as library_transfer
|
||||||
import elekf/web/router
|
import elekf/web/router
|
||||||
import elekf/web/components/player
|
import elekf/web/components/player
|
||||||
|
import elekf/web/components/player/model as player_model
|
||||||
|
import elekf/web/components/player/actions as player_actions
|
||||||
import elekf/web/components/library_view
|
import elekf/web/components/library_view
|
||||||
import elekf/web/components/library_views/tracks_view
|
import elekf/web/components/library_views/tracks_view
|
||||||
import elekf/web/components/library_views/artists_view
|
import elekf/web/components/library_views/artists_view
|
||||||
|
@ -52,7 +54,7 @@ pub type PlayInfo {
|
||||||
track: Track,
|
track: Track,
|
||||||
play_queue: PlayQueue,
|
play_queue: PlayQueue,
|
||||||
play_index: Int,
|
play_index: Int,
|
||||||
player: player.Model,
|
player: player_model.Model,
|
||||||
current_track_status: CurrentTrackStatus,
|
current_track_status: CurrentTrackStatus,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -156,10 +158,12 @@ pub fn update(model: Model, msg) {
|
||||||
let #(status, effect) = handle_start_play(model, tracks, position)
|
let #(status, effect) = handle_start_play(model, tracks, position)
|
||||||
#(Model(..model, play_status: status), effect)
|
#(Model(..model, play_status: status), effect)
|
||||||
}
|
}
|
||||||
PlayerMsg(player.NextTrack) | PlayerMsg(player.PrevTrack) -> {
|
PlayerMsg(player.ActionTriggered(player_actions.NextTrack))
|
||||||
|
| PlayerMsg(player.ActionTriggered(player_actions.PrevTrack)) -> {
|
||||||
if_player(model, fn(info) {
|
if_player(model, fn(info) {
|
||||||
let next_index = case msg {
|
let next_index = case msg {
|
||||||
PlayerMsg(player.NextTrack) -> info.play_index + 1
|
PlayerMsg(player.ActionTriggered(player_actions.NextTrack)) ->
|
||||||
|
info.play_index + 1
|
||||||
_ -> info.play_index - 1
|
_ -> info.play_index - 1
|
||||||
}
|
}
|
||||||
case list.at(info.play_queue, next_index) {
|
case list.at(info.play_queue, next_index) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ pub fn view(
|
||||||
item: LibraryItem(Album),
|
item: LibraryItem(Album),
|
||||||
) {
|
) {
|
||||||
let #(album_id, album) = item
|
let #(album_id, album) = item
|
||||||
|
let album_id_str = int.to_string(album_id)
|
||||||
let tracks = album_utils.get_tracks(library, album)
|
let tracks = album_utils.get_tracks(library, album)
|
||||||
let artist_name = case album.artist_id {
|
let artist_name = case album.artist_id {
|
||||||
0 -> "Unknown artist"
|
0 -> "Unknown artist"
|
||||||
|
@ -32,10 +33,11 @@ pub fn view(
|
||||||
link.link(
|
link.link(
|
||||||
router.to_hash(router.Album(album_id)),
|
router.to_hash(router.Album(album_id)),
|
||||||
"library-item album-item",
|
"library-item album-item",
|
||||||
[attribute.id("album-list-" <> int.to_string(album_id))],
|
[attribute.id("album-list-" <> album_id_str)],
|
||||||
[
|
[
|
||||||
thumbnail.maybe_item_thumbnail(
|
thumbnail.maybe_item_thumbnail(
|
||||||
settings,
|
settings,
|
||||||
|
"album-item-thumbnail-" <> album_id_str,
|
||||||
{ first_track.1 }.artwork_id,
|
{ first_track.1 }.artwork_id,
|
||||||
album.name,
|
album.name,
|
||||||
),
|
),
|
||||||
|
|
|
@ -69,14 +69,16 @@ fn item_view(
|
||||||
item: LibraryItem(Artist),
|
item: LibraryItem(Artist),
|
||||||
) {
|
) {
|
||||||
let #(artist_id, artist) = item
|
let #(artist_id, artist) = item
|
||||||
|
let artist_id_str = int.to_string(artist_id)
|
||||||
[
|
[
|
||||||
link.link(
|
link.link(
|
||||||
router.to_hash(router.Artist(artist_id)),
|
router.to_hash(router.Artist(artist_id)),
|
||||||
"library-item artist-item",
|
"library-item artist-item",
|
||||||
[attribute.id("artist-list-" <> int.to_string(artist_id))],
|
[attribute.id("artist-list-" <> artist_id_str)],
|
||||||
[
|
[
|
||||||
thumbnail.maybe_item_thumbnail(
|
thumbnail.maybe_item_thumbnail(
|
||||||
model.settings,
|
model.settings,
|
||||||
|
"artist-list-thumbnail-" <> artist_id_str,
|
||||||
artist.artwork_id,
|
artist.artwork_id,
|
||||||
artist.name,
|
artist.name,
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,8 +5,8 @@ import gleam/int
|
||||||
import gleam/float
|
import gleam/float
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/io
|
import gleam/io
|
||||||
import lustre/element.{text}
|
import lustre/element
|
||||||
import lustre/element/html.{audio, div, input, p}
|
import lustre/element/html.{audio, div}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/event
|
import lustre/event
|
||||||
import lustre/effect
|
import lustre/effect
|
||||||
|
@ -14,74 +14,38 @@ import plinth/browser/media/metadata
|
||||||
import plinth/browser/media/session
|
import plinth/browser/media/session
|
||||||
import plinth/browser/media/action
|
import plinth/browser/media/action
|
||||||
import plinth/browser/media/position
|
import plinth/browser/media/position
|
||||||
|
import ibroadcast/authed_request.{type RequestConfig}
|
||||||
|
import ibroadcast/streaming
|
||||||
|
import ibroadcast/artwork
|
||||||
import elektrofoni
|
import elektrofoni
|
||||||
import elekf/utils/lustre
|
import elekf/utils/lustre
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
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/volume
|
import elekf/web/volume
|
||||||
|
import elekf/web/components/player/small
|
||||||
|
import elekf/web/components/player/model.{
|
||||||
|
type AudioContext, type GainNode, type MediaElementAudioSourceNode, type Model,
|
||||||
|
type PlayState, Model, Paused, Playing,
|
||||||
|
}
|
||||||
|
import elekf/web/components/player/actions
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/artist.{type Artist}
|
import elekf/library/artist.{type Artist}
|
||||||
import elekf/library/album.{type Album}
|
import elekf/library/album.{type Album}
|
||||||
import elekf/utils/date
|
import elekf/utils/date
|
||||||
import ibroadcast/authed_request.{type RequestConfig}
|
|
||||||
import ibroadcast/streaming
|
|
||||||
import ibroadcast/artwork
|
|
||||||
|
|
||||||
const default_seek_offset = 10.0
|
const default_seek_offset = 10.0
|
||||||
|
|
||||||
pub type AudioContext
|
|
||||||
|
|
||||||
pub type MediaElementAudioSourceNode
|
|
||||||
|
|
||||||
pub type GainNode
|
|
||||||
|
|
||||||
pub type PlayState {
|
|
||||||
Playing
|
|
||||||
Paused
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Model {
|
|
||||||
Model(
|
|
||||||
settings: common.Settings,
|
|
||||||
library: Library,
|
|
||||||
track_id: Int,
|
|
||||||
track: Track,
|
|
||||||
artist: Artist,
|
|
||||||
album: Album,
|
|
||||||
url: uri.Uri,
|
|
||||||
position: Int,
|
|
||||||
state: PlayState,
|
|
||||||
loading_stream: Bool,
|
|
||||||
request_config: RequestConfig,
|
|
||||||
user_is_skipping: Bool,
|
|
||||||
audio_context: AudioContext,
|
|
||||||
audio_source: option.Option(MediaElementAudioSourceNode),
|
|
||||||
gain_node: GainNode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
StartPlay(Int, Track)
|
StartPlay(Int, Track)
|
||||||
NextTrack
|
PlayStateUpdated(PlayState)
|
||||||
PrevTrack
|
|
||||||
Play
|
|
||||||
Pause
|
|
||||||
SetPlayState(PlayState)
|
|
||||||
Clear
|
|
||||||
UpdateTime(Float)
|
UpdateTime(Float)
|
||||||
UpdateRequestConfig(RequestConfig)
|
UpdateRequestConfig(RequestConfig)
|
||||||
StartUserSkip
|
BackwardSeekRequested(Float)
|
||||||
EndUserSkip
|
ForwardSeekRequested(Float)
|
||||||
PositionSelected(Int)
|
|
||||||
PositionChanged(Int)
|
|
||||||
SeekBackward(Float)
|
|
||||||
SeekForward(Float)
|
|
||||||
CreateMediaElementAudioSourceNode
|
CreateMediaElementAudioSourceNode
|
||||||
|
FullScreenToggled
|
||||||
|
ActionTriggered(actions.Action)
|
||||||
|
PositionSelected(Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
|
@ -96,17 +60,33 @@ pub fn init(
|
||||||
|
|
||||||
let action_effect =
|
let action_effect =
|
||||||
effect.from(fn(dispatch) {
|
effect.from(fn(dispatch) {
|
||||||
register_action_handler(action.NextTrack, NextTrack, dispatch)
|
register_action_handler(
|
||||||
register_action_handler(action.PreviousTrack, PrevTrack, dispatch)
|
action.NextTrack,
|
||||||
register_action_handler(action.Play, Play, dispatch)
|
ActionTriggered(actions.NextTrack),
|
||||||
register_action_handler(action.Pause, Pause, dispatch)
|
dispatch,
|
||||||
|
)
|
||||||
|
register_action_handler(
|
||||||
|
action.PreviousTrack,
|
||||||
|
ActionTriggered(actions.PrevTrack),
|
||||||
|
dispatch,
|
||||||
|
)
|
||||||
|
register_action_handler(
|
||||||
|
action.Play,
|
||||||
|
ActionTriggered(actions.Play),
|
||||||
|
dispatch,
|
||||||
|
)
|
||||||
|
register_action_handler(
|
||||||
|
action.Pause,
|
||||||
|
ActionTriggered(actions.Pause),
|
||||||
|
dispatch,
|
||||||
|
)
|
||||||
session.set_action_handler(action.SeekBackward, fn(data) {
|
session.set_action_handler(action.SeekBackward, fn(data) {
|
||||||
let seek_amount = option.unwrap(data.seek_offset, default_seek_offset)
|
let seek_amount = option.unwrap(data.seek_offset, default_seek_offset)
|
||||||
dispatch(SeekBackward(seek_amount))
|
dispatch(BackwardSeekRequested(seek_amount))
|
||||||
})
|
})
|
||||||
session.set_action_handler(action.SeekForward, fn(data) {
|
session.set_action_handler(action.SeekForward, fn(data) {
|
||||||
let seek_amount = option.unwrap(data.seek_offset, default_seek_offset)
|
let seek_amount = option.unwrap(data.seek_offset, default_seek_offset)
|
||||||
dispatch(SeekForward(seek_amount))
|
dispatch(ForwardSeekRequested(seek_amount))
|
||||||
})
|
})
|
||||||
session.set_action_handler(action.SeekTo, fn(data) {
|
session.set_action_handler(action.SeekTo, fn(data) {
|
||||||
let seek_to = option.unwrap(data.seek_time, 0.0)
|
let seek_to = option.unwrap(data.seek_time, 0.0)
|
||||||
|
@ -138,6 +118,7 @@ pub fn init(
|
||||||
audio_context,
|
audio_context,
|
||||||
option.None,
|
option.None,
|
||||||
create_gain(audio_context),
|
create_gain(audio_context),
|
||||||
|
model.Small,
|
||||||
),
|
),
|
||||||
action_effect,
|
action_effect,
|
||||||
)
|
)
|
||||||
|
@ -145,6 +126,39 @@ pub fn init(
|
||||||
|
|
||||||
pub fn update(model: Model, msg) {
|
pub fn update(model: Model, msg) {
|
||||||
case msg {
|
case msg {
|
||||||
|
ActionTriggered(action) -> {
|
||||||
|
case action {
|
||||||
|
actions.Play -> {
|
||||||
|
play()
|
||||||
|
#(model, effect.none())
|
||||||
|
}
|
||||||
|
actions.Pause -> {
|
||||||
|
pause()
|
||||||
|
#(model, effect.none())
|
||||||
|
}
|
||||||
|
actions.NextTrack -> #(model, effect.none())
|
||||||
|
actions.PrevTrack -> #(model, effect.none())
|
||||||
|
actions.Clear -> #(model, effect.none())
|
||||||
|
actions.StartUserSkip -> #(
|
||||||
|
Model(..model, user_is_skipping: True),
|
||||||
|
effect.none(),
|
||||||
|
)
|
||||||
|
actions.EndUserSkip -> #(
|
||||||
|
Model(..model, user_is_skipping: False),
|
||||||
|
effect.none(),
|
||||||
|
)
|
||||||
|
actions.SelectPosition(actions.Commit) -> {
|
||||||
|
let pos = current_track_position()
|
||||||
|
skip_to_time(pos)
|
||||||
|
|
||||||
|
#(Model(..model, position: pos), effect.none())
|
||||||
|
}
|
||||||
|
actions.SelectPosition(actions.Ephemeral) -> {
|
||||||
|
let pos = current_track_position()
|
||||||
|
#(Model(..model, position: pos), effect.none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
StartPlay(track_id, track) -> {
|
StartPlay(track_id, track) -> {
|
||||||
let artist = library.assert_artist(model.library, track.artist_id)
|
let artist = library.assert_artist(model.library, track.artist_id)
|
||||||
let album = library.assert_album(model.library, track.album_id)
|
let album = library.assert_album(model.library, track.album_id)
|
||||||
|
@ -170,21 +184,10 @@ pub fn update(model: Model, msg) {
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Play -> {
|
PlayStateUpdated(play_state) -> #(
|
||||||
play()
|
|
||||||
#(model, effect.none())
|
|
||||||
}
|
|
||||||
Pause -> {
|
|
||||||
pause()
|
|
||||||
#(model, effect.none())
|
|
||||||
}
|
|
||||||
SetPlayState(play_state) -> #(
|
|
||||||
Model(..model, state: play_state),
|
Model(..model, state: play_state),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
NextTrack -> #(model, effect.none())
|
|
||||||
PrevTrack -> #(model, effect.none())
|
|
||||||
Clear -> #(model, effect.none())
|
|
||||||
UpdateTime(time) ->
|
UpdateTime(time) ->
|
||||||
case model.user_is_skipping {
|
case model.user_is_skipping {
|
||||||
True -> #(model, effect.none())
|
True -> #(model, effect.none())
|
||||||
|
@ -214,123 +217,63 @@ pub fn update(model: Model, msg) {
|
||||||
Model(..model, request_config: config),
|
Model(..model, request_config: config),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
StartUserSkip -> #(Model(..model, user_is_skipping: True), effect.none())
|
BackwardSeekRequested(amount) -> {
|
||||||
EndUserSkip -> #(Model(..model, user_is_skipping: False), effect.none())
|
let new_pos = float.round(current_time() -. amount)
|
||||||
|
skip_to_time(new_pos)
|
||||||
|
#(Model(..model, position: new_pos), effect.none())
|
||||||
|
}
|
||||||
|
ForwardSeekRequested(amount) -> {
|
||||||
|
let new_pos = float.round(current_time() +. amount)
|
||||||
|
skip_to_time(new_pos)
|
||||||
|
#(Model(..model, position: new_pos), effect.none())
|
||||||
|
}
|
||||||
PositionSelected(pos) -> {
|
PositionSelected(pos) -> {
|
||||||
skip_to_time(pos)
|
skip_to_time(pos)
|
||||||
|
|
||||||
#(Model(..model, position: pos), effect.none())
|
#(Model(..model, position: pos), effect.none())
|
||||||
}
|
}
|
||||||
PositionChanged(pos) -> {
|
|
||||||
#(Model(..model, position: pos), effect.none())
|
|
||||||
}
|
|
||||||
SeekBackward(amount) -> {
|
|
||||||
let new_pos = float.round(current_time() -. amount)
|
|
||||||
// TODO: Stop lying to hayleigh
|
|
||||||
update(model, PositionSelected(new_pos))
|
|
||||||
}
|
|
||||||
SeekForward(amount) -> {
|
|
||||||
let new_pos = float.round(current_time() +. amount)
|
|
||||||
update(model, PositionSelected(new_pos))
|
|
||||||
}
|
|
||||||
CreateMediaElementAudioSourceNode -> {
|
CreateMediaElementAudioSourceNode -> {
|
||||||
let source = create_media_element_source(model.audio_context)
|
let source = create_media_element_source(model.audio_context)
|
||||||
connect_gain(model.gain_node, model.audio_context, source)
|
connect_gain(model.gain_node, model.audio_context, source)
|
||||||
|
|
||||||
#(Model(..model, audio_source: option.Some(source)), effect.none())
|
#(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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(model: Model) {
|
pub fn view(model: Model) {
|
||||||
let is_playing = model.state == Playing
|
|
||||||
let src = uri.to_string(model.url)
|
let src = uri.to_string(model.url)
|
||||||
let current_time_padding = case model.track.length > 3600 {
|
|
||||||
True -> track_length.Hours
|
|
||||||
False -> track_length.Auto
|
|
||||||
}
|
|
||||||
|
|
||||||
div([attribute.id("player-wrapper")], [
|
div([attribute.id("player-wrapper")], [
|
||||||
thumbnail.maybe_view(
|
audio(
|
||||||
option.Some(model.settings),
|
[
|
||||||
"player-wrapper-thumbnail",
|
attribute.id("player-elem"),
|
||||||
"player-wrapper-thumbnail player-wrapper-thumbnail-placeholder",
|
attribute.src(src),
|
||||||
model.track.artwork_id,
|
attribute.controls(False),
|
||||||
model.album.name,
|
attribute.autoplay(True),
|
||||||
|
attribute.attribute("crossorigin", "anonymous"),
|
||||||
|
event.on("play", event_play),
|
||||||
|
event.on("pause", event_pause),
|
||||||
|
event.on("ended", event_ended),
|
||||||
|
event.on("timeupdate", event_timeupdate),
|
||||||
|
],
|
||||||
|
[],
|
||||||
),
|
),
|
||||||
p([attribute.id("player-wrapper-track-name")], [text(model.track.title)]),
|
case model.view_mode {
|
||||||
p([attribute.id("player-wrapper-artist-name")], [text(model.artist.name)]),
|
model.Small ->
|
||||||
p([attribute.id("player-wrapper-album-name")], [text(model.album.name)]),
|
small.view(model)
|
||||||
div([attribute.id("player-controls")], [
|
|> element.map(ActionTriggered)
|
||||||
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)],
|
|
||||||
),
|
|
||||||
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", event_track_change),
|
|
||||||
event.on_mouse_down(StartUserSkip),
|
|
||||||
event.on_mouse_up(EndUserSkip),
|
|
||||||
event.on_input(event_track_input),
|
|
||||||
]),
|
|
||||||
audio(
|
|
||||||
[
|
|
||||||
attribute.id("player-elem"),
|
|
||||||
attribute.src(src),
|
|
||||||
attribute.controls(False),
|
|
||||||
attribute.autoplay(True),
|
|
||||||
attribute.attribute("crossorigin", "anonymous"),
|
|
||||||
event.on("play", event_play),
|
|
||||||
event.on("pause", event_pause),
|
|
||||||
event.on("ended", event_ended),
|
|
||||||
event.on("timeupdate", event_timeupdate),
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
])
|
])
|
||||||
|
//FullScreen -> full_screen.view()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn form_url(
|
fn form_url(
|
||||||
|
@ -350,29 +293,21 @@ fn form_url(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_play(_: a) {
|
fn event_play(_: a) {
|
||||||
Ok(SetPlayState(Playing))
|
Ok(PlayStateUpdated(Playing))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_pause(_: a) {
|
fn event_pause(_: a) {
|
||||||
Ok(SetPlayState(Paused))
|
Ok(PlayStateUpdated(Paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_ended(_: a) {
|
fn event_ended(_: a) {
|
||||||
Ok(NextTrack)
|
Ok(ActionTriggered(actions.NextTrack))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_timeupdate(_: a) {
|
fn event_timeupdate(_: a) {
|
||||||
Ok(UpdateTime(current_time()))
|
Ok(UpdateTime(current_time()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_track_change(_: a) {
|
|
||||||
Ok(PositionSelected(current_track_position()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_track_input(_: a) {
|
|
||||||
PositionChanged(current_track_position())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_play(
|
fn start_play(
|
||||||
settings: common.Settings,
|
settings: common.Settings,
|
||||||
artist: Artist,
|
artist: Artist,
|
||||||
|
|
15
src/elekf/web/components/player/actions.gleam
Normal file
15
src/elekf/web/components/player/actions.gleam
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
pub type PositionSelection {
|
||||||
|
Ephemeral
|
||||||
|
Commit
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Action {
|
||||||
|
NextTrack
|
||||||
|
PrevTrack
|
||||||
|
Play
|
||||||
|
Pause
|
||||||
|
Clear
|
||||||
|
StartUserSkip
|
||||||
|
EndUserSkip
|
||||||
|
SelectPosition(type_: PositionSelection)
|
||||||
|
}
|
3
src/elekf/web/components/player/full_screen.gleam
Normal file
3
src/elekf/web/components/player/full_screen.gleam
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub fn view() {
|
||||||
|
todo
|
||||||
|
}
|
45
src/elekf/web/components/player/model.gleam
Normal file
45
src/elekf/web/components/player/model.gleam
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import gleam/uri
|
||||||
|
import gleam/option
|
||||||
|
import ibroadcast/authed_request.{type RequestConfig}
|
||||||
|
import elekf/web/common
|
||||||
|
import elekf/library.{type Library}
|
||||||
|
import elekf/library/track.{type Track}
|
||||||
|
import elekf/library/artist.{type Artist}
|
||||||
|
import elekf/library/album.{type Album}
|
||||||
|
|
||||||
|
pub type AudioContext
|
||||||
|
|
||||||
|
pub type MediaElementAudioSourceNode
|
||||||
|
|
||||||
|
pub type GainNode
|
||||||
|
|
||||||
|
pub type PlayState {
|
||||||
|
Playing
|
||||||
|
Paused
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ViewMode {
|
||||||
|
Small
|
||||||
|
FullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Model {
|
||||||
|
Model(
|
||||||
|
settings: common.Settings,
|
||||||
|
library: Library,
|
||||||
|
track_id: Int,
|
||||||
|
track: Track,
|
||||||
|
artist: Artist,
|
||||||
|
album: Album,
|
||||||
|
url: uri.Uri,
|
||||||
|
position: Int,
|
||||||
|
state: PlayState,
|
||||||
|
loading_stream: Bool,
|
||||||
|
request_config: RequestConfig,
|
||||||
|
user_is_skipping: Bool,
|
||||||
|
audio_context: AudioContext,
|
||||||
|
audio_source: option.Option(MediaElementAudioSourceNode),
|
||||||
|
gain_node: GainNode,
|
||||||
|
view_mode: ViewMode,
|
||||||
|
)
|
||||||
|
}
|
93
src/elekf/web/components/player/small.gleam
Normal file
93
src/elekf/web/components/player/small.gleam
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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("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)],
|
||||||
|
),
|
||||||
|
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) }),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
|
@ -8,20 +8,28 @@ import lustre/attribute
|
||||||
import ibroadcast/artwork
|
import ibroadcast/artwork
|
||||||
import elekf/web/common.{type Settings}
|
import elekf/web/common.{type Settings}
|
||||||
|
|
||||||
pub fn view(s: Settings, id: String, class: String) {
|
pub fn view(s: Settings, image_id: String, html_id: String, class: String) {
|
||||||
div([attribute.class("thumbnail " <> class)], [
|
div(
|
||||||
img([
|
[
|
||||||
attribute.alt(""),
|
attribute.id(html_id <> "-wrapper"),
|
||||||
attribute.src(artwork.url(s.artwork_server, id, artwork.S300)),
|
attribute.class("thumbnail " <> class),
|
||||||
attribute.attribute("loading", "lazy"),
|
],
|
||||||
attribute.width(300),
|
[
|
||||||
]),
|
img([
|
||||||
])
|
attribute.id(html_id),
|
||||||
|
attribute.alt(""),
|
||||||
|
attribute.src(artwork.url(s.artwork_server, image_id, artwork.S300)),
|
||||||
|
attribute.attribute("loading", "lazy"),
|
||||||
|
attribute.width(300),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maybe_item_thumbnail(settings, artwork, placeholder) {
|
pub fn maybe_item_thumbnail(settings, id, artwork, placeholder) {
|
||||||
maybe_view(
|
maybe_view(
|
||||||
settings,
|
settings,
|
||||||
|
id,
|
||||||
"library-item-thumbnail",
|
"library-item-thumbnail",
|
||||||
"library-item-thumbnail library-item-thumbnail-placeholder",
|
"library-item-thumbnail library-item-thumbnail-placeholder",
|
||||||
artwork,
|
artwork,
|
||||||
|
@ -31,13 +39,15 @@ pub fn maybe_item_thumbnail(settings, artwork, placeholder) {
|
||||||
|
|
||||||
pub fn maybe_view(
|
pub fn maybe_view(
|
||||||
settings: option.Option(Settings),
|
settings: option.Option(Settings),
|
||||||
|
id: String,
|
||||||
class: String,
|
class: String,
|
||||||
placeholder_class: String,
|
placeholder_class: String,
|
||||||
artwork: option.Option(Int),
|
artwork: option.Option(Int),
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
) {
|
) {
|
||||||
case settings, artwork {
|
case settings, artwork {
|
||||||
option.Some(s), option.Some(id) -> view(s, int.to_string(id), class)
|
option.Some(s), option.Some(image_id) ->
|
||||||
|
view(s, int.to_string(image_id), id, class)
|
||||||
_, _ ->
|
_, _ ->
|
||||||
div([attribute.class("thumbnail-placeholder " <> placeholder_class)], [
|
div([attribute.class("thumbnail-placeholder " <> placeholder_class)], [
|
||||||
text(placeholder),
|
text(placeholder),
|
||||||
|
|
16
style.css
16
style.css
|
@ -182,7 +182,7 @@ single-album-view {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper {
|
#small-player-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr;
|
grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr;
|
||||||
gap: var(--side-margin);
|
gap: var(--side-margin);
|
||||||
|
@ -190,29 +190,29 @@ single-album-view {
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper-track-name,
|
#small-player-track-name,
|
||||||
#player-wrapper-artist-name,
|
#small-player-artist-name,
|
||||||
#player-wrapper-album-name {
|
#small-player-album-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper-track-name {
|
#small-player-track-name {
|
||||||
grid-area: track;
|
grid-area: track;
|
||||||
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper-artist-name {
|
#small-player-artist-name {
|
||||||
grid-area: artist;
|
grid-area: artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper-album-name {
|
#small-player-album-name {
|
||||||
grid-area: album;
|
grid-area: album;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper .thumbnail {
|
#small-player-artwork-wrapper {
|
||||||
grid-area: art;
|
grid-area: art;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue