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),
|
tracks: List(Int),
|
||||||
trashed: Bool,
|
trashed: Bool,
|
||||||
rating: Int,
|
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.
|
/// A track in the music library.
|
||||||
///
|
///
|
||||||
/// The `title_lower` contains the track title in lowercase, which is an
|
/// The `title_lower` contains the track title in lowercase, which is an
|
||||||
|
@ -11,7 +13,7 @@ pub type Track {
|
||||||
genre: String,
|
genre: String,
|
||||||
length: Int,
|
length: Int,
|
||||||
album_id: Int,
|
album_id: Int,
|
||||||
artwork_id: Int,
|
artwork_id: option.Option(Int),
|
||||||
artist_id: Int,
|
artist_id: Int,
|
||||||
enid: Int,
|
enid: Int,
|
||||||
uploaded_on: String,
|
uploaded_on: String,
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import gleam/string
|
import gleam/string
|
||||||
|
import gleam/option
|
||||||
|
import gleam/result
|
||||||
|
import gleam/int
|
||||||
import ibroadcast/library/library.{type Artist as APIArtist}
|
import ibroadcast/library/library.{type Artist as APIArtist}
|
||||||
import elekf/library/artist.{Artist}
|
import elekf/library/artist.{Artist}
|
||||||
|
|
||||||
|
@ -10,6 +13,7 @@ pub fn from(artist: APIArtist) {
|
||||||
tracks: artist.tracks,
|
tracks: artist.tracks,
|
||||||
trashed: artist.trashed,
|
trashed: artist.trashed,
|
||||||
rating: artist.rating,
|
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/string
|
||||||
|
import gleam/option
|
||||||
import ibroadcast/library/library.{type Track as APITrack}
|
import ibroadcast/library/library.{type Track as APITrack}
|
||||||
import elekf/library/track.{Track}
|
import elekf/library/track.{Track}
|
||||||
|
|
||||||
/// Converts API track response to library format.
|
/// Converts API track response to library format.
|
||||||
pub fn from(track: APITrack) {
|
pub fn from(track: APITrack) {
|
||||||
|
let artwork_id = case track.artwork_id {
|
||||||
|
0 -> option.None
|
||||||
|
id -> option.Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
Track(
|
Track(
|
||||||
number: track.number,
|
number: track.number,
|
||||||
year: track.year,
|
year: track.year,
|
||||||
|
@ -12,7 +18,7 @@ pub fn from(track: APITrack) {
|
||||||
genre: track.genre,
|
genre: track.genre,
|
||||||
length: track.length,
|
length: track.length,
|
||||||
album_id: track.album_id,
|
album_id: track.album_id,
|
||||||
artwork_id: track.artwork_id,
|
artwork_id: artwork_id,
|
||||||
artist_id: track.artist_id,
|
artist_id: track.artist_id,
|
||||||
enid: track.enid,
|
enid: track.enid,
|
||||||
uploaded_on: track.uploaded_on,
|
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_view.{AlbumExpandToggled, ShuffleAll}
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
import elekf/web/components/library_item.{type LibraryItem}
|
||||||
import elekf/web/components/library_views/track_item
|
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/components/shuffle_all
|
||||||
import elekf/web/common.{type Settings}
|
import elekf/web/common.{type Settings}
|
||||||
|
|
||||||
|
@ -64,19 +64,11 @@ pub fn view(
|
||||||
attribute.attribute("tabindex", "0"),
|
attribute.attribute("tabindex", "0"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
case settings, { first_track.1 }.artwork_id {
|
thumbnail.maybe_item_thumbnail(
|
||||||
option.Some(s), id if id != 0 ->
|
settings,
|
||||||
thumbnail.view(s, int.to_string(id))
|
{ first_track.1 }.artwork_id,
|
||||||
_, _ ->
|
album.name,
|
||||||
div(
|
),
|
||||||
[
|
|
||||||
attribute.class(
|
|
||||||
"artist-image-placeholder album-item-image",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[text(album.name)],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
h3([attribute.class("album-item-title")], [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-artist")], [text(artist_name)]),
|
||||||
p([attribute.class("album-item-tracks-meta")], [
|
p([attribute.class("album-item-tracks-meta")], [
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/map
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import lustre/element/html.{div, h3, p}
|
import lustre/element/html.{div, h3, p}
|
||||||
import lustre/element.{text}
|
import lustre/element.{text}
|
||||||
|
@ -14,7 +14,7 @@ import elekf/library/artist.{type Artist}
|
||||||
import elekf/library/artist_utils
|
import elekf/library/artist_utils
|
||||||
import elekf/web/components/library_view.{type Model, ShowArtist}
|
import elekf/web/components/library_view.{type Model, ShowArtist}
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
import elekf/web/components/library_item.{type LibraryItem}
|
||||||
import elekf/web/components/library_views/thumbnail
|
import elekf/web/components/thumbnail
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
|
|
||||||
const component_name = "artists-view"
|
const component_name = "artists-view"
|
||||||
|
@ -42,15 +42,12 @@ pub fn render(
|
||||||
|
|
||||||
fn data_getter(library: Library) {
|
fn data_getter(library: Library) {
|
||||||
library.artists
|
library.artists
|
||||||
|> map.fold(
|
|> dict.fold([], fn(acc, key, val) {
|
||||||
[],
|
case val.tracks {
|
||||||
fn(acc, key, val) {
|
[] -> acc
|
||||||
case val.tracks {
|
_ -> [#(key, val), ..acc]
|
||||||
[] -> acc
|
}
|
||||||
_ -> [#(key, val), ..acc]
|
})
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shuffler(library, items: List(LibraryItem(Artist))) {
|
fn shuffler(library, items: List(LibraryItem(Artist))) {
|
||||||
|
@ -81,27 +78,22 @@ fn item_view(
|
||||||
attribute.attribute("role", "button"),
|
attribute.attribute("role", "button"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
case model.settings, artist.artwork_id {
|
thumbnail.maybe_item_thumbnail(
|
||||||
option.Some(s), option.Some(id) -> thumbnail.view(s, id)
|
model.settings,
|
||||||
_, _ ->
|
artist.artwork_id,
|
||||||
div(
|
artist.name,
|
||||||
[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")],
|
|
||||||
),
|
),
|
||||||
|
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) {
|
fn get_tracks(library: Library, artist: Artist) {
|
||||||
list.map(
|
list.map(artist.tracks, fn(track_id) {
|
||||||
artist.tracks,
|
#(track_id, library.assert_track(library, track_id))
|
||||||
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/track_length.{track_length}
|
||||||
import elekf/web/components/button_group
|
import elekf/web/components/button_group
|
||||||
import elekf/web/components/button
|
import elekf/web/components/button
|
||||||
|
import elekf/web/components/thumbnail
|
||||||
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}
|
||||||
|
@ -218,22 +219,18 @@ pub fn view(model: Model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
div([attribute.id("player-wrapper")], [
|
div([attribute.id("player-wrapper")], [
|
||||||
p([attribute.id("player-wrapper-track-title")], [text(model.track.title)]),
|
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),
|
|
||||||
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")], [
|
div([attribute.id("player-controls")], [
|
||||||
button_group.view("", [], [
|
button_group.view("", [attribute.id("player-controls-buttons")], [
|
||||||
button.view(
|
button.view(
|
||||||
"button-small",
|
"button-small",
|
||||||
[attribute.id("player-previous"), event.on_click(PrevTrack)],
|
[attribute.id("player-previous"), event.on_click(PrevTrack)],
|
||||||
|
@ -259,6 +256,22 @@ pub fn view(model: Model) {
|
||||||
[icon("skip-forward-fill", Alt("Next"))],
|
[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([
|
input([
|
||||||
attribute.id("player-track"),
|
attribute.id("player-track"),
|
||||||
attribute.type_("range"),
|
attribute.type_("range"),
|
||||||
|
@ -270,11 +283,19 @@ pub fn view(model: Model) {
|
||||||
event.on_mouse_up(EndUserSkip),
|
event.on_mouse_up(EndUserSkip),
|
||||||
event.on_input(event_track_input),
|
event.on_input(event_track_input),
|
||||||
]),
|
]),
|
||||||
p([attribute.id("player-times")], [
|
audio(
|
||||||
track_length(model.position, current_time_padding),
|
[
|
||||||
text(" / "),
|
attribute.id("player-elem"),
|
||||||
track_length(model.track.length, track_length.Auto),
|
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_title(track.title)
|
||||||
|> metadata.set_artist(artist.name)
|
|> metadata.set_artist(artist.name)
|
||||||
|> metadata.set_album(album.name)
|
|> metadata.set_album(album.name)
|
||||||
|> metadata.set_artwork([
|
|
||||||
metadata.MetadataArtwork(
|
let metadata = case track.artwork_id {
|
||||||
src: artwork.url(
|
option.Some(id) ->
|
||||||
settings.artwork_server,
|
metadata.set_artwork(metadata, [
|
||||||
int.to_string(track.artwork_id),
|
metadata.MetadataArtwork(
|
||||||
artwork.S300,
|
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)
|
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 {
|
case length {
|
||||||
l if l < 60 -> {
|
l if l < 60 -> {
|
||||||
case padding {
|
case padding {
|
||||||
Auto -> "0:" <> pad_00(length)
|
Auto -> "00:" <> pad_00(length)
|
||||||
Hours -> "0: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 {
|
#player-wrapper {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template: "art track" "art artist" "art album" "controls controls" auto / 2fr 8fr;
|
||||||
align-items: stretch;
|
gap: var(--side-margin);
|
||||||
gap: 5px;
|
|
||||||
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-wrapper-track-title {
|
#player-wrapper-track-name,
|
||||||
font-weight: 100;
|
#player-wrapper-artist-name,
|
||||||
|
#player-wrapper-album-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-x: hidden;
|
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 {
|
#player-controls {
|
||||||
display: flex;
|
grid-area: controls;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
display: grid;
|
||||||
gap: 5px;
|
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 {
|
#player-track {
|
||||||
flex: 1 1;
|
grid-area: track;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-track::-webkit-slider-thumb {
|
#player-track::-webkit-slider-thumb {
|
||||||
|
@ -291,7 +331,7 @@ single-artist-view {
|
||||||
}
|
}
|
||||||
|
|
||||||
#authed-view-wrapper[data-player-status="open"] .library-list {
|
#authed-view-wrapper[data-player-status="open"] .library-list {
|
||||||
padding-bottom: 80px;
|
padding-bottom: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#artists-view .library-list,
|
#artists-view .library-list,
|
||||||
|
|
Loading…
Reference in a new issue