From 4e6cc39a2799eeb19b8ef33b3282f6ed67713e1a Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sat, 3 Feb 2024 01:14:40 +0200 Subject: [PATCH] UI improvements for player --- src/elekf/library/artist.gleam | 2 +- src/elekf/library/track.gleam | 4 +- src/elekf/transfer/artist.gleam | 6 +- src/elekf/transfer/track.gleam | 8 +- .../components/library_views/album_item.gleam | 20 ++--- .../library_views/artists_view.gleam | 46 +++++----- .../components/library_views/thumbnail.gleam | 20 ----- src/elekf/web/components/player.gleam | 84 ++++++++++++------- src/elekf/web/components/thumbnail.gleam | 46 ++++++++++ src/elekf/web/components/track_length.gleam | 2 +- style.css | 64 +++++++++++--- 11 files changed, 195 insertions(+), 107 deletions(-) delete mode 100644 src/elekf/web/components/library_views/thumbnail.gleam create mode 100644 src/elekf/web/components/thumbnail.gleam diff --git a/src/elekf/library/artist.gleam b/src/elekf/library/artist.gleam index e8a7215..2d730ee 100644 --- a/src/elekf/library/artist.gleam +++ b/src/elekf/library/artist.gleam @@ -7,6 +7,6 @@ pub type Artist { tracks: List(Int), trashed: Bool, rating: Int, - artwork_id: option.Option(String), + artwork_id: option.Option(Int), ) } diff --git a/src/elekf/library/track.gleam b/src/elekf/library/track.gleam index 47362ad..02d90e5 100644 --- a/src/elekf/library/track.gleam +++ b/src/elekf/library/track.gleam @@ -1,3 +1,5 @@ +import gleam/option + /// A track in the music library. /// /// The `title_lower` contains the track title in lowercase, which is an @@ -11,7 +13,7 @@ pub type Track { genre: String, length: Int, album_id: Int, - artwork_id: Int, + artwork_id: option.Option(Int), artist_id: Int, enid: Int, uploaded_on: String, diff --git a/src/elekf/transfer/artist.gleam b/src/elekf/transfer/artist.gleam index e48b8d6..aaa69d9 100644 --- a/src/elekf/transfer/artist.gleam +++ b/src/elekf/transfer/artist.gleam @@ -1,4 +1,7 @@ import gleam/string +import gleam/option +import gleam/result +import gleam/int import ibroadcast/library/library.{type Artist as APIArtist} import elekf/library/artist.{Artist} @@ -10,6 +13,7 @@ pub fn from(artist: APIArtist) { tracks: artist.tracks, trashed: artist.trashed, rating: artist.rating, - artwork_id: artist.artwork_id, + artwork_id: artist.artwork_id + |> option.map(fn(id) { result.unwrap(int.parse(id), 0) }), ) } diff --git a/src/elekf/transfer/track.gleam b/src/elekf/transfer/track.gleam index 4d55081..d2947da 100644 --- a/src/elekf/transfer/track.gleam +++ b/src/elekf/transfer/track.gleam @@ -1,9 +1,15 @@ import gleam/string +import gleam/option import ibroadcast/library/library.{type Track as APITrack} import elekf/library/track.{Track} /// Converts API track response to library format. pub fn from(track: APITrack) { + let artwork_id = case track.artwork_id { + 0 -> option.None + id -> option.Some(id) + } + Track( number: track.number, year: track.year, @@ -12,7 +18,7 @@ pub fn from(track: APITrack) { genre: track.genre, length: track.length, album_id: track.album_id, - artwork_id: track.artwork_id, + artwork_id: artwork_id, artist_id: track.artist_id, enid: track.enid, uploaded_on: track.uploaded_on, diff --git a/src/elekf/web/components/library_views/album_item.gleam b/src/elekf/web/components/library_views/album_item.gleam index f107abc..3452cd6 100644 --- a/src/elekf/web/components/library_views/album_item.gleam +++ b/src/elekf/web/components/library_views/album_item.gleam @@ -15,7 +15,7 @@ import elekf/library/track_utils import elekf/web/components/library_view.{AlbumExpandToggled, ShuffleAll} import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/library_views/track_item -import elekf/web/components/library_views/thumbnail +import elekf/web/components/thumbnail import elekf/web/components/shuffle_all import elekf/web/common.{type Settings} @@ -64,19 +64,11 @@ pub fn view( attribute.attribute("tabindex", "0"), ], [ - case settings, { first_track.1 }.artwork_id { - option.Some(s), id if id != 0 -> - thumbnail.view(s, int.to_string(id)) - _, _ -> - div( - [ - attribute.class( - "artist-image-placeholder album-item-image", - ), - ], - [text(album.name)], - ) - }, + thumbnail.maybe_item_thumbnail( + settings, + { first_track.1 }.artwork_id, + album.name, + ), h3([attribute.class("album-item-title")], [text(album.name)]), p([attribute.class("album-item-artist")], [text(artist_name)]), p([attribute.class("album-item-tracks-meta")], [ diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index 485ccd1..103dfc6 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -3,7 +3,7 @@ import gleam/string import gleam/int import gleam/list -import gleam/map +import gleam/dict import gleam/option import lustre/element/html.{div, h3, p} import lustre/element.{text} @@ -14,7 +14,7 @@ import elekf/library/artist.{type Artist} import elekf/library/artist_utils import elekf/web/components/library_view.{type Model, ShowArtist} import elekf/web/components/library_item.{type LibraryItem} -import elekf/web/components/library_views/thumbnail +import elekf/web/components/thumbnail import elekf/web/common const component_name = "artists-view" @@ -42,15 +42,12 @@ pub fn render( fn data_getter(library: Library) { library.artists - |> map.fold( - [], - fn(acc, key, val) { - case val.tracks { - [] -> acc - _ -> [#(key, val), ..acc] - } - }, - ) + |> dict.fold([], fn(acc, key, val) { + case val.tracks { + [] -> acc + _ -> [#(key, val), ..acc] + } + }) } fn shuffler(library, items: List(LibraryItem(Artist))) { @@ -81,27 +78,22 @@ fn item_view( attribute.attribute("role", "button"), ], [ - case model.settings, artist.artwork_id { - option.Some(s), option.Some(id) -> thumbnail.view(s, id) - _, _ -> - div( - [attribute.class("artist-image-placeholder")], - [text(artist.name)], - ) - }, - h3([attribute.class("artist-title")], [text(artist.name)]), - p( - [attribute.class("artist-tracks")], - [text(int.to_string(list.length(artist.tracks)) <> " tracks")], + thumbnail.maybe_item_thumbnail( + model.settings, + artist.artwork_id, + artist.name, ), + h3([attribute.class("artist-title")], [text(artist.name)]), + p([attribute.class("artist-tracks")], [ + text(int.to_string(list.length(artist.tracks)) <> " tracks"), + ]), ], ), ] } fn get_tracks(library: Library, artist: Artist) { - list.map( - artist.tracks, - fn(track_id) { #(track_id, library.assert_track(library, track_id)) }, - ) + list.map(artist.tracks, fn(track_id) { + #(track_id, library.assert_track(library, track_id)) + }) } diff --git a/src/elekf/web/components/library_views/thumbnail.gleam b/src/elekf/web/components/library_views/thumbnail.gleam deleted file mode 100644 index 734e348..0000000 --- a/src/elekf/web/components/library_views/thumbnail.gleam +++ /dev/null @@ -1,20 +0,0 @@ -//// A thumbnail element (artist, album...) - -import lustre/element/html.{div, img} -import lustre/attribute -import ibroadcast/artwork -import elekf/web/common.{type Settings} - -pub fn view(s: Settings, id: String) { - div( - [attribute.class("library-item-thumbnail")], - [ - img([ - attribute.alt(""), - attribute.src(artwork.url(s.artwork_server, id, artwork.S300)), - attribute.attribute("loading", "lazy"), - attribute.width(300), - ]), - ], - ) -} diff --git a/src/elekf/web/components/player.gleam b/src/elekf/web/components/player.gleam index daba101..f219ac7 100644 --- a/src/elekf/web/components/player.gleam +++ b/src/elekf/web/components/player.gleam @@ -20,6 +20,7 @@ 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/library.{type Library} import elekf/library/track.{type Track} import elekf/library/artist.{type Artist} @@ -218,22 +219,18 @@ pub fn view(model: Model) { } div([attribute.id("player-wrapper")], [ - p([attribute.id("player-wrapper-track-title")], [text(model.track.title)]), - audio( - [ - attribute.id("player-elem"), - attribute.src(src), - attribute.controls(False), - attribute.autoplay(True), - event.on("play", event_play), - event.on("pause", event_pause), - event.on("ended", event_ended), - event.on("timeupdate", event_timeupdate), - ], - [], + thumbnail.maybe_view( + option.Some(model.settings), + "player-wrapper-thumbnail", + "player-wrapper-thumbnail player-wrapper-thumbnail-placeholder", + model.track.artwork_id, + model.album.name, ), + 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("", [], [ + button_group.view("", [attribute.id("player-controls-buttons")], [ button.view( "button-small", [attribute.id("player-previous"), event.on_click(PrevTrack)], @@ -259,6 +256,22 @@ pub fn view(model: Model) { [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"), @@ -270,11 +283,19 @@ pub fn view(model: Model) { event.on_mouse_up(EndUserSkip), event.on_input(event_track_input), ]), - p([attribute.id("player-times")], [ - track_length(model.position, current_time_padding), - text(" / "), - track_length(model.track.length, track_length.Auto), - ]), + audio( + [ + attribute.id("player-elem"), + attribute.src(src), + attribute.controls(False), + attribute.autoplay(True), + event.on("play", event_play), + event.on("pause", event_pause), + event.on("ended", event_ended), + event.on("timeupdate", event_timeupdate), + ], + [], + ), ]), ]) } @@ -330,17 +351,22 @@ fn start_play( |> metadata.set_title(track.title) |> metadata.set_artist(artist.name) |> metadata.set_album(album.name) - |> metadata.set_artwork([ - metadata.MetadataArtwork( - src: artwork.url( - settings.artwork_server, - int.to_string(track.artwork_id), - artwork.S300, + + let metadata = case track.artwork_id { + option.Some(id) -> + metadata.set_artwork(metadata, [ + metadata.MetadataArtwork( + src: artwork.url( + settings.artwork_server, + int.to_string(id), + artwork.S300, + ), + sizes: "300x300", + type_: "", ), - sizes: "300x300", - type_: "", - ), - ]) + ]) + option.None -> metadata + } session.set_metadata(metadata) } diff --git a/src/elekf/web/components/thumbnail.gleam b/src/elekf/web/components/thumbnail.gleam new file mode 100644 index 0000000..7438a1d --- /dev/null +++ b/src/elekf/web/components/thumbnail.gleam @@ -0,0 +1,46 @@ +//// A thumbnail element (artist, album...) + +import gleam/option +import gleam/int +import lustre/element/html.{div, img} +import lustre/element.{text} +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 maybe_item_thumbnail(settings, artwork, placeholder) { + maybe_view( + settings, + "library-item-thumbnail", + "library-item-thumbnail library-item-thumbnail-placeholder", + artwork, + placeholder, + ) +} + +pub fn maybe_view( + settings: option.Option(Settings), + 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) + _, _ -> + div([attribute.class("thumbnail-placeholder " <> placeholder_class)], [ + text(placeholder), + ]) + } +} diff --git a/src/elekf/web/components/track_length.gleam b/src/elekf/web/components/track_length.gleam index b5a0458..5ab7802 100644 --- a/src/elekf/web/components/track_length.gleam +++ b/src/elekf/web/components/track_length.gleam @@ -19,7 +19,7 @@ fn humanize_length(length: Int, padding: PadTo) -> String { case length { l if l < 60 -> { case padding { - Auto -> "0:" <> pad_00(length) + Auto -> "00:" <> pad_00(length) Hours -> "0:00:" <> pad_00(length) } } diff --git a/style.css b/style.css index 6fa7284..400625d 100644 --- a/style.css +++ b/style.css @@ -174,28 +174,68 @@ single-artist-view { } #player-wrapper { - display: flex; - flex-direction: column; - align-items: stretch; - gap: 5px; + display: grid; + grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr; + gap: var(--side-margin); + + font-weight: 100; } -#player-wrapper-track-title { - font-weight: 100; +#player-wrapper-track-name, +#player-wrapper-artist-name, +#player-wrapper-album-name { white-space: nowrap; text-overflow: ellipsis; overflow-x: hidden; } +#player-wrapper-track-name { + grid-area: track; + + font-weight: normal; +} + +#player-wrapper-artist-name { + grid-area: artist; +} + +#player-wrapper-album-name { + grid-area: album; +} + +#player-wrapper .thumbnail { + grid-area: art; +} + #player-controls { - display: flex; - flex-direction: row; - justify-content: space-between; - gap: 5px; + grid-area: controls; + + display: grid; + grid-template: "elapsed track total" "buttons buttons buttons" / auto 1fr auto; + gap: var(--side-margin); +} + +#player-controls-buttons { + grid-area: buttons; + + justify-content: center; + font-size: 1.5rem; +} + +#player-controls-buttons button { + padding: calc(var(--side-margin) * 2) calc(var(--side-margin) * 3); +} + +#player-time-elapsed { + grid-area: elapsed; +} + +#player-time-total { + grid-area: total; } #player-track { - flex: 1 1; + grid-area: track; } #player-track::-webkit-slider-thumb { @@ -291,7 +331,7 @@ single-artist-view { } #authed-view-wrapper[data-player-status="open"] .library-list { - padding-bottom: 80px; + padding-bottom: 100vh; } #artists-view .library-list,