From 4f869df09fd2c4e639a886a24bf5afdd553ef2ca Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sat, 2 Mar 2024 21:19:33 +0200 Subject: [PATCH] Prevent crash when library is not yet loaded --- src/elekf/web/authed_view.gleam | 43 ++++-- src/elekf/web/components/library_view.gleam | 146 +++++++++++------- .../library_views/albums_view.gleam | 6 +- .../library_views/artists_view.gleam | 4 +- .../library_views/play_queue_view.gleam | 8 +- .../library_views/single_album_view.gleam | 8 +- .../library_views/single_artist_view.gleam | 19 +-- .../library_views/tracks_view.gleam | 8 +- 8 files changed, 143 insertions(+), 99 deletions(-) diff --git a/src/elekf/web/authed_view.gleam b/src/elekf/web/authed_view.gleam index 7e83bc2..2254da8 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -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() diff --git a/src/elekf/web/components/library_view.gleam b/src/elekf/web/components/library_view.gleam index 28269be..212ad5d 100644 --- a/src/elekf/web/components/library_view.gleam +++ b/src/elekf/web/components/library_view.gleam @@ -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) +} diff --git a/src/elekf/web/components/library_views/albums_view.gleam b/src/elekf/web/components/library_views/albums_view.gleam index aaa2278..c84d35b 100644 --- a/src/elekf/web/components/library_views/albums_view.gleam +++ b/src/elekf/web/components/library_views/albums_view.gleam @@ -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) } diff --git a/src/elekf/web/components/library_views/artists_view.gleam b/src/elekf/web/components/library_views/artists_view.gleam index 0e20dd7..edb2d6e 100644 --- a/src/elekf/web/components/library_views/artists_view.gleam +++ b/src/elekf/web/components/library_views/artists_view.gleam @@ -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), diff --git a/src/elekf/web/components/library_views/play_queue_view.gleam b/src/elekf/web/components/library_views/play_queue_view.gleam index 51750df..913eb40 100644 --- a/src/elekf/web/components/library_views/play_queue_view.gleam +++ b/src/elekf/web/components/library_views/play_queue_view.gleam @@ -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") } 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 87c269c..1d91b3b 100644 --- a/src/elekf/web/components/library_views/single_album_view.gleam +++ b/src/elekf/web/components/library_views/single_album_view.gleam @@ -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) { 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 b33d05f..8443461 100644 --- a/src/elekf/web/components/library_views/single_artist_view.gleam +++ b/src/elekf/web/components/library_views/single_artist_view.gleam @@ -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) { diff --git a/src/elekf/web/components/library_views/tracks_view.gleam b/src/elekf/web/components/library_views/tracks_view.gleam index 2b10965..e309704 100644 --- a/src/elekf/web/components/library_views/tracks_view.gleam +++ b/src/elekf/web/components/library_views/tracks_view.gleam @@ -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") }