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 {
library_view.Tracks ->
tracks_view.render(model.library, model.settings, [
tracks_view.render(library_if_loaded(model), model.settings, [
attribute.id("tracks-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
library_view.Artists ->
artists_view.render(model.library, model.settings, [
artists_view.render(library_if_loaded(model), model.settings, [
attribute.id("artists-view"),
attribute.class("glass-bg"),
])
library_view.Albums ->
albums_view.render(model.library, model.settings, [
albums_view.render(library_if_loaded(model), model.settings, [
attribute.id("albums-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
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),
])
single_artist_view.render(
library_if_loaded(model),
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),
])
single_album_view.render(
library_if_loaded(model),
id,
model.settings,
[
attribute.id("single-album-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
],
)
},
]),
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) {
use dispatch <- effect.from()

View file

@ -10,7 +10,7 @@ import gleam/string
import gleam/result
import lustre
import lustre/effect
import lustre/element
import lustre/element.{text}
import lustre/element/html.{div}
import lustre/attribute
import lustre/event
@ -36,9 +36,26 @@ pub type DataGetter(a, filter) =
/// the first element for the item itself and subsequent elements for the
/// expanded contents.
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)))
/// 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
/// `True` if the item should be shown and `False` if not.
///
@ -80,9 +97,8 @@ pub type Msg(filter) {
pub type Model(a, filter) {
Model(
id: String,
filter: filter,
library: Library,
library_loading: Bool,
filter: Filter(filter),
library_status: LibraryStatus,
data: List(LibraryItem(a)),
data_getter: DataGetter(a, filter),
shuffler: Shuffler(a),
@ -105,7 +121,6 @@ pub type Model(a, filter) {
/// and shuffle them for play.
pub fn register(
name: String,
default_filter: filter,
data_getter: DataGetter(a, filter),
item_view: ItemView(a, filter),
shuffler: Shuffler(a),
@ -115,17 +130,7 @@ pub fn register(
) {
lustre.component(
name,
fn() {
init(
name,
default_filter,
library.empty(),
True,
data_getter,
shuffler,
sorter,
)
},
fn() { init(name, FilterNotLoaded, data_getter, shuffler, sorter) },
update,
generate_view(item_view, search_filter),
dict.merge(generic_attributes(), extra_attrs),
@ -141,7 +146,7 @@ pub fn generic_attributes() {
/// Render the component using a custom element.
pub fn render(
name: String,
library: Library,
library: option.Option(Library),
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
@ -159,7 +164,7 @@ pub fn render(
@external(javascript, "../../../library_view_ffi.mjs", "requestScroll")
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 =
effect.from(fn(dispatch) {
lustre_utils.after_next_render(fn() {
@ -183,9 +188,8 @@ pub fn init(id, filter, library, library_loading, data_getter, shuffler, sorter)
Model(
id,
filter,
library,
library_loading,
data_getter(library, filter),
NoLibrary,
[],
data_getter,
shuffler,
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 {
LibraryUpdated(library) -> #(
Model(..model, library: library, library_loading: False)
|> update_data(library),
update_data(Model(..model, library_status: new_library(library))),
effect.none(),
)
SettingsUpdated(settings) -> #(
@ -217,8 +220,8 @@ pub fn update(model, msg) {
}
FilterUpdated(filter) -> {
let new_model = Model(..model, filter: filter)
#(update_data(new_model, new_model.library), effect.none())
let new_model = Model(..model, filter: Filter(filter))
#(update_data(new_model), effect.none())
}
ListScrolled(pos) -> {
@ -245,27 +248,38 @@ pub fn library_view(
item_view: ItemView(a, filter),
search_filter: SearchFilter(a),
) {
let items = case model.search.search_text {
"" -> model.data
txt -> {
let search_txt = string.lowercase(txt)
model.data
|> list.filter(fn(item) { search_filter(item.1, search_txt) })
}
}
case model.library_status, model.filter == FilterNotLoaded {
HaveLibrary(lib), False -> {
let items = case model.search.search_text {
"" -> model.data
txt -> {
let search_txt = string.lowercase(txt)
model.data
|> list.filter(fn(item) { search_filter(item.1, search_txt) })
}
}
div(
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
list.append(
[
search.view(model.search)
|> element.map(Search),
shuffle_all.view([event.on_click(ShuffleAll)]),
],
list.index_map(items, fn(item, i) { item_view(model, items, i, item) })
|> list.flatten(),
),
)
div(
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
list.append(
[
search.view(model.search)
|> element.map(Search),
shuffle_all.view([event.on_click(ShuffleAll)]),
],
list.index_map(items, fn(item, i) {
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)) {
@ -273,13 +287,21 @@ fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a))
}
fn shuffle_all(model: Model(a, filter)) {
let tracks = model.shuffler(model.library, model.data)
start_play.emit(tracks, 0)
case model.library_status {
HaveLibrary(lib) -> {
let tracks = model.shuffler(lib, model.data)
start_play.emit(tracks, 0)
}
NoLibrary -> effect.none()
}
}
fn library_decode(data: dynamic.Dynamic) {
let library: Library = dynamic.unsafe_coerce(data)
Ok(LibraryUpdated(library))
let library: option.Option(Library) = dynamic.unsafe_coerce(data)
case library {
option.Some(lib) -> Ok(LibraryUpdated(lib))
option.None -> Error([])
}
}
fn settings_decode(data: dynamic.Dynamic) {
@ -287,13 +309,17 @@ fn settings_decode(data: dynamic.Dynamic) {
Ok(SettingsUpdated(settings))
}
fn update_data(model: Model(a, filter), library: Library) {
Model(
..model,
data: library
|> model.data_getter(model.filter)
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
)
fn update_data(model: Model(a, filter)) {
case model.library_status, model.filter {
HaveLibrary(l), Filter(f) ->
Model(
..model,
data: l
|> model.data_getter(f)
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
)
_, _ -> model
}
}
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")
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() {
library_view.register(
component_name,
Nil,
data_getter,
item_view,
shuffler,
@ -31,7 +30,7 @@ pub fn register() {
/// Render the albums view.
pub fn render(
library: Library,
library: option.Option(Library),
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
@ -55,9 +54,10 @@ fn search_filter(item: Album, search_text: String) {
fn item_view(
model: Model(Album, Nil),
library: Library,
_items: List(LibraryItem(Album)),
_index: Int,
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() {
library_view.register(
component_name,
Nil,
data_getter,
item_view,
shuffler,
@ -36,7 +35,7 @@ pub fn register() {
/// Render the artists view.
pub fn render(
library: Library,
library: option.Option(Library),
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
@ -66,6 +65,7 @@ fn search_filter(item: Artist, search_text: String) {
fn item_view(
model: Model(Artist, Nil),
_library: Library,
_items: List(LibraryItem(Artist)),
_index: Int,
item: LibraryItem(Artist),

View file

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

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/list
@ -20,7 +20,6 @@ const component_name = "single-artist-view"
pub fn register() {
library_view.register(
component_name,
library.invalid_id,
data_getter,
item_view,
shuffler,
@ -32,7 +31,7 @@ pub fn register() {
/// Render the single artist view.
pub fn render(
library: Library,
library: option.Option(Library),
artist_id: Int,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
@ -44,12 +43,9 @@ pub fn render(
}
fn data_getter(library: Library, artist_id: Int) {
library.tracks
|> dict.fold([], fn(acc, key, val) {
case val.artist_id == artist_id && val.album_id == 0 {
True -> [#(key, val), ..acc]
False -> acc
}
let artist = library.assert_artist(library, artist_id)
list.map(artist.tracks, fn(track_id) {
#(track_id, library.assert_track(library, track_id))
})
}
@ -63,12 +59,13 @@ fn search_filter(item: Track, search_text: String) {
}
fn item_view(
model: library_view.Model(Track, Int),
_model: library_view.Model(Track, Int),
library: Library,
items: List(LibraryItem(Track)),
index: Int,
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) {

View file

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