Add artist artwork

This commit is contained in:
Mikko Ahlroth 2023-10-23 20:23:10 +03:00
parent 2216d4b720
commit 960eaa73f7
9 changed files with 126 additions and 28 deletions

View file

@ -1,3 +1,5 @@
import gleam/option
pub type Artist { pub type Artist {
Artist( Artist(
name: String, name: String,
@ -5,5 +7,6 @@ pub type Artist {
tracks: List(Int), tracks: List(Int),
trashed: Bool, trashed: Bool,
rating: Int, rating: Int,
artwork_id: option.Option(String),
) )
} }

View file

@ -10,5 +10,6 @@ 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,
) )
} }

View file

@ -220,16 +220,19 @@ pub fn view(model: Model) {
library_view.Tracks -> library_view.Tracks ->
tracks_view.render( tracks_view.render(
model.library, model.library,
model.settings,
[attribute.id("tracks-view"), start_play.on(StartPlay)], [attribute.id("tracks-view"), start_play.on(StartPlay)],
) )
library_view.Artists -> library_view.Artists ->
artists_view.render( artists_view.render(
model.library, model.library,
model.settings,
[attribute.id("artists-view"), start_play.on(StartPlay)], [attribute.id("artists-view"), start_play.on(StartPlay)],
) )
library_view.Albums -> library_view.Albums ->
albums_view.render( albums_view.render(
model.library, model.library,
model.settings,
[attribute.id("albums-view"), start_play.on(StartPlay)], [attribute.id("albums-view"), start_play.on(StartPlay)],
) )
}, },

View file

@ -5,6 +5,7 @@
import gleam/dynamic import gleam/dynamic
import gleam/list import gleam/list
import gleam/map import gleam/map
import gleam/option
import lustre import lustre
import lustre/effect import lustre/effect
import lustre/element.{text} import lustre/element.{text}
@ -15,6 +16,7 @@ import elekf/library.{Library}
import elekf/library/track.{Track} import elekf/library/track.{Track}
import elekf/web/events/start_play import elekf/web/events/start_play
import elekf/web/components/search import elekf/web/components/search
import elekf/web/common
/// An item in the library with its ID. /// An item in the library with its ID.
pub type LibraryItem(a) = pub type LibraryItem(a) =
@ -26,7 +28,8 @@ pub type DataGetter(a) =
/// A view that renders a single item from the library. /// A view that renders a single item from the library.
pub type ItemView(a) = pub type ItemView(a) =
fn(Library, List(LibraryItem(a)), Int, LibraryItem(a)) -> element.Element(Msg) fn(Model(a), List(LibraryItem(a)), Int, LibraryItem(a)) ->
element.Element(Msg)
/// A filter that gets the item and the current search string and must return /// A filter that gets the item and the current search string and must return
/// `True` if the item should be shown and `False` if not. /// `True` if the item should be shown and `False` if not.
@ -57,6 +60,7 @@ pub type View {
pub type Msg { pub type Msg {
LibraryUpdated(Library) LibraryUpdated(Library)
SettingsUpdated(option.Option(common.Settings))
ShuffleAll ShuffleAll
StartPlay(List(LibraryItem(Track)), Int) StartPlay(List(LibraryItem(Track)), Int)
Search(search.Msg) Search(search.Msg)
@ -69,6 +73,7 @@ pub type Model(a) {
data_getter: DataGetter(a), data_getter: DataGetter(a),
shuffler: Shuffler(a), shuffler: Shuffler(a),
search: search.Model, search: search.Model,
settings: option.Option(common.Settings),
) )
} }
@ -93,7 +98,7 @@ pub fn register(
fn() { init(library.empty(), data_getter, shuffler) }, fn() { init(library.empty(), data_getter, shuffler) },
update, update,
generate_view(item_view, search_filter), generate_view(item_view, search_filter),
map.from_list([#("library", library_decode)]), map.from_list([#("library", library_decode), #("settings", settings_decode)]),
) )
} }
@ -101,18 +106,32 @@ pub fn register(
pub fn render( pub fn render(
name: String, name: String,
library: Library, library: Library,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
element.element( element.element(
name, name,
list.concat([[attribute.property("library", library)], extra_attrs]), list.concat([
[
attribute.property("library", library),
attribute.property("settings", settings),
],
extra_attrs,
]),
[], [],
) )
} }
fn init(library, data_getter, shuffler) { fn init(library, data_getter, shuffler) {
#( #(
Model(library, data_getter(library), data_getter, shuffler, search.init()), Model(
library,
data_getter(library),
data_getter,
shuffler,
search.init(),
option.None,
),
effect.none(), effect.none(),
) )
} }
@ -123,6 +142,10 @@ fn update(model, msg) {
Model(..model, library: library, data: model.data_getter(library)), Model(..model, library: library, data: model.data_getter(library)),
effect.none(), effect.none(),
) )
SettingsUpdated(settings) -> #(
Model(..model, settings: settings),
effect.none(),
)
ShuffleAll -> #(model, shuffle_all(model)) ShuffleAll -> #(model, shuffle_all(model))
StartPlay(tracks, position) -> #(model, start_play.emit(tracks, position)) StartPlay(tracks, position) -> #(model, start_play.emit(tracks, position))
Search(search_msg) -> { Search(search_msg) -> {
@ -155,10 +178,7 @@ fn generate_view(item_view: ItemView(a), search_filter: SearchFilter(a)) {
[h3([attribute.class("library-item-title")], [text("Shuffle all")])], [h3([attribute.class("library-item-title")], [text("Shuffle all")])],
), ),
], ],
list.index_map( list.index_map(items, fn(i, item) { item_view(model, items, i, item) }),
items,
fn(i, item) { item_view(model.library, items, i, item) },
),
), ),
) )
} }
@ -173,3 +193,8 @@ fn library_decode(data: dynamic.Dynamic) {
let library: Library = dynamic.unsafe_coerce(data) let library: Library = dynamic.unsafe_coerce(data)
Ok(LibraryUpdated(library)) Ok(LibraryUpdated(library))
} }
fn settings_decode(data: dynamic.Dynamic) {
let settings: option.Option(common.Settings) = dynamic.unsafe_coerce(data)
Ok(SettingsUpdated(settings))
}

View file

@ -4,13 +4,15 @@ import gleam/string
import gleam/int import gleam/int
import gleam/list import gleam/list
import gleam/map import gleam/map
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}
import lustre/attribute import lustre/attribute
import lustre/event import lustre/event
import elekf/library.{Library} import elekf/library.{Library}
import elekf/library/album.{Album} import elekf/library/album.{Album}
import elekf/web/components/library_view.{LibraryItem, StartPlay} import elekf/web/components/library_view.{LibraryItem, Model, StartPlay}
import elekf/web/common
const component_name = "albums-view" const component_name = "albums-view"
@ -26,8 +28,12 @@ pub fn register() {
} }
/// Render the albums view. /// Render the albums view.
pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { pub fn render(
library_view.render(component_name, library, extra_attrs) library: Library,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
library_view.render(component_name, library, settings, extra_attrs)
} }
fn data_getter(library: Library) { fn data_getter(library: Library) {
@ -46,16 +52,16 @@ fn search_filter(item: Album, search_text: String) {
} }
fn item_view( fn item_view(
library: Library, model: Model(Album),
_items: List(LibraryItem(Album)), _items: List(LibraryItem(Album)),
_index: Int, _index: Int,
item: LibraryItem(Album), item: LibraryItem(Album),
) { ) {
let #(album_id, album) = item let #(album_id, album) = item
let tracks = get_tracks(library, album) let tracks = get_tracks(model.library, album)
let artist_name = case album.artist_id { let artist_name = case album.artist_id {
0 -> "Unknown artist" 0 -> "Unknown artist"
id -> library.assert_artist(library, id).name id -> library.assert_artist(model.library, id).name
} }
div( div(

View file

@ -4,13 +4,16 @@ import gleam/string
import gleam/int import gleam/int
import gleam/list import gleam/list
import gleam/map import gleam/map
import lustre/element/html.{div, h3, p} import gleam/option
import lustre/element/html.{div, h3, img, p}
import lustre/element.{text} import lustre/element.{text}
import lustre/attribute import lustre/attribute
import lustre/event import lustre/event
import ibroadcast/artwork
import elekf/library.{Library} import elekf/library.{Library}
import elekf/library/artist.{Artist} import elekf/library/artist.{Artist}
import elekf/web/components/library_view.{LibraryItem, StartPlay} import elekf/web/components/library_view.{LibraryItem, Model, StartPlay}
import elekf/web/common
const component_name = "artists-view" const component_name = "artists-view"
@ -26,8 +29,12 @@ pub fn register() {
} }
/// Render the artists view. /// Render the artists view.
pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { pub fn render(
library_view.render(component_name, library, extra_attrs) library: Library,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
library_view.render(component_name, library, settings, extra_attrs)
} }
fn data_getter(library: Library) { fn data_getter(library: Library) {
@ -55,13 +62,13 @@ fn search_filter(item: Artist, search_text: String) {
} }
fn item_view( fn item_view(
library: Library, model: Model(Artist),
_items: List(LibraryItem(Artist)), _items: List(LibraryItem(Artist)),
_index: Int, _index: Int,
item: LibraryItem(Artist), item: LibraryItem(Artist),
) { ) {
let #(artist_id, artist) = item let #(artist_id, artist) = item
let tracks = get_tracks(library, artist) let tracks = get_tracks(model.library, artist)
div( div(
[ [
attribute.id("artist-list-" <> int.to_string(artist_id)), attribute.id("artist-list-" <> int.to_string(artist_id)),
@ -70,6 +77,20 @@ fn item_view(
attribute.attribute("role", "button"), attribute.attribute("role", "button"),
], ],
[ [
case model.settings, artist.artwork_id {
option.Some(s), option.Some(id) ->
img([
attribute.class("artist-image"),
attribute.alt("artist.name"),
attribute.src(artwork.url(s.artwork_server, id, artwork.S300)),
attribute.attribute("loading", "lazy"),
])
_, _ ->
div(
[attribute.class("artist-image-placeholder")],
[text(artist.name)],
)
},
h3([attribute.class("artist-title")], [text(artist.name)]), h3([attribute.class("artist-title")], [text(artist.name)]),
p( p(
[attribute.class("artist-tracks")], [attribute.class("artist-tracks")],

View file

@ -4,13 +4,15 @@ import gleam/string
import gleam/int import gleam/int
import gleam/list import gleam/list
import gleam/map import gleam/map
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}
import lustre/attribute import lustre/attribute
import lustre/event import lustre/event
import elekf/library.{Library} import elekf/library.{Library}
import elekf/library/track.{Track} import elekf/library/track.{Track}
import elekf/web/components/library_view.{LibraryItem, StartPlay} import elekf/web/components/library_view.{LibraryItem, Model, StartPlay}
import elekf/web/common
const component_name = "tracks-view" const component_name = "tracks-view"
@ -26,8 +28,12 @@ pub fn register() {
} }
/// Render the tracks view. /// Render the tracks view.
pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { pub fn render(
library_view.render(component_name, library, extra_attrs) library: Library,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
library_view.render(component_name, library, settings, extra_attrs)
} }
fn data_getter(library: Library) { fn data_getter(library: Library) {
@ -43,14 +49,14 @@ fn search_filter(item: Track, search_text: String) {
} }
fn item_view( fn item_view(
library: Library, model: Model(Track),
items: List(LibraryItem(Track)), items: List(LibraryItem(Track)),
index: Int, index: Int,
item: LibraryItem(Track), item: LibraryItem(Track),
) { ) {
let #(track_id, track) = item let #(track_id, track) = item
let album = library.assert_album(library, track.album_id) let album = library.assert_album(model.library, track.album_id)
let artist = library.assert_artist(library, track.artist_id) let artist = library.assert_artist(model.library, track.artist_id)
div( div(
[ [
attribute.id("track-list-" <> int.to_string(track_id)), attribute.id("track-list-" <> int.to_string(track_id)),

View file

@ -0,0 +1,22 @@
//// Artwork is fetched from the artwork API server, whose address can be found
//// in the library response.
/// The desired size of the image in pixels square.
pub type Size {
S150
S300
S1000
}
/// Get the URL to an artwork.
pub fn url(artwork_server: String, artwork_id: String, size: Size) {
artwork_server <> "/artwork/" <> artwork_id <> "-" <> size_arg(size)
}
fn size_arg(size: Size) {
case size {
S150 -> "150"
S300 -> "300"
S1000 -> "1000"
}
}

View file

@ -3,6 +3,7 @@ import gleam/json
import gleam/map.{Map} import gleam/map.{Map}
import gleam/dynamic import gleam/dynamic
import gleam/result import gleam/result
import gleam/option
import ibroadcast/servers import ibroadcast/servers
import ibroadcast/request.{DecodeFailed} import ibroadcast/request.{DecodeFailed}
import ibroadcast/authed_request.{RequestConfig} import ibroadcast/authed_request.{RequestConfig}
@ -23,7 +24,13 @@ pub type Album {
} }
pub type Artist { pub type Artist {
Artist(name: String, tracks: List(Int), trashed: Bool, rating: Int) Artist(
name: String,
tracks: List(Int),
trashed: Bool,
rating: Int,
artwork_id: option.Option(String),
)
} }
pub type Track { pub type Track {
@ -132,12 +139,16 @@ fn artists_decoder() {
} }
fn artist_decoder() { fn artist_decoder() {
dynamic.decode4( dynamic.decode5(
Artist, Artist,
dynamic.element(0, dynamic.string), dynamic.element(0, dynamic.string),
dynamic.element(1, dynamic.list(dynamic.int)), dynamic.element(1, dynamic.list(dynamic.int)),
dynamic.element(2, dynamic.bool), dynamic.element(2, dynamic.bool),
dynamic.element(3, dynamic.int), dynamic.element(3, dynamic.int),
dynamic.any([
dynamic.element(4, dynamic.optional(dynamic.string)),
fn(_) { Ok(option.None) },
]),
) )
} }