UI improvements for player

This commit is contained in:
Mikko Ahlroth 2024-02-03 01:14:40 +02:00
parent 06c4410d09
commit 4e6cc39a27
11 changed files with 195 additions and 107 deletions

View file

@ -7,6 +7,6 @@ pub type Artist {
tracks: List(Int),
trashed: Bool,
rating: Int,
artwork_id: option.Option(String),
artwork_id: option.Option(Int),
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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