From 830918c344bf6a585d383981c5ca60972dc6f993 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Tue, 27 Feb 2024 10:17:03 +0200 Subject: [PATCH] Add single album view --- src/elekf/web/authed_view.gleam | 61 ++++-- src/elekf/web/components/library_view.gleam | 37 ++-- .../components/library_views/album_item.gleam | 56 ++--- .../library_views/albums_view.gleam | 1 + .../library_views/artists_view.gleam | 19 +- .../library_views/single_album_view.gleam | 200 ++++++++++++++++++ .../library_views/single_artist_view.gleam | 165 ++++++++------- .../components/library_views/track_item.gleam | 2 +- .../library_views/tracks_view.gleam | 2 +- src/elekf/web/components/link.gleam | 11 +- src/elekf/web/main.gleam | 39 ++-- src/elekf/web/router.gleam | 8 +- style.css | 17 +- 13 files changed, 421 insertions(+), 197 deletions(-) create mode 100644 src/elekf/web/components/library_views/single_album_view.gleam diff --git a/src/elekf/web/authed_view.gleam b/src/elekf/web/authed_view.gleam index c5a8e6b..293618f 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -25,16 +25,15 @@ import elekf/library/track.{type Track} import elekf/transfer/library as library_transfer import elekf/web/router import elekf/web/components/player -import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/library_view import elekf/web/components/library_views/tracks_view import elekf/web/components/library_views/artists_view import elekf/web/components/library_views/albums_view import elekf/web/components/library_views/single_artist_view +import elekf/web/components/library_views/single_album_view import elekf/web/components/button_group import elekf/web/components/link import elekf/web/events/start_play -import elekf/web/events/show_artist import elekf/web/utils import elekf/web/components/icon.{Alt, icon} import elektrofoni @@ -83,20 +82,29 @@ pub type Msg { } pub fn init(auth_data: common.AuthData) { - let model = - Model( - loading_library: True, - library: library.empty(), - settings: option.None, - request_config: form_request_config(auth_data), - play_status: NoTracks, - view: library_view.Tracks, - ) + let initial_library = library.empty() let router_effect = effect.from(fn(dispatch) { router.init(dispatch) }) |> effect.map(Router) + let initial_view = + router.get_current_path() + |> router.parse() + |> result.unwrap(router.TrackList) + |> route_to_view() + |> result.unwrap(library_view.Tracks) + + let model = + Model( + loading_library: True, + library: initial_library, + settings: option.None, + request_config: form_request_config(auth_data), + play_status: NoTracks, + view: initial_view, + ) + #(model, effect.batch([load_library(model), router_effect])) } @@ -183,8 +191,14 @@ pub fn update(model: Model, msg) { }) } Router(router.RouteChanged(route)) -> { - let view = result.unwrap(route_to_view(route), library_view.Tracks) - #(Model(..model, view: view), effect.none()) + case route_to_view(route) { + Ok(view) -> #(Model(..model, view: view), effect.none()) + Error(_) -> { + io.println_error("Unable to change to route:") + io.debug(route) + #(model, effect.none()) + } + } } } } @@ -205,13 +219,13 @@ pub fn view(model: Model) { div([attribute.id("authed-view-library")], [ nav([attribute.id("library-top-nav")], [ button_group.view("", [], [ - link.view(router.to_hash(router.TrackList), "", [], [ + link.button(router.to_hash(router.TrackList), "", [], [ icon("music-note-beamed", Alt("Tracks")), ]), - link.view(router.to_hash(router.ArtistList), "", [], [ + link.button(router.to_hash(router.ArtistList), "", [], [ icon("file-person", Alt("Artists")), ]), - link.view(router.to_hash(router.AlbumList), "", [], [ + link.button(router.to_hash(router.AlbumList), "", [], [ icon("disc", Alt("Albums")), ]), ]), @@ -234,12 +248,18 @@ pub fn view(model: Model) { attribute.class("glass-bg"), start_play.on(StartPlay), ]) - library_view.SingleArtist(artist_info) -> - single_artist_view.render(model.library, artist_info, model.settings, [ + library_view.SingleArtist(id) -> + single_artist_view.render(model.library, id, model.settings, [ attribute.id("single-artist-view"), attribute.class("glass-bg"), start_play.on(StartPlay), ]) + library_view.SingleAlbum(id) -> + single_album_view.render(model.library, id, model.settings, [ + attribute.id("single-album-view"), + attribute.class("glass-bg"), + start_play.on(StartPlay), + ]) }, ]), div( @@ -382,5 +402,10 @@ fn route_to_view(route: router.Route) { router.TrackList -> Ok(library_view.Tracks) router.ArtistList -> Ok(library_view.Artists) router.AlbumList -> Ok(library_view.Albums) + router.Artist(id) -> Ok(library_view.SingleArtist(id)) + // TODO + router.Album(id) -> Ok(library_view.SingleAlbum(id)) + // TODO + router.Settings -> Error(Nil) } } diff --git a/src/elekf/web/components/library_view.gleam b/src/elekf/web/components/library_view.gleam index ba59191..efdbd43 100644 --- a/src/elekf/web/components/library_view.gleam +++ b/src/elekf/web/components/library_view.gleam @@ -4,7 +4,7 @@ import gleam/dynamic import gleam/list -import gleam/map +import gleam/dict import gleam/option import gleam/string import lustre @@ -13,7 +13,7 @@ import lustre/element import lustre/element/html.{div} import lustre/attribute import lustre/event -import elekf/utils/order.{type Sorter} as order_utils +import elekf/utils/order.{type Sorter} import elekf/library.{type Library} import elekf/library/track.{type Track} import elekf/library/artist.{type Artist} @@ -54,11 +54,11 @@ pub type View { /// All albums. Albums /// Tracks of a single album. - /// SingleAlbum(Int, Album) + SingleAlbum(Int) /// All artists. Artists /// Albums of a single artist. - SingleArtist(LibraryItem(Artist)) + SingleArtist(Int) /// Tracks of a single artist. /// SingleArtistTracks(Int, Artist) /// All tracks. @@ -73,11 +73,13 @@ pub type Msg { ShowArtist(LibraryItem(Artist)) Search(search.Msg) AlbumExpandToggled(LibraryItem(Album)) + FilterUpdated } pub type Model(a) { Model( library: Library, + library_loading: Bool, data: List(LibraryItem(a)), data_getter: DataGetter(a), shuffler: Shuffler(a), @@ -106,7 +108,7 @@ pub fn register( ) { lustre.component( name, - fn() { init(library.empty(), data_getter, shuffler, sorter) }, + fn() { init(library.empty(), True, data_getter, shuffler, sorter) }, update, generate_view(item_view, search_filter), generic_attributes(), @@ -116,7 +118,7 @@ pub fn register( /// Get the generic properties common to all library views, that can be input /// into the Lustre component attribute change pub fn generic_attributes() { - map.from_list([#("library", library_decode), #("settings", settings_decode)]) + dict.from_list([#("library", library_decode), #("settings", settings_decode)]) } /// Render the component using a custom element. @@ -137,10 +139,11 @@ pub fn render( ) } -pub fn init(library, data_getter, shuffler, sorter) { +pub fn init(library, library_loading, data_getter, shuffler, sorter) { #( Model( library, + library_loading, data_getter(library), data_getter, shuffler, @@ -155,13 +158,8 @@ pub fn init(library, data_getter, shuffler, sorter) { pub fn update(model, msg) { case msg { LibraryUpdated(library) -> #( - Model( - ..model, - library: library, - data: library - |> model.data_getter() - |> list.sort(fn(a, b) { model.sorter(a.1, b.1) }), - ), + Model(..model, library: library, library_loading: False) + |> update_data(library), effect.none(), ) SettingsUpdated(settings) -> #( @@ -178,6 +176,8 @@ pub fn update(model, msg) { // Base case, this should be handled in an implementing view AlbumExpandToggled(_album) -> #(model, effect.none()) + + FilterUpdated -> #(update_data(model, model.library), effect.none()) } } @@ -227,3 +227,12 @@ fn settings_decode(data: dynamic.Dynamic) { let settings: option.Option(common.Settings) = dynamic.unsafe_coerce(data) Ok(SettingsUpdated(settings)) } + +fn update_data(model: Model(a), library: Library) { + Model( + ..model, + data: library + |> model.data_getter() + |> list.sort(fn(a, b) { model.sorter(a.1, b.1) }), + ) +} diff --git a/src/elekf/web/components/library_views/album_item.gleam b/src/elekf/web/components/library_views/album_item.gleam index 3452cd6..145f571 100644 --- a/src/elekf/web/components/library_views/album_item.gleam +++ b/src/elekf/web/components/library_views/album_item.gleam @@ -17,11 +17,9 @@ import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/library_views/track_item import elekf/web/components/thumbnail import elekf/web/components/shuffle_all +import elekf/web/components/link import elekf/web/common.{type Settings} - -const base_classes = "library-item album-item" - -const showing_tracks_class = "library-item-expanded" +import elekf/web/router pub fn view( library: Library, @@ -37,45 +35,23 @@ pub fn view( } let assert Ok(first_track) = list.first(tracks) - let class = case show_tracks { - True -> base_classes <> " " <> showing_tracks_class - False -> base_classes - } - let expanded = case show_tracks { - True -> "true" - False -> "false" - } - list.append( [ - div( + link.link( + router.to_hash(router.Album(album_id)), + "library-item album-item", + [attribute.id("album-list-" <> int.to_string(album_id))], [ - attribute.id("album-list-" <> int.to_string(album_id)), - attribute.class(class), - ], - [ - div( - [ - attribute.class("album-item-expander"), - event.on_click(AlbumExpandToggled(item)), - event.on_keydown(fn(_) { AlbumExpandToggled(item) }), - attribute.attribute("role", "button"), - attribute.attribute("aria-expanded", expanded), - attribute.attribute("tabindex", "0"), - ], - [ - thumbnail.maybe_item_thumbnail( - settings, - { first_track.1 }.artwork_id, - 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")], [ - text(int.to_string(list.length(album.tracks)) <> " tracks"), - ]), - ], + thumbnail.maybe_item_thumbnail( + settings, + { first_track.1 }.artwork_id, + 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")], [ + text(int.to_string(list.length(album.tracks)) <> " tracks"), + ]), ], ), ], @@ -99,7 +75,7 @@ pub fn view_tracks(library: Library, tracks: List(LibraryItem(Track))) { list.flatten([ [shuffle_all.view([event.on_click(ShuffleAll)])], list.index_map(tracks, fn(track_item, index) { - track_item.view(library, tracks, track_item, index, "album-tracks-list") + track_item.view(library, tracks, index, track_item, "album-tracks-list") }) |> list.flatten(), ]), diff --git a/src/elekf/web/components/library_views/albums_view.gleam b/src/elekf/web/components/library_views/albums_view.gleam index 69c8fe5..b920551 100644 --- a/src/elekf/web/components/library_views/albums_view.gleam +++ b/src/elekf/web/components/library_views/albums_view.gleam @@ -60,6 +60,7 @@ fn init() { let #(lib_m, lib_e) = library_view.init( library.empty(), + True, data_getter, shuffler, album_utils.sort_by_name, diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index 103dfc6..354e127 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -5,17 +5,18 @@ import gleam/int import gleam/list import gleam/dict import gleam/option -import lustre/element/html.{div, h3, p} +import lustre/element/html.{h3, p} import lustre/element.{text} import lustre/attribute -import lustre/event import elekf/library.{type Library} 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_view.{type Model} import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/thumbnail import elekf/web/common +import elekf/web/router +import elekf/web/components/link const component_name = "artists-view" @@ -69,14 +70,10 @@ fn item_view( ) { let #(artist_id, artist) = item [ - div( - [ - attribute.id("artist-list-" <> int.to_string(artist_id)), - attribute.class("library-item artist-item"), - attribute.type_("button"), - event.on_click(ShowArtist(item)), - attribute.attribute("role", "button"), - ], + link.link( + router.to_hash(router.Artist(artist_id)), + "library-item artist-item", + [attribute.id("artist-list-" <> int.to_string(artist_id))], [ thumbnail.maybe_item_thumbnail( model.settings, diff --git a/src/elekf/web/components/library_views/single_album_view.gleam b/src/elekf/web/components/library_views/single_album_view.gleam new file mode 100644 index 0000000..1ccf08b --- /dev/null +++ b/src/elekf/web/components/library_views/single_album_view.gleam @@ -0,0 +1,200 @@ +//// A library view to a single album's tracks. + +import gleam/string +import gleam/list +import gleam/dict +import gleam/option +import gleam/dynamic +import gleam/result +import gleam/int +import lustre +import lustre/element/html.{div, header} +import lustre/element.{text} +import lustre/attribute +import lustre/effect +import elekf/library.{type Library} +import elekf/library/album.{type Album} +import elekf/library/track.{type Track} +import elekf/library/track_utils +import elekf/web/components/library_view +import elekf/web/components/library_item.{type LibraryItem} +import elekf/web/components/library_views/track_item +import elekf/web/common + +const component_name = "single-album-view" + +type Model { + Model( + library_view: library_view.Model(Track), + album_id: Int, + album: option.Option(Album), + ) +} + +type Msg { + LibraryViewMsg(library_view.Msg) + AlbumUpdated(Int) +} + +/// Register the single album view as a custom element. +pub fn register() { + lustre.component( + component_name, + init, + update, + generate_view(search_filter), + dict.merge( + library_view.generic_attributes() + |> dict.map_values(fn(_key, decoder) { + fn(data: dynamic.Dynamic) { + data + |> decoder() + |> result.map(LibraryViewMsg) + } + }), + dict.from_list([#("album-id", id_decode)]), + ), + ) +} + +/// Render the single album view. +pub fn render( + library: Library, + album_id: Int, + settings: option.Option(common.Settings), + extra_attrs: List(attribute.Attribute(msg)), +) { + library_view.render(component_name, library, settings, [ + attribute.property("album-id", album_id), + ..extra_attrs + ]) +} + +fn init() { + let #(lib_m, lib_e) = + library_view.init( + library.empty(), + True, + fn(_) -> List(LibraryItem(Track)) { [] }, + shuffler, + track_utils.sort_by_name, + ) + + #( + Model(album_id: library.invalid_id, album: option.None, library_view: lib_m), + effect.map(lib_e, LibraryViewMsg), + ) +} + +fn update(model: Model, msg) { + case msg { + AlbumUpdated(id) -> { + let new_getter = data_getter(_, id) + + let album = + load_album(model.library_view.library, id) + |> option.from_result() + + #( + Model( + album_id: id, + album: album, + library_view: library_view.Model( + ..model.library_view, + data_getter: new_getter, + ), + ), + effect.map( + effect.from(fn(dispatch) { dispatch(library_view.FilterUpdated) }), + LibraryViewMsg, + ), + ) + } + + LibraryViewMsg(lib_msg) -> { + let #(lib_m, lib_e) = library_view.update(model.library_view, lib_msg) + + // Update album when library is updated + let album = case lib_msg { + library_view.LibraryUpdated(new_lib) -> + load_album(new_lib, model.album_id) + |> option.from_result() + _ -> model.album + } + + #( + Model(..model, album: album, library_view: lib_m), + effect.map(lib_e, LibraryViewMsg), + ) + } + } +} + +fn data_getter(library: Library, album_id: Int) { + library.tracks + |> dict.fold([], fn(acc, key, val) { + case val.album_id == album_id { + True -> [#(key, val), ..acc] + False -> acc + } + }) +} + +fn shuffler(_library, items: List(LibraryItem(Track))) { + items + |> list.shuffle() +} + +fn search_filter(item: Track, search_text: String) { + string.contains(item.title_lower, search_text) +} + +fn generate_view(search_filter: library_view.SearchFilter(Track)) { + fn(model: Model) { + case model.library_view.library_loading, model.album { + True, _ -> + div([attribute.class("library-view-loading")], [ + text("Loading library…"), + ]) + False, option.None -> + div([attribute.class("library-view-not-specified")], [ + text("Album not found with ID: " <> int.to_string(model.album_id)), + ]) + False, option.Some(album) -> + view(model, #(model.album_id, album), search_filter) + } + } +} + +fn view( + model: Model, + album: LibraryItem(Album), + search_filter: library_view.SearchFilter(Track), +) { + div([], [ + header([attribute.class("library-header")], [text({ album.1 }.name)]), + library_view.library_view( + model.library_view, + fn(library, items, index, item) { + track_item.view( + library.library, + items, + index, + item, + "album-tracks-list", + ) + }, + search_filter, + ) + |> element.map(LibraryViewMsg), + ]) +} + +fn id_decode(data: dynamic.Dynamic) { + let album_id: Int = dynamic.unsafe_coerce(data) + Ok(AlbumUpdated(album_id)) +} + +fn load_album(library: Library, id: Int) { + library.get_album(library, id) +} diff --git a/src/elekf/web/components/library_views/single_artist_view.gleam b/src/elekf/web/components/library_views/single_artist_view.gleam index 341aad8..d6c14b8 100644 --- a/src/elekf/web/components/library_views/single_artist_view.gleam +++ b/src/elekf/web/components/library_views/single_artist_view.gleam @@ -2,21 +2,21 @@ import gleam/string import gleam/list -import gleam/map +import gleam/dict import gleam/option import gleam/dynamic import gleam/result +import gleam/int import lustre import lustre/element/html.{div, header} import lustre/element.{text} import lustre/attribute import lustre/effect -import elekf/utils/misc import elekf/library.{type Library} import elekf/library/album.{type Album} import elekf/library/album_utils import elekf/library/artist.{type Artist} -import elekf/web/components/library_view.{AlbumExpandToggled} +import elekf/web/components/library_view import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/library_views/album_item import elekf/web/common @@ -26,14 +26,14 @@ const component_name = "single-artist-view" type Model { Model( library_view: library_view.Model(Album), - artist: option.Option(LibraryItem(Artist)), - expanded_album: Int, + artist_id: Int, + artist: option.Option(Artist), ) } type Msg { LibraryViewMsg(library_view.Msg) - ArtistUpdated(option.Option(LibraryItem(Artist))) + ArtistUpdated(Int) } /// Register the single artist view as a custom element. @@ -43,16 +43,16 @@ pub fn register() { init, update, generate_view(search_filter), - map.merge( + dict.merge( library_view.generic_attributes() - |> map.map_values(fn(_key, decoder) { + |> dict.map_values(fn(_key, decoder) { fn(data: dynamic.Dynamic) { data |> decoder() |> result.map(LibraryViewMsg) } }), - map.from_list([#("artist", artist_decode)]), + dict.from_list([#("artist-id", id_decode)]), ), ) } @@ -60,22 +60,21 @@ pub fn register() { /// Render the single artist view. pub fn render( library: Library, - artist: LibraryItem(Artist), + artist_id: Int, settings: option.Option(common.Settings), extra_attrs: List(attribute.Attribute(msg)), ) { - library_view.render( - component_name, - library, - settings, - [attribute.property("artist", option.Some(artist)), ..extra_attrs], - ) + library_view.render(component_name, library, settings, [ + attribute.property("artist-id", artist_id), + ..extra_attrs + ]) } fn init() { let #(lib_m, lib_e) = library_view.init( library.empty(), + True, fn(_) -> List(LibraryItem(Album)) { [] }, shuffler, album_utils.sort_by_year, @@ -83,8 +82,8 @@ fn init() { #( Model( + artist_id: library.invalid_id, artist: option.None, - expanded_album: library.invalid_id, library_view: lib_m, ), effect.map(lib_e, LibraryViewMsg), @@ -93,55 +92,56 @@ fn init() { fn update(model: Model, msg) { case msg { - ArtistUpdated(option.Some(artist)) -> #( - Model( - artist: option.Some(artist), - expanded_album: library.invalid_id, - library_view: library_view.Model( - ..model.library_view, - data: data_getter(model.library_view.library, artist) - |> list.sort(fn(a, b) { model.library_view.sorter(a.1, b.1) }), + ArtistUpdated(id) -> { + let new_getter = data_getter(_, id) + + let artist = + load_artist(model.library_view.library, id) + |> option.from_result() + + #( + Model( + artist_id: id, + artist: artist, + library_view: library_view.Model( + ..model.library_view, + data_getter: new_getter, + ), ), - ), - effect.none(), - ) - ArtistUpdated(option.None) -> #( - Model( - artist: option.None, - expanded_album: library.invalid_id, - library_view: library_view.Model(..model.library_view, data: []), - ), - effect.none(), - ) - LibraryViewMsg(AlbumExpandToggled(album)) -> #( - Model( - ..model, - expanded_album: misc.toggle( - model.expanded_album, - album.0, - library.invalid_id, + effect.map( + effect.from(fn(dispatch) { dispatch(library_view.FilterUpdated) }), + LibraryViewMsg, ), - ), - effect.none(), - ) + ) + } + LibraryViewMsg(lib_msg) -> { let #(lib_m, lib_e) = library_view.update(model.library_view, lib_msg) - #(Model(..model, library_view: lib_m), effect.map(lib_e, LibraryViewMsg)) + + // Update artist when library is updated + let artist = case lib_msg { + library_view.LibraryUpdated(new_lib) -> + load_artist(new_lib, model.artist_id) + |> option.from_result() + _ -> model.artist + } + + #( + Model(..model, artist: artist, library_view: lib_m), + effect.map(lib_e, LibraryViewMsg), + ) } } } -fn data_getter(library: Library, artist: LibraryItem(Artist)) { +fn data_getter(library: Library, artist_id: Int) { library.albums - |> map.fold( - [], - fn(acc, key, val) { - case val.artist_id == artist.0 { - True -> [#(key, val), ..acc] - False -> acc - } - }, - ) + |> dict.fold([], fn(acc, key, val) { + case val.artist_id == artist_id { + True -> [#(key, val), ..acc] + False -> acc + } + }) } fn shuffler(library, items: List(LibraryItem(Album))) { @@ -157,13 +157,17 @@ fn search_filter(item: Album, search_text: String) { fn generate_view(search_filter: library_view.SearchFilter(Album)) { fn(model: Model) { - case model.artist { - option.None -> - div( - [attribute.class("library-view-not-specified")], - [text("Artist not specified.")], - ) - option.Some(artist) -> view(model, artist, search_filter) + case model.library_view.library_loading, model.artist { + True, _ -> + div([attribute.class("library-view-loading")], [ + text("Loading library…"), + ]) + False, option.None -> + div([attribute.class("library-view-not-specified")], [ + text("Artist not found with ID: " <> int.to_string(model.artist_id)), + ]) + False, option.Some(artist) -> + view(model, #(model.artist_id, artist), search_filter) } } } @@ -173,18 +177,15 @@ fn view( artist: LibraryItem(Artist), search_filter: library_view.SearchFilter(Album), ) { - div( - [], - [ - header([attribute.class("library-header")], [text({ artist.1 }.name)]), - library_view.library_view( - model.library_view, - fn(_library_model, _items, _index, item) { album_view(model, item) }, - search_filter, - ) - |> element.map(LibraryViewMsg), - ], - ) + div([], [ + header([attribute.class("library-header")], [text({ artist.1 }.name)]), + library_view.library_view( + model.library_view, + fn(_library_model, _items, _index, item) { album_view(model, item) }, + search_filter, + ) + |> element.map(LibraryViewMsg), + ]) } fn album_view(model: Model, item: LibraryItem(Album)) { @@ -192,11 +193,15 @@ fn album_view(model: Model, item: LibraryItem(Album)) { model.library_view.library, model.library_view.settings, item, - item.0 == model.expanded_album, + False, ) } -fn artist_decode(data: dynamic.Dynamic) { - let artist: option.Option(LibraryItem(Artist)) = dynamic.unsafe_coerce(data) - Ok(ArtistUpdated(artist)) +fn id_decode(data: dynamic.Dynamic) { + let artist_id: Int = dynamic.unsafe_coerce(data) + Ok(ArtistUpdated(artist_id)) +} + +fn load_artist(library: Library, id: Int) { + library.get_artist(library, id) } diff --git a/src/elekf/web/components/library_views/track_item.gleam b/src/elekf/web/components/library_views/track_item.gleam index 440786f..9c27fe3 100644 --- a/src/elekf/web/components/library_views/track_item.gleam +++ b/src/elekf/web/components/library_views/track_item.gleam @@ -11,8 +11,8 @@ import elekf/web/components/library_view.{StartPlay} pub fn view( library: Library, items: List(LibraryItem(Track)), - item: LibraryItem(Track), index: Int, + item: LibraryItem(Track), id_prefix: String, ) { let #(track_id, track) = item diff --git a/src/elekf/web/components/library_views/tracks_view.gleam b/src/elekf/web/components/library_views/tracks_view.gleam index c77275e..9faf5c2 100644 --- a/src/elekf/web/components/library_views/tracks_view.gleam +++ b/src/elekf/web/components/library_views/tracks_view.gleam @@ -54,5 +54,5 @@ fn item_view( index: Int, item: LibraryItem(Track), ) { - track_item.view(model.library, items, item, index, "track-list") + track_item.view(model.library, items, index, item, "track-list") } diff --git a/src/elekf/web/components/link.gleam b/src/elekf/web/components/link.gleam index 04422a5..01a5868 100644 --- a/src/elekf/web/components/link.gleam +++ b/src/elekf/web/components/link.gleam @@ -2,7 +2,7 @@ import lustre/attribute import lustre/element import lustre/element/html.{a} -pub fn view( +pub fn button( href: String, class: String, extra_attributes: List(attribute.Attribute(a)), @@ -17,3 +17,12 @@ pub fn view( content, ) } + +pub fn link( + href: String, + class: String, + extra_attributes: List(attribute.Attribute(a)), + content: List(element.Element(a)), +) { + a([attribute.href(href), attribute.class(class), ..extra_attributes], content) +} diff --git a/src/elekf/web/main.gleam b/src/elekf/web/main.gleam index 1cc788d..9664d38 100644 --- a/src/elekf/web/main.gleam +++ b/src/elekf/web/main.gleam @@ -16,6 +16,7 @@ import elekf/web/components/library_views/tracks_view import elekf/web/components/library_views/artists_view import elekf/web/components/library_views/albums_view import elekf/web/components/library_views/single_artist_view +import elekf/web/components/library_views/single_album_view import elekf/api/auth/storage as auth_storage import elekf/api/auth/models as auth_models import elekf/utils/option as option_utils @@ -41,6 +42,7 @@ pub fn main() { let assert Ok(_) = artists_view.register() let assert Ok(_) = albums_view.register() let assert Ok(_) = single_artist_view.register() + let assert Ok(_) = single_album_view.register() let app = lustre.application(init, update, view) let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) @@ -128,29 +130,20 @@ fn update(model: Model, msg) { } fn view(model: Model) { - html.main( - [], - [ - case model.current_view { - view.LoginView -> - div( - [attribute.id("login-view")], - [ - login_view.view(model.login_view) - |> element.map(LoginView), - ], - ) - view.AuthedView -> - div( - [attribute.id("authed-view")], - [ - authed_view.view(option_utils.assert_some(model.authed_view)) - |> element.map(AuthedView), - ], - ) - }, - ], - ) + html.main([], [ + case model.current_view { + view.LoginView -> + div([attribute.id("login-view")], [ + login_view.view(model.login_view) + |> element.map(LoginView), + ]) + view.AuthedView -> + div([attribute.id("authed-view")], [ + authed_view.view(option_utils.assert_some(model.authed_view)) + |> element.map(AuthedView), + ]) + }, + ]) } fn logged_in(model: Model, token: String, session: login_token.Session) { diff --git a/src/elekf/web/router.gleam b/src/elekf/web/router.gleam index e3574c6..dd78131 100644 --- a/src/elekf/web/router.gleam +++ b/src/elekf/web/router.gleam @@ -30,7 +30,7 @@ pub fn init(dispatch: fn(Msg) -> Nil) { } pub fn run(dispatch: fn(Msg) -> Nil) { - use hash <- result.try(window.get_hash()) + let hash = get_current_path() use route <- result.try(parse(hash)) Ok(dispatch(RouteChanged(route))) } @@ -39,7 +39,7 @@ pub fn to_hash(route: Route) { "#" <> to_string(route) } -fn parse(route: String) { +pub fn parse(route: String) { let parts = route |> string.drop_left(1) @@ -62,6 +62,10 @@ fn parse(route: String) { } } +pub fn get_current_path() { + result.unwrap(window.get_hash(), "/") +} + fn to_string(route: Route) { let parts = case route { TrackList -> [] diff --git a/style.css b/style.css index 3d86028..6fb00c9 100644 --- a/style.css +++ b/style.css @@ -144,7 +144,8 @@ a { tracks-view, albums-view, artists-view, -single-artist-view { +single-artist-view, +single-album-view { display: block; margin-top: calc(var(--library-top-nav-height) + var(--side-margin)); @@ -166,11 +167,6 @@ single-artist-view { overflow-y: auto; } -/* Ensure that the view stack only shows the topmost view and the nav bar. */ -#authed-view-library > *:not(:first-child, :last-child) { - display: none; -} - #authed-view-player { position: absolute; bottom: 0; @@ -362,6 +358,15 @@ single-artist-view { font-size: var(--search-size); } +.library-header { + padding: var(--side-margin); + overflow-x: hidden; + text-wrap: nowrap; + text-overflow: ellipsis; + + font-size: 2rem; +} + .library-list { height: 100%; overflow-y: auto;