From 33f8cd92f2f94d52ae1a9cbaa8182bb95273cd21 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sat, 2 Mar 2024 14:10:36 +0200 Subject: [PATCH] Initial work for play queue --- src/elekf/utils/order.gleam | 21 ++- src/elekf/web/components/library_view.gleam | 61 ++++--- .../library_views/albums_view.gleam | 80 ++------ .../library_views/artists_view.gleam | 6 +- .../library_views/play_queue_view.gleam | 60 ++++++ .../library_views/single_album_view.gleam | 154 ++-------------- .../library_views/single_artist_view.gleam | 172 +++--------------- .../library_views/tracks_view.gleam | 6 +- 8 files changed, 173 insertions(+), 387 deletions(-) create mode 100644 src/elekf/web/components/library_views/play_queue_view.gleam diff --git a/src/elekf/utils/order.gleam b/src/elekf/utils/order.gleam index cc00ae3..249272a 100644 --- a/src/elekf/utils/order.gleam +++ b/src/elekf/utils/order.gleam @@ -8,14 +8,15 @@ pub type Sorter(a) = /// Compare two values by using multiple sorters. The comparison will stop after /// the first sorter that returns something other than `order.Eq`. pub fn compare_by_multiple(sorters: List(Sorter(a)), a: a, b: a) { - list.fold_until( - sorters, - order.Eq, - fn(prev, sorter) { - case prev { - order.Eq -> list.Continue(sorter(a, b)) - other -> list.Stop(other) - } - }, - ) + list.fold_until(sorters, order.Eq, fn(prev, sorter) { + case prev { + order.Eq -> list.Continue(sorter(a, b)) + other -> list.Stop(other) + } + }) +} + +/// A sorter that does not sort, keeping the order of the elements. +pub fn noop(_a, _b) { + order.Eq } diff --git a/src/elekf/web/components/library_view.gleam b/src/elekf/web/components/library_view.gleam index beeaaa6..28269be 100644 --- a/src/elekf/web/components/library_view.gleam +++ b/src/elekf/web/components/library_view.gleam @@ -27,17 +27,17 @@ import elekf/web/common import elekf/web/storage/history/storage as history_store /// Function to get the data of the view from the library. -pub type DataGetter(a) = - fn(Library) -> List(LibraryItem(a)) +pub type DataGetter(a, filter) = + fn(Library, filter) -> List(LibraryItem(a)) /// A view that renders a single item from the library. /// /// It should return a list of elements, which can be used for expanding items: /// the first element for the item itself and subsequent elements for the /// expanded contents. -pub type ItemView(a) = - fn(Model(a), List(LibraryItem(a)), Int, LibraryItem(a)) -> - List(element.Element(Msg)) +pub type ItemView(a, filter) = + fn(Model(a, filter), List(LibraryItem(a)), Int, LibraryItem(a)) -> + List(element.Element(Msg(filter))) /// 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. @@ -66,24 +66,25 @@ pub type View { Tracks } -pub type Msg { +pub type Msg(filter) { LibraryUpdated(Library) SettingsUpdated(option.Option(common.Settings)) ShuffleAll StartPlay(List(LibraryItem(Track)), Int) Search(search.Msg) - FilterUpdated + FilterUpdated(filter) ListScrolled(Float) ScrollRequested(Float) } -pub type Model(a) { +pub type Model(a, filter) { Model( id: String, + filter: filter, library: Library, library_loading: Bool, data: List(LibraryItem(a)), - data_getter: DataGetter(a), + data_getter: DataGetter(a, filter), shuffler: Shuffler(a), sorter: Sorter(a), search: search.Model, @@ -104,18 +105,30 @@ pub type Model(a) { /// and shuffle them for play. pub fn register( name: String, - data_getter: DataGetter(a), - item_view: ItemView(a), + default_filter: filter, + data_getter: DataGetter(a, filter), + item_view: ItemView(a, filter), shuffler: Shuffler(a), sorter: Sorter(a), search_filter: SearchFilter(a), + extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))), ) { lustre.component( name, - fn() { init(name, library.empty(), True, data_getter, shuffler, sorter) }, + fn() { + init( + name, + default_filter, + library.empty(), + True, + data_getter, + shuffler, + sorter, + ) + }, update, generate_view(item_view, search_filter), - generic_attributes(), + dict.merge(generic_attributes(), extra_attrs), ) } @@ -146,7 +159,7 @@ pub fn render( @external(javascript, "../../../library_view_ffi.mjs", "requestScroll") pub fn request_scroll(pos: Float) -> Nil -pub fn init(id, library, library_loading, data_getter, shuffler, sorter) { +pub fn init(id, filter, library, library_loading, data_getter, shuffler, sorter) { let scrollend_effect = effect.from(fn(dispatch) { lustre_utils.after_next_render(fn() { @@ -169,9 +182,10 @@ pub fn init(id, library, library_loading, data_getter, shuffler, sorter) { #( Model( id, + filter, library, library_loading, - data_getter(library), + data_getter(library, filter), data_getter, shuffler, sorter, @@ -202,7 +216,10 @@ pub fn update(model, msg) { #(Model(..model, search: search_model), effect.none()) } - FilterUpdated -> #(update_data(model, model.library), effect.none()) + FilterUpdated(filter) -> { + let new_model = Model(..model, filter: filter) + #(update_data(new_model, new_model.library), effect.none()) + } ListScrolled(pos) -> { let view_history = get_view_history(model.history_api) @@ -224,8 +241,8 @@ pub fn update(model, msg) { } pub fn library_view( - model: Model(a), - item_view: ItemView(a), + model: Model(a, filter), + item_view: ItemView(a, filter), search_filter: SearchFilter(a), ) { let items = case model.search.search_text { @@ -251,11 +268,11 @@ pub fn library_view( ) } -fn generate_view(item_view: ItemView(a), search_filter: SearchFilter(a)) { +fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a)) { library_view(_, item_view, search_filter) } -fn shuffle_all(model: Model(a)) { +fn shuffle_all(model: Model(a, filter)) { let tracks = model.shuffler(model.library, model.data) start_play.emit(tracks, 0) } @@ -270,11 +287,11 @@ fn settings_decode(data: dynamic.Dynamic) { Ok(SettingsUpdated(settings)) } -fn update_data(model: Model(a), library: Library) { +fn update_data(model: Model(a, filter), library: Library) { Model( ..model, data: library - |> model.data_getter() + |> model.data_getter(model.filter) |> list.sort(fn(a, b) { model.sorter(a.1, b.1) }), ) } diff --git a/src/elekf/web/components/library_views/albums_view.gleam b/src/elekf/web/components/library_views/albums_view.gleam index ba5dc37..aaa2278 100644 --- a/src/elekf/web/components/library_views/albums_view.gleam +++ b/src/elekf/web/components/library_views/albums_view.gleam @@ -4,45 +4,28 @@ import gleam/string import gleam/list import gleam/dict import gleam/option -import gleam/dynamic -import gleam/result -import lustre -import lustre/effect import lustre/attribute -import lustre/element import elekf/library.{type Library} import elekf/library/album.{type Album} import elekf/library/album_utils -import elekf/web/components/library_view +import elekf/web/components/library_view.{type Model} import elekf/web/components/library_item.{type LibraryItem} import elekf/web/components/library_views/album_item import elekf/web/common const component_name = "albums-view" -type Model { - Model(library_view: library_view.Model(Album)) -} - -type Msg { - LibraryViewMsg(library_view.Msg) -} - /// Register the albums view as a custom element. pub fn register() { - lustre.component( + library_view.register( component_name, - init, - update, - generate_view(search_filter), - library_view.generic_attributes() - |> dict.map_values(fn(_key, decoder) { - fn(data: dynamic.Dynamic) { - data - |> decoder() - |> result.map(LibraryViewMsg) - } - }), + Nil, + data_getter, + item_view, + shuffler, + album_utils.sort_by_name, + search_filter, + dict.new(), ) } @@ -55,30 +38,7 @@ pub fn render( library_view.render(component_name, library, settings, extra_attrs) } -fn init() { - let #(lib_m, lib_e) = - library_view.init( - component_name, - library.empty(), - True, - data_getter, - shuffler, - album_utils.sort_by_name, - ) - - #(Model(library_view: lib_m), effect.map(lib_e, LibraryViewMsg)) -} - -fn update(model: Model, msg) { - case msg { - LibraryViewMsg(lib_msg) -> { - let #(lib_m, lib_e) = library_view.update(model.library_view, lib_msg) - #(Model(library_view: lib_m), effect.map(lib_e, LibraryViewMsg)) - } - } -} - -fn data_getter(library: Library) { +fn data_getter(library: Library, _filter: Nil) { dict.to_list(library.albums) } @@ -93,17 +53,11 @@ fn search_filter(item: Album, search_text: String) { string.contains(item.name_lower, search_text) } -fn generate_view(search_filter: library_view.SearchFilter(Album)) { - fn(model: Model) { - library_view.library_view( - model.library_view, - fn(_library_model, _items, _index, item) { view(model, item) }, - search_filter, - ) - |> element.map(LibraryViewMsg) - } -} - -fn view(model: Model, item: LibraryItem(Album)) { - album_item.view(model.library_view.library, model.library_view.settings, item) +fn item_view( + model: Model(Album, Nil), + _items: List(LibraryItem(Album)), + _index: Int, + item: LibraryItem(Album), +) { + album_item.view(model.library, model.settings, item) } diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index 49f9db2..0e20dd7 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -24,11 +24,13 @@ const component_name = "artists-view" pub fn register() { library_view.register( component_name, + Nil, data_getter, item_view, shuffler, artist_utils.sort_by_name, search_filter, + dict.new(), ) } @@ -41,7 +43,7 @@ pub fn render( library_view.render(component_name, library, settings, extra_attrs) } -fn data_getter(library: Library) { +fn data_getter(library: Library, _filter: Nil) { library.artists |> dict.fold([], fn(acc, key, val) { case val.tracks { @@ -63,7 +65,7 @@ fn search_filter(item: Artist, search_text: String) { } fn item_view( - model: Model(Artist), + model: Model(Artist, Nil), _items: List(LibraryItem(Artist)), _index: Int, item: LibraryItem(Artist), diff --git a/src/elekf/web/components/library_views/play_queue_view.gleam b/src/elekf/web/components/library_views/play_queue_view.gleam new file mode 100644 index 0000000..51750df --- /dev/null +++ b/src/elekf/web/components/library_views/play_queue_view.gleam @@ -0,0 +1,60 @@ +//// A library view to the current play queue. + +import gleam/string +import gleam/list +import gleam/dict +import gleam/option +import lustre/attribute +import elekf/library.{type Library} +import elekf/library/track.{type Track} +import elekf/utils/order +import elekf/web/components/library_view.{type Model} as 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 = "play-queue-view" + +/// Register the play queue view as a custom element. +pub fn register() { + library_view.register( + component_name, + Nil, + data_getter, + item_view, + shuffler, + order.noop, + search_filter, + dict.new(), + ) +} + +/// Render the play queue view. +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, _filter: Nil) { + dict.to_list(library.tracks) +} + +fn shuffler(_library, items) { + list.shuffle(items) +} + +fn search_filter(item: Track, search_text: String) { + string.contains(item.title_lower, search_text) +} + +fn item_view( + model: Model(Track, Nil), + items: List(LibraryItem(Track)), + index: Int, + item: LibraryItem(Track), +) { + track_item.view(model.library, items, index, item, "track-list") +} diff --git a/src/elekf/web/components/library_views/single_album_view.gleam b/src/elekf/web/components/library_views/single_album_view.gleam index 058f58d..87c269c 100644 --- a/src/elekf/web/components/library_views/single_album_view.gleam +++ b/src/elekf/web/components/library_views/single_album_view.gleam @@ -5,15 +5,8 @@ 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 @@ -23,37 +16,17 @@ 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( + library_view.register( 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)]), - ), + library.invalid_id, + data_getter, + item_view, + shuffler, + track_utils.sort_by_track_number, + search_filter, + dict.from_list([#("album-id", id_decode)]), ) } @@ -70,67 +43,6 @@ pub fn render( ]) } -fn init() { - let #(lib_m, lib_e) = - library_view.init( - component_name, - library.empty(), - True, - fn(_) -> List(LibraryItem(Track)) { [] }, - shuffler, - track_utils.sort_by_track_number, - ) - - #( - 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) { @@ -150,52 +62,16 @@ 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), +fn item_view( + model: library_view.Model(Track, Int), + items: List(LibraryItem(Track)), + index: Int, + item: LibraryItem(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), - ]) + track_item.view(model.library, items, index, item, "album-tracks-list") } 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) + Ok(library_view.FilterUpdated(album_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 7d46aed..b33d05f 100644 --- a/src/elekf/web/components/library_views/single_artist_view.gleam +++ b/src/elekf/web/components/library_views/single_artist_view.gleam @@ -5,55 +5,28 @@ 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/album_utils -import elekf/library/artist.{type Artist} +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/album_item +import elekf/web/components/library_views/track_item import elekf/web/common const component_name = "single-artist-view" -type Model { - Model( - library_view: library_view.Model(Album), - artist_id: Int, - artist: option.Option(Artist), - ) -} - -type Msg { - LibraryViewMsg(library_view.Msg) - ArtistUpdated(Int) -} - /// Register the single artist view as a custom element. pub fn register() { - lustre.component( + library_view.register( 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([#("artist-id", id_decode)]), - ), + library.invalid_id, + data_getter, + item_view, + shuffler, + track_utils.sort_by_name, + search_filter, + dict.from_list([#("artist-id", id_decode)]), ) } @@ -70,134 +43,35 @@ pub fn render( ]) } -fn init() { - let #(lib_m, lib_e) = - library_view.init( - component_name, - library.empty(), - True, - fn(_) -> List(LibraryItem(Album)) { [] }, - shuffler, - album_utils.sort_by_year, - ) - - #( - Model( - artist_id: library.invalid_id, - artist: option.None, - library_view: lib_m, - ), - effect.map(lib_e, LibraryViewMsg), - ) -} - -fn update(model: Model, msg) { - case msg { - 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.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 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_id: Int) { - library.albums + library.tracks |> dict.fold([], fn(acc, key, val) { - case val.artist_id == artist_id { + case val.artist_id == artist_id && val.album_id == 0 { True -> [#(key, val), ..acc] False -> acc } }) } -fn shuffler(library, items: List(LibraryItem(Album))) { +fn shuffler(_library, items: List(LibraryItem(Track))) { items |> list.shuffle() - |> list.map(fn(album) { album_utils.get_tracks(library, album.1) }) - |> list.flatten() } -fn search_filter(item: Album, search_text: String) { - string.contains(item.name_lower, search_text) +fn search_filter(item: Track, search_text: String) { + string.contains(item.title_lower, search_text) } -fn generate_view(search_filter: library_view.SearchFilter(Album)) { - fn(model: Model) { - 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) - } - } -} - -fn view( - model: Model, - artist: LibraryItem(Artist), - search_filter: library_view.SearchFilter(Album), +fn item_view( + model: library_view.Model(Track, Int), + items: List(LibraryItem(Track)), + index: Int, + item: LibraryItem(Track), ) { - 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)) { - album_item.view(model.library_view.library, model.library_view.settings, item) + track_item.view(model.library, items, index, item, "artist-tracks-list") } 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) + Ok(library_view.FilterUpdated(artist_id)) } diff --git a/src/elekf/web/components/library_views/tracks_view.gleam b/src/elekf/web/components/library_views/tracks_view.gleam index f80e6f4..2b10965 100644 --- a/src/elekf/web/components/library_views/tracks_view.gleam +++ b/src/elekf/web/components/library_views/tracks_view.gleam @@ -19,11 +19,13 @@ const component_name = "tracks-view" pub fn register() { library_view.register( component_name, + Nil, data_getter, item_view, shuffler, track_utils.sort_by_name, search_filter, + dict.new(), ) } @@ -36,7 +38,7 @@ pub fn render( library_view.render(component_name, library, settings, extra_attrs) } -fn data_getter(library: Library) { +fn data_getter(library: Library, _filter: Nil) { dict.to_list(library.tracks) } @@ -49,7 +51,7 @@ fn search_filter(item: Track, search_text: String) { } fn item_view( - model: Model(Track), + model: Model(Track, Nil), items: List(LibraryItem(Track)), index: Int, item: LibraryItem(Track),