Add artist artwork
This commit is contained in:
parent
2216d4b720
commit
960eaa73f7
9 changed files with 126 additions and 28 deletions
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")],
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
22
src/ibroadcast/artwork.gleam
Normal file
22
src/ibroadcast/artwork.gleam
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) },
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue