Prevent crash when library is not yet loaded

This commit is contained in:
Mikko Ahlroth 2024-03-02 21:19:33 +02:00
parent 33f8cd92f2
commit 4f869df09f
8 changed files with 143 additions and 99 deletions

View file

@ -287,34 +287,44 @@ pub fn view(model: Model) {
]), ]),
case model.view { case model.view {
library_view.Tracks -> library_view.Tracks ->
tracks_view.render(model.library, model.settings, [ tracks_view.render(library_if_loaded(model), model.settings, [
attribute.id("tracks-view"), attribute.id("tracks-view"),
attribute.class("glass-bg"), attribute.class("glass-bg"),
start_play.on(StartPlay), start_play.on(StartPlay),
]) ])
library_view.Artists -> library_view.Artists ->
artists_view.render(model.library, model.settings, [ artists_view.render(library_if_loaded(model), model.settings, [
attribute.id("artists-view"), attribute.id("artists-view"),
attribute.class("glass-bg"), attribute.class("glass-bg"),
]) ])
library_view.Albums -> library_view.Albums ->
albums_view.render(model.library, model.settings, [ albums_view.render(library_if_loaded(model), model.settings, [
attribute.id("albums-view"), attribute.id("albums-view"),
attribute.class("glass-bg"), attribute.class("glass-bg"),
start_play.on(StartPlay), start_play.on(StartPlay),
]) ])
library_view.SingleArtist(id) -> library_view.SingleArtist(id) ->
single_artist_view.render(model.library, id, model.settings, [ single_artist_view.render(
attribute.id("single-artist-view"), library_if_loaded(model),
attribute.class("glass-bg"), id,
start_play.on(StartPlay), model.settings,
]) [
attribute.id("single-artist-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
],
)
library_view.SingleAlbum(id) -> library_view.SingleAlbum(id) ->
single_album_view.render(model.library, id, model.settings, [ single_album_view.render(
attribute.id("single-album-view"), library_if_loaded(model),
attribute.class("glass-bg"), id,
start_play.on(StartPlay), model.settings,
]) [
attribute.id("single-album-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
],
)
}, },
]), ]),
case model.play_status { case model.play_status {
@ -374,6 +384,13 @@ fn if_player(
} }
} }
fn library_if_loaded(model: Model) {
case model.loading_library {
True -> option.None
False -> option.Some(model.library)
}
}
fn load_library(model: Model) { fn load_library(model: Model) {
use dispatch <- effect.from() use dispatch <- effect.from()

View file

@ -10,7 +10,7 @@ import gleam/string
import gleam/result import gleam/result
import lustre import lustre
import lustre/effect import lustre/effect
import lustre/element import lustre/element.{text}
import lustre/element/html.{div} import lustre/element/html.{div}
import lustre/attribute import lustre/attribute
import lustre/event import lustre/event
@ -36,9 +36,26 @@ pub type DataGetter(a, filter) =
/// 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, filter) = pub type ItemView(a, filter) =
fn(Model(a, filter), List(LibraryItem(a)), Int, LibraryItem(a)) -> fn(Model(a, filter), Library, List(LibraryItem(a)), Int, LibraryItem(a)) ->
List(element.Element(Msg(filter))) List(element.Element(Msg(filter)))
/// A filter to restrict the items to be shown.
pub type Filter(filter) {
/// The filter has not yet been obtained as the component is initializing. No
/// data fetches should be done yet.
FilterNotLoaded
Filter(filter)
}
pub type LibraryAcquired
pub type LibraryNotAcquired
pub opaque type LibraryStatus {
HaveLibrary(library: Library)
NoLibrary
}
/// 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.
/// ///
@ -80,9 +97,8 @@ pub type Msg(filter) {
pub type Model(a, filter) { pub type Model(a, filter) {
Model( Model(
id: String, id: String,
filter: filter, filter: Filter(filter),
library: Library, library_status: LibraryStatus,
library_loading: Bool,
data: List(LibraryItem(a)), data: List(LibraryItem(a)),
data_getter: DataGetter(a, filter), data_getter: DataGetter(a, filter),
shuffler: Shuffler(a), shuffler: Shuffler(a),
@ -105,7 +121,6 @@ pub type Model(a, filter) {
/// and shuffle them for play. /// and shuffle them for play.
pub fn register( pub fn register(
name: String, name: String,
default_filter: filter,
data_getter: DataGetter(a, filter), data_getter: DataGetter(a, filter),
item_view: ItemView(a, filter), item_view: ItemView(a, filter),
shuffler: Shuffler(a), shuffler: Shuffler(a),
@ -115,17 +130,7 @@ pub fn register(
) { ) {
lustre.component( lustre.component(
name, name,
fn() { fn() { init(name, FilterNotLoaded, data_getter, shuffler, sorter) },
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),
dict.merge(generic_attributes(), extra_attrs), dict.merge(generic_attributes(), extra_attrs),
@ -141,7 +146,7 @@ pub fn generic_attributes() {
/// Render the component using a custom element. /// Render the component using a custom element.
pub fn render( pub fn render(
name: String, name: String,
library: Library, library: option.Option(Library),
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
@ -159,7 +164,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, filter, library, library_loading, data_getter, shuffler, sorter) { pub fn init(id, filter, 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() {
@ -183,9 +188,8 @@ pub fn init(id, filter, library, library_loading, data_getter, shuffler, sorter)
Model( Model(
id, id,
filter, filter,
library, NoLibrary,
library_loading, [],
data_getter(library, filter),
data_getter, data_getter,
shuffler, shuffler,
sorter, sorter,
@ -198,11 +202,10 @@ pub fn init(id, filter, library, library_loading, data_getter, shuffler, sorter)
) )
} }
pub fn update(model, msg) { pub fn update(model: Model(a, filter), msg) {
case msg { case msg {
LibraryUpdated(library) -> #( LibraryUpdated(library) -> #(
Model(..model, library: library, library_loading: False) update_data(Model(..model, library_status: new_library(library))),
|> update_data(library),
effect.none(), effect.none(),
) )
SettingsUpdated(settings) -> #( SettingsUpdated(settings) -> #(
@ -217,8 +220,8 @@ pub fn update(model, msg) {
} }
FilterUpdated(filter) -> { FilterUpdated(filter) -> {
let new_model = Model(..model, filter: filter) let new_model = Model(..model, filter: Filter(filter))
#(update_data(new_model, new_model.library), effect.none()) #(update_data(new_model), effect.none())
} }
ListScrolled(pos) -> { ListScrolled(pos) -> {
@ -245,27 +248,38 @@ pub fn library_view(
item_view: ItemView(a, filter), item_view: ItemView(a, filter),
search_filter: SearchFilter(a), search_filter: SearchFilter(a),
) { ) {
let items = case model.search.search_text { case model.library_status, model.filter == FilterNotLoaded {
"" -> model.data HaveLibrary(lib), False -> {
txt -> { let items = case model.search.search_text {
let search_txt = string.lowercase(txt) "" -> model.data
model.data txt -> {
|> list.filter(fn(item) { search_filter(item.1, search_txt) }) let search_txt = string.lowercase(txt)
} model.data
} |> list.filter(fn(item) { search_filter(item.1, search_txt) })
}
}
div( div(
[attribute.id("library-list"), scroll_to.on(ScrollRequested)], [attribute.id("library-list"), scroll_to.on(ScrollRequested)],
list.append( list.append(
[ [
search.view(model.search) search.view(model.search)
|> element.map(Search), |> element.map(Search),
shuffle_all.view([event.on_click(ShuffleAll)]), shuffle_all.view([event.on_click(ShuffleAll)]),
], ],
list.index_map(items, fn(item, i) { item_view(model, items, i, item) }) list.index_map(items, fn(item, i) {
|> list.flatten(), item_view(model, lib, items, i, item)
), })
) |> list.flatten(),
),
)
}
_, _ ->
div(
[attribute.id("library-list"), attribute.class("library-list-loading")],
[text("")],
)
}
} }
fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a)) { fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a)) {
@ -273,13 +287,21 @@ fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a))
} }
fn shuffle_all(model: Model(a, filter)) { fn shuffle_all(model: Model(a, filter)) {
let tracks = model.shuffler(model.library, model.data) case model.library_status {
start_play.emit(tracks, 0) HaveLibrary(lib) -> {
let tracks = model.shuffler(lib, model.data)
start_play.emit(tracks, 0)
}
NoLibrary -> effect.none()
}
} }
fn library_decode(data: dynamic.Dynamic) { fn library_decode(data: dynamic.Dynamic) {
let library: Library = dynamic.unsafe_coerce(data) let library: option.Option(Library) = dynamic.unsafe_coerce(data)
Ok(LibraryUpdated(library)) case library {
option.Some(lib) -> Ok(LibraryUpdated(lib))
option.None -> Error([])
}
} }
fn settings_decode(data: dynamic.Dynamic) { fn settings_decode(data: dynamic.Dynamic) {
@ -287,13 +309,17 @@ fn settings_decode(data: dynamic.Dynamic) {
Ok(SettingsUpdated(settings)) Ok(SettingsUpdated(settings))
} }
fn update_data(model: Model(a, filter), library: Library) { fn update_data(model: Model(a, filter)) {
Model( case model.library_status, model.filter {
..model, HaveLibrary(l), Filter(f) ->
data: library Model(
|> model.data_getter(model.filter) ..model,
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }), data: l
) |> model.data_getter(f)
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
)
_, _ -> model
}
} }
fn get_view_history(history_api) { fn get_view_history(history_api) {
@ -305,3 +331,7 @@ fn add_scrollend_listener(callback: fn(Float) -> Nil) -> Nil
@external(javascript, "../../../library_view_ffi.mjs", "scrollTo") @external(javascript, "../../../library_view_ffi.mjs", "scrollTo")
fn scroll_to(pos: Float) -> Nil fn scroll_to(pos: Float) -> Nil
fn new_library(library: Library) -> LibraryStatus {
HaveLibrary(library)
}

View file

@ -19,7 +19,6 @@ const component_name = "albums-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,
@ -31,7 +30,7 @@ pub fn register() {
/// Render the albums view. /// Render the albums view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
@ -55,9 +54,10 @@ fn search_filter(item: Album, search_text: String) {
fn item_view( fn item_view(
model: Model(Album, Nil), model: Model(Album, Nil),
library: Library,
_items: List(LibraryItem(Album)), _items: List(LibraryItem(Album)),
_index: Int, _index: Int,
item: LibraryItem(Album), item: LibraryItem(Album),
) { ) {
album_item.view(model.library, model.settings, item) album_item.view(library, model.settings, item)
} }

View file

@ -24,7 +24,6 @@ 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,
@ -36,7 +35,7 @@ pub fn register() {
/// Render the artists view. /// Render the artists view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
@ -66,6 +65,7 @@ fn search_filter(item: Artist, search_text: String) {
fn item_view( fn item_view(
model: Model(Artist, Nil), model: Model(Artist, Nil),
_library: Library,
_items: List(LibraryItem(Artist)), _items: List(LibraryItem(Artist)),
_index: Int, _index: Int,
item: LibraryItem(Artist), item: LibraryItem(Artist),

View file

@ -19,7 +19,6 @@ const component_name = "play-queue-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,
@ -31,7 +30,7 @@ pub fn register() {
/// Render the play queue view. /// Render the play queue view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
@ -51,10 +50,11 @@ fn search_filter(item: Track, search_text: String) {
} }
fn item_view( fn item_view(
model: Model(Track, Nil), _model: Model(Track, Nil),
library: Library,
items: List(LibraryItem(Track)), items: List(LibraryItem(Track)),
index: Int, index: Int,
item: LibraryItem(Track), item: LibraryItem(Track),
) { ) {
track_item.view(model.library, items, index, item, "track-list") track_item.view(library, items, index, item, "track-list")
} }

View file

@ -20,7 +20,6 @@ const component_name = "single-album-view"
pub fn register() { pub fn register() {
library_view.register( library_view.register(
component_name, component_name,
library.invalid_id,
data_getter, data_getter,
item_view, item_view,
shuffler, shuffler,
@ -32,7 +31,7 @@ pub fn register() {
/// Render the single album view. /// Render the single album view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
album_id: Int, album_id: Int,
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
@ -63,12 +62,13 @@ fn search_filter(item: Track, search_text: String) {
} }
fn item_view( fn item_view(
model: library_view.Model(Track, Int), _model: library_view.Model(Track, Int),
library: Library,
items: List(LibraryItem(Track)), items: List(LibraryItem(Track)),
index: Int, index: Int,
item: LibraryItem(Track), item: LibraryItem(Track),
) { ) {
track_item.view(model.library, items, index, item, "album-tracks-list") track_item.view(library, items, index, item, "album-tracks-list")
} }
fn id_decode(data: dynamic.Dynamic) { fn id_decode(data: dynamic.Dynamic) {

View file

@ -1,4 +1,4 @@
//// A library view to a single artist's albums. //// A library view to a single artist's albums and non-album tracks.
import gleam/string import gleam/string
import gleam/list import gleam/list
@ -20,7 +20,6 @@ const component_name = "single-artist-view"
pub fn register() { pub fn register() {
library_view.register( library_view.register(
component_name, component_name,
library.invalid_id,
data_getter, data_getter,
item_view, item_view,
shuffler, shuffler,
@ -32,7 +31,7 @@ pub fn register() {
/// Render the single artist view. /// Render the single artist view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
artist_id: Int, artist_id: Int,
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
@ -44,12 +43,9 @@ pub fn render(
} }
fn data_getter(library: Library, artist_id: Int) { fn data_getter(library: Library, artist_id: Int) {
library.tracks let artist = library.assert_artist(library, artist_id)
|> dict.fold([], fn(acc, key, val) { list.map(artist.tracks, fn(track_id) {
case val.artist_id == artist_id && val.album_id == 0 { #(track_id, library.assert_track(library, track_id))
True -> [#(key, val), ..acc]
False -> acc
}
}) })
} }
@ -63,12 +59,13 @@ fn search_filter(item: Track, search_text: String) {
} }
fn item_view( fn item_view(
model: library_view.Model(Track, Int), _model: library_view.Model(Track, Int),
library: Library,
items: List(LibraryItem(Track)), items: List(LibraryItem(Track)),
index: Int, index: Int,
item: LibraryItem(Track), item: LibraryItem(Track),
) { ) {
track_item.view(model.library, items, index, item, "artist-tracks-list") track_item.view(library, items, index, item, "artist-tracks-list")
} }
fn id_decode(data: dynamic.Dynamic) { fn id_decode(data: dynamic.Dynamic) {

View file

@ -19,7 +19,6 @@ 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,
@ -31,7 +30,7 @@ pub fn register() {
/// Render the tracks view. /// Render the tracks view.
pub fn render( pub fn render(
library: Library, library: option.Option(Library),
settings: option.Option(common.Settings), settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)), extra_attrs: List(attribute.Attribute(msg)),
) { ) {
@ -51,10 +50,11 @@ fn search_filter(item: Track, search_text: String) {
} }
fn item_view( fn item_view(
model: Model(Track, Nil), _model: Model(Track, Nil),
library: Library,
items: List(LibraryItem(Track)), items: List(LibraryItem(Track)),
index: Int, index: Int,
item: LibraryItem(Track), item: LibraryItem(Track),
) { ) {
track_item.view(model.library, items, index, item, "track-list") track_item.view(library, items, index, item, "track-list")
} }