Add single album view
This commit is contained in:
parent
451b619f5b
commit
830918c344
13 changed files with 421 additions and 197 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,32 +35,12 @@ 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(
|
||||
[
|
||||
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"),
|
||||
],
|
||||
link.link(
|
||||
router.to_hash(router.Album(album_id)),
|
||||
"library-item album-item",
|
||||
[attribute.id("album-list-" <> int.to_string(album_id))],
|
||||
[
|
||||
thumbnail.maybe_item_thumbnail(
|
||||
settings,
|
||||
|
@ -77,8 +55,6 @@ pub fn view(
|
|||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
case show_tracks {
|
||||
True -> [view_tracks(library, tracks)]
|
||||
False -> []
|
||||
|
@ -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(),
|
||||
]),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
200
src/elekf/web/components/library_views/single_album_view.gleam
Normal file
200
src/elekf/web/components/library_views/single_album_view.gleam
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)) -> #(
|
||||
ArtistUpdated(id) -> {
|
||||
let new_getter = data_getter(_, id)
|
||||
|
||||
let artist =
|
||||
load_artist(model.library_view.library, id)
|
||||
|> option.from_result()
|
||||
|
||||
#(
|
||||
Model(
|
||||
artist: option.Some(artist),
|
||||
expanded_album: library.invalid_id,
|
||||
artist_id: id,
|
||||
artist: artist,
|
||||
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) }),
|
||||
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.none(),
|
||||
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)
|
||||
#(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 {
|
||||
|> 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,9 +177,7 @@ fn view(
|
|||
artist: LibraryItem(Artist),
|
||||
search_filter: library_view.SearchFilter(Album),
|
||||
) {
|
||||
div(
|
||||
[],
|
||||
[
|
||||
div([], [
|
||||
header([attribute.class("library-header")], [text({ artist.1 }.name)]),
|
||||
library_view.library_view(
|
||||
model.library_view,
|
||||
|
@ -183,8 +185,7 @@ fn view(
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
[],
|
||||
[
|
||||
html.main([], [
|
||||
case model.current_view {
|
||||
view.LoginView ->
|
||||
div(
|
||||
[attribute.id("login-view")],
|
||||
[
|
||||
div([attribute.id("login-view")], [
|
||||
login_view.view(model.login_view)
|
||||
|> element.map(LoginView),
|
||||
],
|
||||
)
|
||||
])
|
||||
view.AuthedView ->
|
||||
div(
|
||||
[attribute.id("authed-view")],
|
||||
[
|
||||
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) {
|
||||
|
|
|
@ -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 -> []
|
||||
|
|
17
style.css
17
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;
|
||||
|
|
Loading…
Reference in a new issue