From 960eaa73f70c0fa86119789bc485fedc36e1825f Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Mon, 23 Oct 2023 20:23:10 +0300 Subject: [PATCH] Add artist artwork --- src/elekf/library/artist.gleam | 3 ++ src/elekf/transfer/artist.gleam | 1 + src/elekf/web/authed_view.gleam | 3 ++ src/elekf/web/components/library_view.gleam | 41 +++++++++++++++---- .../library_views/albums_view.gleam | 18 +++++--- .../library_views/artists_view.gleam | 33 ++++++++++++--- .../library_views/tracks_view.gleam | 18 +++++--- src/ibroadcast/artwork.gleam | 22 ++++++++++ src/ibroadcast/library/library.gleam | 15 ++++++- 9 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 src/ibroadcast/artwork.gleam diff --git a/src/elekf/library/artist.gleam b/src/elekf/library/artist.gleam index 23012be..e8a7215 100644 --- a/src/elekf/library/artist.gleam +++ b/src/elekf/library/artist.gleam @@ -1,3 +1,5 @@ +import gleam/option + pub type Artist { Artist( name: String, @@ -5,5 +7,6 @@ pub type Artist { tracks: List(Int), trashed: Bool, rating: Int, + artwork_id: option.Option(String), ) } diff --git a/src/elekf/transfer/artist.gleam b/src/elekf/transfer/artist.gleam index 133a9f6..31fa9a3 100644 --- a/src/elekf/transfer/artist.gleam +++ b/src/elekf/transfer/artist.gleam @@ -10,5 +10,6 @@ pub fn from(artist: APIArtist) { tracks: artist.tracks, trashed: artist.trashed, rating: artist.rating, + artwork_id: artist.artwork_id, ) } diff --git a/src/elekf/web/authed_view.gleam b/src/elekf/web/authed_view.gleam index a00a47a..5db3141 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -220,16 +220,19 @@ pub fn view(model: Model) { library_view.Tracks -> tracks_view.render( model.library, + model.settings, [attribute.id("tracks-view"), start_play.on(StartPlay)], ) library_view.Artists -> artists_view.render( model.library, + model.settings, [attribute.id("artists-view"), start_play.on(StartPlay)], ) library_view.Albums -> albums_view.render( model.library, + model.settings, [attribute.id("albums-view"), start_play.on(StartPlay)], ) }, diff --git a/src/elekf/web/components/library_view.gleam b/src/elekf/web/components/library_view.gleam index c7537ff..faf36e0 100644 --- a/src/elekf/web/components/library_view.gleam +++ b/src/elekf/web/components/library_view.gleam @@ -5,6 +5,7 @@ import gleam/dynamic import gleam/list import gleam/map +import gleam/option import lustre import lustre/effect import lustre/element.{text} @@ -15,6 +16,7 @@ import elekf/library.{Library} import elekf/library/track.{Track} import elekf/web/events/start_play import elekf/web/components/search +import elekf/web/common /// An item in the library with its ID. pub type LibraryItem(a) = @@ -26,7 +28,8 @@ pub type DataGetter(a) = /// A view that renders a single item from the library. 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 /// `True` if the item should be shown and `False` if not. @@ -57,6 +60,7 @@ pub type View { pub type Msg { LibraryUpdated(Library) + SettingsUpdated(option.Option(common.Settings)) ShuffleAll StartPlay(List(LibraryItem(Track)), Int) Search(search.Msg) @@ -69,6 +73,7 @@ pub type Model(a) { data_getter: DataGetter(a), shuffler: Shuffler(a), search: search.Model, + settings: option.Option(common.Settings), ) } @@ -93,7 +98,7 @@ pub fn register( fn() { init(library.empty(), data_getter, shuffler) }, update, 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( name: String, library: Library, + settings: option.Option(common.Settings), extra_attrs: List(attribute.Attribute(msg)), ) { element.element( 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) { #( - Model(library, data_getter(library), data_getter, shuffler, search.init()), + Model( + library, + data_getter(library), + data_getter, + shuffler, + search.init(), + option.None, + ), effect.none(), ) } @@ -123,6 +142,10 @@ fn update(model, msg) { Model(..model, library: library, data: model.data_getter(library)), effect.none(), ) + SettingsUpdated(settings) -> #( + Model(..model, settings: settings), + effect.none(), + ) ShuffleAll -> #(model, shuffle_all(model)) StartPlay(tracks, position) -> #(model, start_play.emit(tracks, position)) 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")])], ), ], - list.index_map( - items, - fn(i, item) { item_view(model.library, items, i, item) }, - ), + list.index_map(items, fn(i, item) { item_view(model, items, i, item) }), ), ) } @@ -173,3 +193,8 @@ fn library_decode(data: dynamic.Dynamic) { let library: Library = dynamic.unsafe_coerce(data) Ok(LibraryUpdated(library)) } + +fn settings_decode(data: dynamic.Dynamic) { + let settings: option.Option(common.Settings) = dynamic.unsafe_coerce(data) + Ok(SettingsUpdated(settings)) +} diff --git a/src/elekf/web/components/library_views/albums_view.gleam b/src/elekf/web/components/library_views/albums_view.gleam index a1daaea..834ba88 100644 --- a/src/elekf/web/components/library_views/albums_view.gleam +++ b/src/elekf/web/components/library_views/albums_view.gleam @@ -4,13 +4,15 @@ import gleam/string import gleam/int import gleam/list import gleam/map +import gleam/option import lustre/element/html.{div, h3, p} import lustre/element.{text} import lustre/attribute import lustre/event import elekf/library.{Library} 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" @@ -26,8 +28,12 @@ pub fn register() { } /// Render the albums view. -pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { - library_view.render(component_name, library, extra_attrs) +pub fn render( + 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) { @@ -46,16 +52,16 @@ fn search_filter(item: Album, search_text: String) { } fn item_view( - library: Library, + model: Model(Album), _items: List(LibraryItem(Album)), _index: Int, item: LibraryItem(Album), ) { 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 { 0 -> "Unknown artist" - id -> library.assert_artist(library, id).name + id -> library.assert_artist(model.library, id).name } div( diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index cced4d7..e77ec1d 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -4,13 +4,16 @@ import gleam/string import gleam/int import gleam/list 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/attribute import lustre/event +import ibroadcast/artwork import elekf/library.{Library} 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" @@ -26,8 +29,12 @@ pub fn register() { } /// Render the artists view. -pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { - library_view.render(component_name, library, extra_attrs) +pub fn render( + 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) { @@ -55,13 +62,13 @@ fn search_filter(item: Artist, search_text: String) { } fn item_view( - library: Library, + model: Model(Artist), _items: List(LibraryItem(Artist)), _index: Int, item: LibraryItem(Artist), ) { let #(artist_id, artist) = item - let tracks = get_tracks(library, artist) + let tracks = get_tracks(model.library, artist) div( [ attribute.id("artist-list-" <> int.to_string(artist_id)), @@ -70,6 +77,20 @@ fn item_view( 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)]), p( [attribute.class("artist-tracks")], diff --git a/src/elekf/web/components/library_views/tracks_view.gleam b/src/elekf/web/components/library_views/tracks_view.gleam index 98bb362..ba98bb1 100644 --- a/src/elekf/web/components/library_views/tracks_view.gleam +++ b/src/elekf/web/components/library_views/tracks_view.gleam @@ -4,13 +4,15 @@ import gleam/string import gleam/int import gleam/list import gleam/map +import gleam/option import lustre/element/html.{div, h3, p} import lustre/element.{text} import lustre/attribute import lustre/event import elekf/library.{Library} 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" @@ -26,8 +28,12 @@ pub fn register() { } /// Render the tracks view. -pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) { - library_view.render(component_name, library, extra_attrs) +pub fn render( + 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) { @@ -43,14 +49,14 @@ fn search_filter(item: Track, search_text: String) { } fn item_view( - library: Library, + model: Model(Track), items: List(LibraryItem(Track)), index: Int, item: LibraryItem(Track), ) { let #(track_id, track) = item - let album = library.assert_album(library, track.album_id) - let artist = library.assert_artist(library, track.artist_id) + let album = library.assert_album(model.library, track.album_id) + let artist = library.assert_artist(model.library, track.artist_id) div( [ attribute.id("track-list-" <> int.to_string(track_id)), diff --git a/src/ibroadcast/artwork.gleam b/src/ibroadcast/artwork.gleam new file mode 100644 index 0000000..c06d81f --- /dev/null +++ b/src/ibroadcast/artwork.gleam @@ -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" + } +} diff --git a/src/ibroadcast/library/library.gleam b/src/ibroadcast/library/library.gleam index 1bdd93e..4020f0c 100644 --- a/src/ibroadcast/library/library.gleam +++ b/src/ibroadcast/library/library.gleam @@ -3,6 +3,7 @@ import gleam/json import gleam/map.{Map} import gleam/dynamic import gleam/result +import gleam/option import ibroadcast/servers import ibroadcast/request.{DecodeFailed} import ibroadcast/authed_request.{RequestConfig} @@ -23,7 +24,13 @@ pub type Album { } 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 { @@ -132,12 +139,16 @@ fn artists_decoder() { } fn artist_decoder() { - dynamic.decode4( + dynamic.decode5( Artist, dynamic.element(0, dynamic.string), dynamic.element(1, dynamic.list(dynamic.int)), dynamic.element(2, dynamic.bool), dynamic.element(3, dynamic.int), + dynamic.any([ + dynamic.element(4, dynamic.optional(dynamic.string)), + fn(_) { Ok(option.None) }, + ]), ) }