diff --git a/src/elekf/library.gleam b/src/elekf/library.gleam index 6edf2ef..5e7a6ee 100644 --- a/src/elekf/library.gleam +++ b/src/elekf/library.gleam @@ -13,6 +13,11 @@ pub type Library { ) } +/// Gets an empty library for use as a default value. +pub fn empty() { + Library(albums: map.new(), artists: map.new(), tracks: map.new()) +} + /// Gets an album from the library based on ID. pub fn get_album(library: Library, id: Int) { map.get(library.albums, id) diff --git a/src/elekf/web/authed_view.gleam b/src/elekf/web/authed_view.gleam index 831d939..12b084a 100644 --- a/src/elekf/web/authed_view.gleam +++ b/src/elekf/web/authed_view.gleam @@ -23,6 +23,7 @@ import elekf/library/track.{Track} import elekf/transfer/library as library_transfer import elekf/web/components/player import elekf/web/components/search +import elekf/web/components/library_views/tracks_view import elekf/web/utils pub type PlayQueue = @@ -201,60 +202,60 @@ pub fn view(model: Model) { [ case model.library { option.None -> p([], [text("Loading library…")]) - option.Some(lib) -> - div( - [attribute.class("track-list")], - list.append( - [ - div( - [ - attribute.class("track-list-shuffle-all"), - event.on_click(ShuffleAll), - ], - [h3([attribute.class("track-title")], [text("Shuffle all")])], - ), - ], - { - let tracks = - map.to_list(lib.tracks) - |> list.filter(fn(track) { - search_text == "" || string.contains( - { track.1 }.title_lower, - search_text, - ) - }) - - list.index_map( - tracks, - fn(i, item) { - let #(track_id, track) = item - let album = library.assert_album(lib, track.album_id) - let artist = library.assert_artist(lib, track.artist_id) - div( - [ - attribute.id("track-list-" <> int.to_string(track_id)), - attribute.type_("button"), - event.on_click(StartPlay(tracks, i)), - attribute.attribute("role", "button"), - ], - [ - h3( - [attribute.class("track-title")], - [text(track.title)], - ), - p( - [attribute.class("track-artist")], - [text(artist.name)], - ), - p([attribute.class("track-album")], [text(album.name)]), - ], - ) - }, - ) - }, - ), - ) + option.Some(lib) -> tracks_view.render(lib) }, + // div( + // [attribute.class("track-list")], + // list.append( + // [ + // div( + // [ + // attribute.class("track-list-shuffle-all"), + // event.on_click(ShuffleAll), + // ], + // [h3([attribute.class("track-title")], [text("Shuffle all")])], + // ), + // ], + // { + // let tracks = + // map.to_list(lib.tracks) + // |> list.filter(fn(track) { + // search_text == "" || string.contains( + // { track.1 }.title_lower, + // search_text, + // ) + // }) + + // list.index_map( + // tracks, + // fn(i, item) { + // let #(track_id, track) = item + // let album = library.assert_album(lib, track.album_id) + // let artist = library.assert_artist(lib, track.artist_id) + // div( + // [ + // attribute.id("track-list-" <> int.to_string(track_id)), + // attribute.type_("button"), + // event.on_click(StartPlay(tracks, i)), + // attribute.attribute("role", "button"), + // ], + // [ + // h3( + // [attribute.class("track-title")], + // [text(track.title)], + // ), + // p( + // [attribute.class("track-artist")], + // [text(artist.name)], + // ), + // p([attribute.class("track-album")], [text(album.name)]), + // ], + // ) + // }, + // ) + // }, + // ), + // ) div( [attribute.id("search-positioner")], [ diff --git a/src/elekf/web/components/library_view.gleam b/src/elekf/web/components/library_view.gleam new file mode 100644 index 0000000..853bba3 --- /dev/null +++ b/src/elekf/web/components/library_view.gleam @@ -0,0 +1,125 @@ +//// A view to the library, presenting artists, albums, or tracks. + +import gleam/dynamic +import gleam/list +import gleam/map +import lustre +import lustre/effect +import lustre/element.{text} +import lustre/element/html.{div, h3} +import lustre/attribute +import lustre/event +import elekf/library.{Library} +import elekf/library/album.{Album} +import elekf/library/artist.{Artist} +import elekf/library/track.{Track} +import elekf/web/events/start_play + +pub const start_play_event = "start-play" + +pub type LibraryItem(a) = + #(Int, a) + +pub type DataGetter(a) = + fn(Library) -> List(LibraryItem(a)) + +pub type ItemView(a) = + fn(Library, List(LibraryItem(a)), Int, LibraryItem(a)) -> element.Element(Msg) + +pub type Shuffler(a) = + fn(List(LibraryItem(a))) -> List(LibraryItem(Track)) + +pub type View { + /// All albums. + Albums + /// Tracks of a single album. + SingleAlbum(Int, Album) + /// All artists. + Artists + /// Albums of a single artist. + SingleArtist(Int, Artist) + /// Tracks of a single artist. + SingleArtistTracks(Int, Artist) + /// All tracks. + Tracks +} + +pub type Msg { + LibraryUpdated(Library) + ShuffleAll + StartPlay(List(LibraryItem(Track)), Int) +} + +pub type Model(a) { + Model( + library: Library, + data: List(LibraryItem(a)), + item_view: ItemView(a), + shuffler: Shuffler(a), + ) +} + +pub fn register( + name: String, + data_getter: DataGetter(a), + item_view: ItemView(a), + shuffler: Shuffler(a), +) { + lustre.component( + name, + fn() { init(library.empty(), data_getter, item_view, shuffler) }, + update, + view, + map.from_list([#("library", library_decode)]), + ) +} + +pub fn render(name: String, library: Library) { + element.element(name, [attribute.property("library", library)], []) +} + +fn init(library, data_getter, item_view, shuffler) { + #(Model(library, data_getter(library), item_view, shuffler), effect.none()) +} + +fn update(model, msg) { + case msg { + LibraryUpdated(library) -> #( + Model(..model, library: library), + effect.none(), + ) + ShuffleAll -> #(model, shuffle_all(model)) + StartPlay(tracks, position) -> #(model, start_play.emit(tracks, position)) + } +} + +fn view(model: Model(a)) { + div( + [attribute.class("library-list")], + list.append( + [ + div( + [ + attribute.class("library-item library-list-shuffle-all"), + event.on_click(ShuffleAll), + ], + [h3([attribute.class("library-item-title")], [text("Shuffle all")])], + ), + ], + list.index_map( + model.data, + fn(i, item) { model.item_view(model.library, model.data, i, item) }, + ), + ), + ) +} + +fn shuffle_all(model: Model(a)) { + let tracks = model.shuffler(model.data) + start_play.emit(tracks, 0) +} + +fn library_decode(data: dynamic.Dynamic) { + let library: Library = dynamic.unsafe_coerce(data) + Ok(LibraryUpdated(library)) +} diff --git a/src/elekf/web/components/library_views/tracks_view.gleam b/src/elekf/web/components/library_views/tracks_view.gleam new file mode 100644 index 0000000..f0c96b7 --- /dev/null +++ b/src/elekf/web/components/library_views/tracks_view.gleam @@ -0,0 +1,52 @@ +import gleam/int +import gleam/list +import gleam/map +import lustre/element/html.{div, h3, p} +import lustre/element.{text} +import lustre/attribute +import lustre/event +import elekf/library.{Library} +import elekf/library/track.{Track} +import elekf/web/components/library_view.{LibraryItem, StartPlay} + +const component_name = "tracks-view" + +pub fn register() { + library_view.register(component_name, data_getter, item_view, shuffler) +} + +pub fn render(library: Library) { + library_view.render(component_name, library) +} + +fn data_getter(library: Library) { + map.to_list(library.tracks) +} + +fn shuffler(items) { + list.shuffle(items) +} + +fn item_view( + library: Library, + items: List(LibraryItem(Track)), + index: Int, + item: LibraryItem(Track), +) { + let #(track_id, track) = item + let album = library.assert_album(library, track.album_id) + let artist = library.assert_artist(library, track.artist_id) + div( + [ + attribute.id("track-list-" <> int.to_string(track_id)), + attribute.type_("button"), + event.on_click(StartPlay(items, index)), + attribute.attribute("role", "button"), + ], + [ + h3([attribute.class("track-title")], [text(track.title)]), + p([attribute.class("track-artist")], [text(artist.name)]), + p([attribute.class("track-album")], [text(album.name)]), + ], + ) +} diff --git a/src/elekf/web/events/start_play.gleam b/src/elekf/web/events/start_play.gleam new file mode 100644 index 0000000..76dc535 --- /dev/null +++ b/src/elekf/web/events/start_play.gleam @@ -0,0 +1,17 @@ +import gleam/dynamic +import lustre/event +import elekf/library/track.{Track} + +pub const event_name = "start-play" + +pub type EventData { + EventData(tracks: List(#(Int, Track)), position: Int) +} + +pub fn emit(tracks: List(#(Int, Track)), position: Int) { + event.emit(event_name, EventData(tracks, position)) +} + +pub fn decoder(data) -> EventData { + dynamic.unsafe_coerce(data) +} diff --git a/src/elekf/web/main.gleam b/src/elekf/web/main.gleam index 1f88579..9880f66 100644 --- a/src/elekf/web/main.gleam +++ b/src/elekf/web/main.gleam @@ -12,6 +12,7 @@ import elekf/web/login_view import elekf/web/authed_view import elekf/web/view import elekf/web/utils +import elekf/web/components/library_views/tracks_view import elekf/api/auth/storage as auth_storage import elekf/api/auth/models as auth_models import elekf/utils/option as option_utils @@ -33,6 +34,8 @@ type Msg { } pub fn main() { + let assert Ok(_) = tracks_view.register() + let app = lustre.application(init, update, view) let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil)