Initial work for play queue
This commit is contained in:
parent
5b8f4fdd2d
commit
33f8cd92f2
8 changed files with 173 additions and 387 deletions
|
@ -8,14 +8,15 @@ pub type Sorter(a) =
|
||||||
/// Compare two values by using multiple sorters. The comparison will stop after
|
/// Compare two values by using multiple sorters. The comparison will stop after
|
||||||
/// the first sorter that returns something other than `order.Eq`.
|
/// the first sorter that returns something other than `order.Eq`.
|
||||||
pub fn compare_by_multiple(sorters: List(Sorter(a)), a: a, b: a) {
|
pub fn compare_by_multiple(sorters: List(Sorter(a)), a: a, b: a) {
|
||||||
list.fold_until(
|
list.fold_until(sorters, order.Eq, fn(prev, sorter) {
|
||||||
sorters,
|
|
||||||
order.Eq,
|
|
||||||
fn(prev, sorter) {
|
|
||||||
case prev {
|
case prev {
|
||||||
order.Eq -> list.Continue(sorter(a, b))
|
order.Eq -> list.Continue(sorter(a, b))
|
||||||
other -> list.Stop(other)
|
other -> list.Stop(other)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
}
|
||||||
|
|
||||||
|
/// A sorter that does not sort, keeping the order of the elements.
|
||||||
|
pub fn noop(_a, _b) {
|
||||||
|
order.Eq
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,17 +27,17 @@ import elekf/web/common
|
||||||
import elekf/web/storage/history/storage as history_store
|
import elekf/web/storage/history/storage as history_store
|
||||||
|
|
||||||
/// Function to get the data of the view from the library.
|
/// Function to get the data of the view from the library.
|
||||||
pub type DataGetter(a) =
|
pub type DataGetter(a, filter) =
|
||||||
fn(Library) -> List(LibraryItem(a))
|
fn(Library, filter) -> List(LibraryItem(a))
|
||||||
|
|
||||||
/// A view that renders a single item from the library.
|
/// A view that renders a single item from the library.
|
||||||
///
|
///
|
||||||
/// It should return a list of elements, which can be used for expanding items:
|
/// 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
|
/// the first element for the item itself and subsequent elements for the
|
||||||
/// expanded contents.
|
/// expanded contents.
|
||||||
pub type ItemView(a) =
|
pub type ItemView(a, filter) =
|
||||||
fn(Model(a), List(LibraryItem(a)), Int, LibraryItem(a)) ->
|
fn(Model(a, filter), List(LibraryItem(a)), Int, LibraryItem(a)) ->
|
||||||
List(element.Element(Msg))
|
List(element.Element(Msg(filter)))
|
||||||
|
|
||||||
/// 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.
|
||||||
|
@ -66,24 +66,25 @@ pub type View {
|
||||||
Tracks
|
Tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg(filter) {
|
||||||
LibraryUpdated(Library)
|
LibraryUpdated(Library)
|
||||||
SettingsUpdated(option.Option(common.Settings))
|
SettingsUpdated(option.Option(common.Settings))
|
||||||
ShuffleAll
|
ShuffleAll
|
||||||
StartPlay(List(LibraryItem(Track)), Int)
|
StartPlay(List(LibraryItem(Track)), Int)
|
||||||
Search(search.Msg)
|
Search(search.Msg)
|
||||||
FilterUpdated
|
FilterUpdated(filter)
|
||||||
ListScrolled(Float)
|
ListScrolled(Float)
|
||||||
ScrollRequested(Float)
|
ScrollRequested(Float)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Model(a) {
|
pub type Model(a, filter) {
|
||||||
Model(
|
Model(
|
||||||
id: String,
|
id: String,
|
||||||
|
filter: filter,
|
||||||
library: Library,
|
library: Library,
|
||||||
library_loading: Bool,
|
library_loading: Bool,
|
||||||
data: List(LibraryItem(a)),
|
data: List(LibraryItem(a)),
|
||||||
data_getter: DataGetter(a),
|
data_getter: DataGetter(a, filter),
|
||||||
shuffler: Shuffler(a),
|
shuffler: Shuffler(a),
|
||||||
sorter: Sorter(a),
|
sorter: Sorter(a),
|
||||||
search: search.Model,
|
search: search.Model,
|
||||||
|
@ -104,18 +105,30 @@ pub type Model(a) {
|
||||||
/// and shuffle them for play.
|
/// and shuffle them for play.
|
||||||
pub fn register(
|
pub fn register(
|
||||||
name: String,
|
name: String,
|
||||||
data_getter: DataGetter(a),
|
default_filter: filter,
|
||||||
item_view: ItemView(a),
|
data_getter: DataGetter(a, filter),
|
||||||
|
item_view: ItemView(a, filter),
|
||||||
shuffler: Shuffler(a),
|
shuffler: Shuffler(a),
|
||||||
sorter: Sorter(a),
|
sorter: Sorter(a),
|
||||||
search_filter: SearchFilter(a),
|
search_filter: SearchFilter(a),
|
||||||
|
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
||||||
) {
|
) {
|
||||||
lustre.component(
|
lustre.component(
|
||||||
name,
|
name,
|
||||||
fn() { init(name, library.empty(), True, data_getter, shuffler, sorter) },
|
fn() {
|
||||||
|
init(
|
||||||
|
name,
|
||||||
|
default_filter,
|
||||||
|
library.empty(),
|
||||||
|
True,
|
||||||
|
data_getter,
|
||||||
|
shuffler,
|
||||||
|
sorter,
|
||||||
|
)
|
||||||
|
},
|
||||||
update,
|
update,
|
||||||
generate_view(item_view, search_filter),
|
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")
|
@external(javascript, "../../../library_view_ffi.mjs", "requestScroll")
|
||||||
pub fn request_scroll(pos: Float) -> Nil
|
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 =
|
let scrollend_effect =
|
||||||
effect.from(fn(dispatch) {
|
effect.from(fn(dispatch) {
|
||||||
lustre_utils.after_next_render(fn() {
|
lustre_utils.after_next_render(fn() {
|
||||||
|
@ -169,9 +182,10 @@ pub fn init(id, library, library_loading, data_getter, shuffler, sorter) {
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
id,
|
id,
|
||||||
|
filter,
|
||||||
library,
|
library,
|
||||||
library_loading,
|
library_loading,
|
||||||
data_getter(library),
|
data_getter(library, filter),
|
||||||
data_getter,
|
data_getter,
|
||||||
shuffler,
|
shuffler,
|
||||||
sorter,
|
sorter,
|
||||||
|
@ -202,7 +216,10 @@ pub fn update(model, msg) {
|
||||||
#(Model(..model, search: search_model), effect.none())
|
#(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) -> {
|
ListScrolled(pos) -> {
|
||||||
let view_history = get_view_history(model.history_api)
|
let view_history = get_view_history(model.history_api)
|
||||||
|
@ -224,8 +241,8 @@ pub fn update(model, msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn library_view(
|
pub fn library_view(
|
||||||
model: Model(a),
|
model: Model(a, filter),
|
||||||
item_view: ItemView(a),
|
item_view: ItemView(a, filter),
|
||||||
search_filter: SearchFilter(a),
|
search_filter: SearchFilter(a),
|
||||||
) {
|
) {
|
||||||
let items = case model.search.search_text {
|
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)
|
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)
|
let tracks = model.shuffler(model.library, model.data)
|
||||||
start_play.emit(tracks, 0)
|
start_play.emit(tracks, 0)
|
||||||
}
|
}
|
||||||
|
@ -270,11 +287,11 @@ fn settings_decode(data: dynamic.Dynamic) {
|
||||||
Ok(SettingsUpdated(settings))
|
Ok(SettingsUpdated(settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_data(model: Model(a), library: Library) {
|
fn update_data(model: Model(a, filter), library: Library) {
|
||||||
Model(
|
Model(
|
||||||
..model,
|
..model,
|
||||||
data: library
|
data: library
|
||||||
|> model.data_getter()
|
|> model.data_getter(model.filter)
|
||||||
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,45 +4,28 @@ import gleam/string
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/dynamic
|
|
||||||
import gleam/result
|
|
||||||
import lustre
|
|
||||||
import lustre/effect
|
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/element
|
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/album.{type Album}
|
import elekf/library/album.{type Album}
|
||||||
import elekf/library/album_utils
|
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_item.{type LibraryItem}
|
||||||
import elekf/web/components/library_views/album_item
|
import elekf/web/components/library_views/album_item
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
|
|
||||||
const component_name = "albums-view"
|
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.
|
/// Register the albums view as a custom element.
|
||||||
pub fn register() {
|
pub fn register() {
|
||||||
lustre.component(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
init,
|
Nil,
|
||||||
update,
|
data_getter,
|
||||||
generate_view(search_filter),
|
item_view,
|
||||||
library_view.generic_attributes()
|
shuffler,
|
||||||
|> dict.map_values(fn(_key, decoder) {
|
album_utils.sort_by_name,
|
||||||
fn(data: dynamic.Dynamic) {
|
search_filter,
|
||||||
data
|
dict.new(),
|
||||||
|> decoder()
|
|
||||||
|> result.map(LibraryViewMsg)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,30 +38,7 @@ pub fn render(
|
||||||
library_view.render(component_name, library, settings, extra_attrs)
|
library_view.render(component_name, library, settings, extra_attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init() {
|
fn data_getter(library: Library, _filter: Nil) {
|
||||||
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) {
|
|
||||||
dict.to_list(library.albums)
|
dict.to_list(library.albums)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,17 +53,11 @@ fn search_filter(item: Album, search_text: String) {
|
||||||
string.contains(item.name_lower, search_text)
|
string.contains(item.name_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_view(search_filter: library_view.SearchFilter(Album)) {
|
fn item_view(
|
||||||
fn(model: Model) {
|
model: Model(Album, Nil),
|
||||||
library_view.library_view(
|
_items: List(LibraryItem(Album)),
|
||||||
model.library_view,
|
_index: Int,
|
||||||
fn(_library_model, _items, _index, item) { view(model, item) },
|
item: LibraryItem(Album),
|
||||||
search_filter,
|
) {
|
||||||
)
|
album_item.view(model.library, model.settings, item)
|
||||||
|> element.map(LibraryViewMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(model: Model, item: LibraryItem(Album)) {
|
|
||||||
album_item.view(model.library_view.library, model.library_view.settings, item)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,13 @@ const component_name = "artists-view"
|
||||||
pub fn register() {
|
pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
|
Nil,
|
||||||
data_getter,
|
data_getter,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
artist_utils.sort_by_name,
|
artist_utils.sort_by_name,
|
||||||
search_filter,
|
search_filter,
|
||||||
|
dict.new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ pub fn render(
|
||||||
library_view.render(component_name, library, settings, extra_attrs)
|
library_view.render(component_name, library, settings, extra_attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_getter(library: Library) {
|
fn data_getter(library: Library, _filter: Nil) {
|
||||||
library.artists
|
library.artists
|
||||||
|> dict.fold([], fn(acc, key, val) {
|
|> dict.fold([], fn(acc, key, val) {
|
||||||
case val.tracks {
|
case val.tracks {
|
||||||
|
@ -63,7 +65,7 @@ fn search_filter(item: Artist, search_text: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
model: Model(Artist),
|
model: Model(Artist, Nil),
|
||||||
_items: List(LibraryItem(Artist)),
|
_items: List(LibraryItem(Artist)),
|
||||||
_index: Int,
|
_index: Int,
|
||||||
item: LibraryItem(Artist),
|
item: LibraryItem(Artist),
|
||||||
|
|
60
src/elekf/web/components/library_views/play_queue_view.gleam
Normal file
60
src/elekf/web/components/library_views/play_queue_view.gleam
Normal file
|
@ -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")
|
||||||
|
}
|
|
@ -5,15 +5,8 @@ import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/dynamic
|
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/attribute
|
||||||
import lustre/effect
|
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/album.{type Album}
|
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
import elekf/library/track_utils
|
||||||
import elekf/web/components/library_view
|
import elekf/web/components/library_view
|
||||||
|
@ -23,37 +16,17 @@ import elekf/web/common
|
||||||
|
|
||||||
const component_name = "single-album-view"
|
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.
|
/// Register the single album view as a custom element.
|
||||||
pub fn register() {
|
pub fn register() {
|
||||||
lustre.component(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
init,
|
library.invalid_id,
|
||||||
update,
|
data_getter,
|
||||||
generate_view(search_filter),
|
item_view,
|
||||||
dict.merge(
|
shuffler,
|
||||||
library_view.generic_attributes()
|
track_utils.sort_by_track_number,
|
||||||
|> dict.map_values(fn(_key, decoder) {
|
search_filter,
|
||||||
fn(data: dynamic.Dynamic) {
|
|
||||||
data
|
|
||||||
|> decoder()
|
|
||||||
|> result.map(LibraryViewMsg)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
dict.from_list([#("album-id", id_decode)]),
|
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) {
|
fn data_getter(library: Library, album_id: Int) {
|
||||||
library.tracks
|
library.tracks
|
||||||
|> dict.fold([], fn(acc, key, val) {
|
|> 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)
|
string.contains(item.title_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_view(search_filter: library_view.SearchFilter(Track)) {
|
fn item_view(
|
||||||
fn(model: Model) {
|
model: library_view.Model(Track, Int),
|
||||||
case model.library_view.library_loading, model.album {
|
items: List(LibraryItem(Track)),
|
||||||
True, _ ->
|
index: Int,
|
||||||
div([attribute.class("library-view-loading")], [
|
item: LibraryItem(Track),
|
||||||
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([], [
|
track_item.view(model.library, items, index, item, "album-tracks-list")
|
||||||
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) {
|
fn id_decode(data: dynamic.Dynamic) {
|
||||||
let album_id: Int = dynamic.unsafe_coerce(data)
|
let album_id: Int = dynamic.unsafe_coerce(data)
|
||||||
Ok(AlbumUpdated(album_id))
|
Ok(library_view.FilterUpdated(album_id))
|
||||||
}
|
|
||||||
|
|
||||||
fn load_album(library: Library, id: Int) {
|
|
||||||
library.get_album(library, id)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,55 +5,28 @@ import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/dynamic
|
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/attribute
|
||||||
import lustre/effect
|
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/album.{type Album}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/album_utils
|
import elekf/library/track_utils
|
||||||
import elekf/library/artist.{type Artist}
|
|
||||||
import elekf/web/components/library_view
|
import elekf/web/components/library_view
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
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
|
import elekf/web/common
|
||||||
|
|
||||||
const component_name = "single-artist-view"
|
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.
|
/// Register the single artist view as a custom element.
|
||||||
pub fn register() {
|
pub fn register() {
|
||||||
lustre.component(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
init,
|
library.invalid_id,
|
||||||
update,
|
data_getter,
|
||||||
generate_view(search_filter),
|
item_view,
|
||||||
dict.merge(
|
shuffler,
|
||||||
library_view.generic_attributes()
|
track_utils.sort_by_name,
|
||||||
|> dict.map_values(fn(_key, decoder) {
|
search_filter,
|
||||||
fn(data: dynamic.Dynamic) {
|
|
||||||
data
|
|
||||||
|> decoder()
|
|
||||||
|> result.map(LibraryViewMsg)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
dict.from_list([#("artist-id", id_decode)]),
|
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) {
|
fn data_getter(library: Library, artist_id: Int) {
|
||||||
library.albums
|
library.tracks
|
||||||
|> dict.fold([], fn(acc, key, val) {
|
|> 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]
|
True -> [#(key, val), ..acc]
|
||||||
False -> acc
|
False -> acc
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shuffler(library, items: List(LibraryItem(Album))) {
|
fn shuffler(_library, items: List(LibraryItem(Track))) {
|
||||||
items
|
items
|
||||||
|> list.shuffle()
|
|> list.shuffle()
|
||||||
|> list.map(fn(album) { album_utils.get_tracks(library, album.1) })
|
|
||||||
|> list.flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_filter(item: Album, search_text: String) {
|
fn search_filter(item: Track, search_text: String) {
|
||||||
string.contains(item.name_lower, search_text)
|
string.contains(item.title_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_view(search_filter: library_view.SearchFilter(Album)) {
|
fn item_view(
|
||||||
fn(model: Model) {
|
model: library_view.Model(Track, Int),
|
||||||
case model.library_view.library_loading, model.artist {
|
items: List(LibraryItem(Track)),
|
||||||
True, _ ->
|
index: Int,
|
||||||
div([attribute.class("library-view-loading")], [
|
item: LibraryItem(Track),
|
||||||
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),
|
|
||||||
) {
|
) {
|
||||||
div([], [
|
track_item.view(model.library, items, index, item, "artist-tracks-list")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn id_decode(data: dynamic.Dynamic) {
|
fn id_decode(data: dynamic.Dynamic) {
|
||||||
let artist_id: Int = dynamic.unsafe_coerce(data)
|
let artist_id: Int = dynamic.unsafe_coerce(data)
|
||||||
Ok(ArtistUpdated(artist_id))
|
Ok(library_view.FilterUpdated(artist_id))
|
||||||
}
|
|
||||||
|
|
||||||
fn load_artist(library: Library, id: Int) {
|
|
||||||
library.get_artist(library, id)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,13 @@ const component_name = "tracks-view"
|
||||||
pub fn register() {
|
pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
|
Nil,
|
||||||
data_getter,
|
data_getter,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
track_utils.sort_by_name,
|
track_utils.sort_by_name,
|
||||||
search_filter,
|
search_filter,
|
||||||
|
dict.new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +38,7 @@ pub fn render(
|
||||||
library_view.render(component_name, library, settings, extra_attrs)
|
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)
|
dict.to_list(library.tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +51,7 @@ fn search_filter(item: Track, search_text: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
model: Model(Track),
|
model: Model(Track, Nil),
|
||||||
items: List(LibraryItem(Track)),
|
items: List(LibraryItem(Track)),
|
||||||
index: Int,
|
index: Int,
|
||||||
item: LibraryItem(Track),
|
item: LibraryItem(Track),
|
||||||
|
|
Loading…
Reference in a new issue