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/web/router
|
||||
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_views/tracks_view
|
||||
import elekf/web/components/library_views/artists_view
|
||||
|
@ -52,7 +54,7 @@ pub type PlayInfo {
|
|||
track: Track,
|
||||
play_queue: PlayQueue,
|
||||
play_index: Int,
|
||||
player: player.Model,
|
||||
player: player_model.Model,
|
||||
current_track_status: CurrentTrackStatus,
|
||||
)
|
||||
}
|
||||
|
@ -156,10 +158,12 @@ pub fn update(model: Model, msg) {
|
|||
let #(status, effect) = handle_start_play(model, tracks, position)
|
||||
#(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) {
|
||||
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
|
||||
}
|
||||
case list.at(info.play_queue, next_index) {
|
||||
|
|
|
@ -21,6 +21,7 @@ pub fn view(
|
|||
item: LibraryItem(Album),
|
||||
) {
|
||||
let #(album_id, album) = item
|
||||
let album_id_str = int.to_string(album_id)
|
||||
let tracks = album_utils.get_tracks(library, album)
|
||||
let artist_name = case album.artist_id {
|
||||
0 -> "Unknown artist"
|
||||
|
@ -32,10 +33,11 @@ pub fn view(
|
|||
link.link(
|
||||
router.to_hash(router.Album(album_id)),
|
||||
"library-item album-item",
|
||||
[attribute.id("album-list-" <> int.to_string(album_id))],
|
||||
[attribute.id("album-list-" <> album_id_str)],
|
||||
[
|
||||
thumbnail.maybe_item_thumbnail(
|
||||
settings,
|
||||
"album-item-thumbnail-" <> album_id_str,
|
||||
{ first_track.1 }.artwork_id,
|
||||
album.name,
|
||||
),
|
||||
|
|
|
@ -69,14 +69,16 @@ fn item_view(
|
|||
item: LibraryItem(Artist),
|
||||
) {
|
||||
let #(artist_id, artist) = item
|
||||
let artist_id_str = int.to_string(artist_id)
|
||||
[
|
||||
link.link(
|
||||
router.to_hash(router.Artist(artist_id)),
|
||||
"library-item artist-item",
|
||||
[attribute.id("artist-list-" <> int.to_string(artist_id))],
|
||||
[attribute.id("artist-list-" <> artist_id_str)],
|
||||
[
|
||||
thumbnail.maybe_item_thumbnail(
|
||||
model.settings,
|
||||
"artist-list-thumbnail-" <> artist_id_str,
|
||||
artist.artwork_id,
|
||||
artist.name,
|
||||
),
|
||||
|
|
|
@ -5,8 +5,8 @@ import gleam/int
|
|||
import gleam/float
|
||||
import gleam/option
|
||||
import gleam/io
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{audio, div, input, p}
|
||||
import lustre/element
|
||||
import lustre/element/html.{audio, div}
|
||||
import lustre/attribute
|
||||
import lustre/event
|
||||
import lustre/effect
|
||||
|
@ -14,74 +14,38 @@ import plinth/browser/media/metadata
|
|||
import plinth/browser/media/session
|
||||
import plinth/browser/media/action
|
||||
import plinth/browser/media/position
|
||||
import ibroadcast/authed_request.{type RequestConfig}
|
||||
import ibroadcast/streaming
|
||||
import ibroadcast/artwork
|
||||
import elektrofoni
|
||||
import elekf/utils/lustre
|
||||
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/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/track.{type Track}
|
||||
import elekf/library/artist.{type Artist}
|
||||
import elekf/library/album.{type Album}
|
||||
import elekf/utils/date
|
||||
import ibroadcast/authed_request.{type RequestConfig}
|
||||
import ibroadcast/streaming
|
||||
import ibroadcast/artwork
|
||||
|
||||
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 {
|
||||
StartPlay(Int, Track)
|
||||
NextTrack
|
||||
PrevTrack
|
||||
Play
|
||||
Pause
|
||||
SetPlayState(PlayState)
|
||||
Clear
|
||||
PlayStateUpdated(PlayState)
|
||||
UpdateTime(Float)
|
||||
UpdateRequestConfig(RequestConfig)
|
||||
StartUserSkip
|
||||
EndUserSkip
|
||||
PositionSelected(Int)
|
||||
PositionChanged(Int)
|
||||
SeekBackward(Float)
|
||||
SeekForward(Float)
|
||||
BackwardSeekRequested(Float)
|
||||
ForwardSeekRequested(Float)
|
||||
CreateMediaElementAudioSourceNode
|
||||
FullScreenToggled
|
||||
ActionTriggered(actions.Action)
|
||||
PositionSelected(Int)
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
|
@ -96,17 +60,33 @@ pub fn init(
|
|||
|
||||
let action_effect =
|
||||
effect.from(fn(dispatch) {
|
||||
register_action_handler(action.NextTrack, NextTrack, dispatch)
|
||||
register_action_handler(action.PreviousTrack, PrevTrack, dispatch)
|
||||
register_action_handler(action.Play, Play, dispatch)
|
||||
register_action_handler(action.Pause, Pause, dispatch)
|
||||
register_action_handler(
|
||||
action.NextTrack,
|
||||
ActionTriggered(actions.NextTrack),
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
let seek_to = option.unwrap(data.seek_time, 0.0)
|
||||
|
@ -138,6 +118,7 @@ pub fn init(
|
|||
audio_context,
|
||||
option.None,
|
||||
create_gain(audio_context),
|
||||
model.Small,
|
||||
),
|
||||
action_effect,
|
||||
)
|
||||
|
@ -145,6 +126,39 @@ pub fn init(
|
|||
|
||||
pub fn update(model: Model, 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) -> {
|
||||
let artist = library.assert_artist(model.library, track.artist_id)
|
||||
let album = library.assert_album(model.library, track.album_id)
|
||||
|
@ -170,21 +184,10 @@ pub fn update(model: Model, msg) {
|
|||
effect.none(),
|
||||
)
|
||||
}
|
||||
Play -> {
|
||||
play()
|
||||
#(model, effect.none())
|
||||
}
|
||||
Pause -> {
|
||||
pause()
|
||||
#(model, effect.none())
|
||||
}
|
||||
SetPlayState(play_state) -> #(
|
||||
PlayStateUpdated(play_state) -> #(
|
||||
Model(..model, state: play_state),
|
||||
effect.none(),
|
||||
)
|
||||
NextTrack -> #(model, effect.none())
|
||||
PrevTrack -> #(model, effect.none())
|
||||
Clear -> #(model, effect.none())
|
||||
UpdateTime(time) ->
|
||||
case model.user_is_skipping {
|
||||
True -> #(model, effect.none())
|
||||
|
@ -214,123 +217,63 @@ pub fn update(model: Model, msg) {
|
|||
Model(..model, request_config: config),
|
||||
effect.none(),
|
||||
)
|
||||
StartUserSkip -> #(Model(..model, user_is_skipping: True), effect.none())
|
||||
EndUserSkip -> #(Model(..model, user_is_skipping: False), effect.none())
|
||||
BackwardSeekRequested(amount) -> {
|
||||
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) -> {
|
||||
skip_to_time(pos)
|
||||
|
||||
#(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 -> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: Model) {
|
||||
let is_playing = model.state == Playing
|
||||
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")], [
|
||||
thumbnail.maybe_view(
|
||||
option.Some(model.settings),
|
||||
"player-wrapper-thumbnail",
|
||||
"player-wrapper-thumbnail player-wrapper-thumbnail-placeholder",
|
||||
model.track.artwork_id,
|
||||
model.album.name,
|
||||
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),
|
||||
],
|
||||
[],
|
||||
),
|
||||
p([attribute.id("player-wrapper-track-name")], [text(model.track.title)]),
|
||||
p([attribute.id("player-wrapper-artist-name")], [text(model.artist.name)]),
|
||||
p([attribute.id("player-wrapper-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", 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),
|
||||
],
|
||||
[],
|
||||
),
|
||||
]),
|
||||
case model.view_mode {
|
||||
model.Small ->
|
||||
small.view(model)
|
||||
|> element.map(ActionTriggered)
|
||||
},
|
||||
])
|
||||
//FullScreen -> full_screen.view()
|
||||
}
|
||||
|
||||
fn form_url(
|
||||
|
@ -350,29 +293,21 @@ fn form_url(
|
|||
}
|
||||
|
||||
fn event_play(_: a) {
|
||||
Ok(SetPlayState(Playing))
|
||||
Ok(PlayStateUpdated(Playing))
|
||||
}
|
||||
|
||||
fn event_pause(_: a) {
|
||||
Ok(SetPlayState(Paused))
|
||||
Ok(PlayStateUpdated(Paused))
|
||||
}
|
||||
|
||||
fn event_ended(_: a) {
|
||||
Ok(NextTrack)
|
||||
Ok(ActionTriggered(actions.NextTrack))
|
||||
}
|
||||
|
||||
fn event_timeupdate(_: a) {
|
||||
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(
|
||||
settings: common.Settings,
|
||||
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 elekf/web/common.{type Settings}
|
||||
|
||||
pub fn view(s: Settings, id: String, class: String) {
|
||||
div([attribute.class("thumbnail " <> class)], [
|
||||
img([
|
||||
attribute.alt(""),
|
||||
attribute.src(artwork.url(s.artwork_server, id, artwork.S300)),
|
||||
attribute.attribute("loading", "lazy"),
|
||||
attribute.width(300),
|
||||
]),
|
||||
])
|
||||
pub fn view(s: Settings, image_id: String, html_id: String, class: String) {
|
||||
div(
|
||||
[
|
||||
attribute.id(html_id <> "-wrapper"),
|
||||
attribute.class("thumbnail " <> class),
|
||||
],
|
||||
[
|
||||
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(
|
||||
settings,
|
||||
id,
|
||||
"library-item-thumbnail",
|
||||
"library-item-thumbnail library-item-thumbnail-placeholder",
|
||||
artwork,
|
||||
|
@ -31,13 +39,15 @@ pub fn maybe_item_thumbnail(settings, artwork, placeholder) {
|
|||
|
||||
pub fn maybe_view(
|
||||
settings: option.Option(Settings),
|
||||
id: String,
|
||||
class: String,
|
||||
placeholder_class: String,
|
||||
artwork: option.Option(Int),
|
||||
placeholder: String,
|
||||
) {
|
||||
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)], [
|
||||
text(placeholder),
|
||||
|
|
16
style.css
16
style.css
|
@ -182,7 +182,7 @@ single-album-view {
|
|||
display: none;
|
||||
}
|
||||
|
||||
#player-wrapper {
|
||||
#small-player-wrapper {
|
||||
display: grid;
|
||||
grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr;
|
||||
gap: var(--side-margin);
|
||||
|
@ -190,29 +190,29 @@ single-album-view {
|
|||
font-weight: 100;
|
||||
}
|
||||
|
||||
#player-wrapper-track-name,
|
||||
#player-wrapper-artist-name,
|
||||
#player-wrapper-album-name {
|
||||
#small-player-track-name,
|
||||
#small-player-artist-name,
|
||||
#small-player-album-name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#player-wrapper-track-name {
|
||||
#small-player-track-name {
|
||||
grid-area: track;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#player-wrapper-artist-name {
|
||||
#small-player-artist-name {
|
||||
grid-area: artist;
|
||||
}
|
||||
|
||||
#player-wrapper-album-name {
|
||||
#small-player-album-name {
|
||||
grid-area: album;
|
||||
}
|
||||
|
||||
#player-wrapper .thumbnail {
|
||||
#small-player-artwork-wrapper {
|
||||
grid-area: art;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue