Beginnings of refactoring to component based model

This commit is contained in:
Mikko Ahlroth 2023-10-14 13:18:14 +03:00
parent f8310f11e6
commit 8ea17ecd82
6 changed files with 256 additions and 53 deletions

View file

@ -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)

View file

@ -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")],
[

View file

@ -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))
}

View file

@ -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)]),
],
)
}

View file

@ -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)
}

View file

@ -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)