UI improvements for player
This commit is contained in:
parent
06c4410d09
commit
4e6cc39a27
11 changed files with 195 additions and 107 deletions
|
@ -7,6 +7,6 @@ pub type Artist {
|
|||
tracks: List(Int),
|
||||
trashed: Bool,
|
||||
rating: Int,
|
||||
artwork_id: option.Option(String),
|
||||
artwork_id: option.Option(Int),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
thumbnail.maybe_item_thumbnail(
|
||||
settings,
|
||||
{ first_track.1 }.artwork_id,
|
||||
album.name,
|
||||
),
|
||||
],
|
||||
[text(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")], [
|
||||
|
|
|
@ -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) {
|
||||
|> 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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
]),
|
||||
],
|
||||
)
|
||||
}
|
|
@ -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([
|
||||
|
||||
let metadata = case track.artwork_id {
|
||||
option.Some(id) ->
|
||||
metadata.set_artwork(metadata, [
|
||||
metadata.MetadataArtwork(
|
||||
src: artwork.url(
|
||||
settings.artwork_server,
|
||||
int.to_string(track.artwork_id),
|
||||
int.to_string(id),
|
||||
artwork.S300,
|
||||
),
|
||||
sizes: "300x300",
|
||||
type_: "",
|
||||
),
|
||||
])
|
||||
option.None -> metadata
|
||||
}
|
||||
|
||||
session.set_metadata(metadata)
|
||||
}
|
||||
|
|
46
src/elekf/web/components/thumbnail.gleam
Normal file
46
src/elekf/web/components/thumbnail.gleam
Normal 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),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
64
style.css
64
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,
|
||||
|
|
Loading…
Reference in a new issue