Album and artist views added, previous track selection added

This commit is contained in:
Mikko Ahlroth 2023-10-22 11:15:38 +03:00
parent 89f30e0225
commit 8856392c98
13 changed files with 274 additions and 20 deletions

View file

@ -2,16 +2,16 @@
# You typically do not need to edit this file
packages = [
{ name = "gleam_fetch", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_http", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "F64E93C754D948B2D37ABC4ADD5482FE0FAED4B99C79E66012DDE96BEDC40544" },
{ name = "gleam_fetch", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "F64E93C754D948B2D37ABC4ADD5482FE0FAED4B99C79E66012DDE96BEDC40544" },
{ name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" },
{ name = "gleam_javascript", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "BFEBB63ABE4A1694E07DEFD19B160C2980304B5D775A89D4B02E7DE7C9D8008B" },
{ name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" },
{ name = "gleam_stdlib", version = "0.31.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6D1BC5B4D4179B9FEE866B1E69FE180AC2CE485AD90047C0B32B2CA984052736" },
{ name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" },
{ name = "lustre", version = "3.0.7", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "0C4763BEB8426DEFCCD96015CE2F0EF1446E26A731B614DDF3EA09BEF9BA8DB2" },
{ name = "plinth", version = "0.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_javascript"], otp_app = "plinth", source = "hex", outer_checksum = "E81BA6A6CEAFFADBCB85B04DC817A4CDC43AFA7BB6AE56CE0B7C7E66D1C9ADD1" },
{ name = "lustre", version = "3.0.9", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "CBD20163B7CB7FA2AC2BCFBAB5A083B22AED9612B6522BE479E8FEF09C82A9E1" },
{ name = "plinth", version = "0.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_javascript"], otp_app = "plinth", source = "hex", outer_checksum = "43307A0271B8AC9B646B3C39F80D22C41DB4022CDAD67ABFAA555BB3F1321E2E" },
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
{ name = "varasto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "0621E5BFD0B9B7F7D19B8FC6369C6E2EAC5C1F3858A1E5E51342F5BCE10C3728" },
{ name = "varasto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "plinth", "gleam_json"], otp_app = "varasto", source = "hex", outer_checksum = "0621E5BFD0B9B7F7D19B8FC6369C6E2EAC5C1F3858A1E5E51342F5BCE10C3728" },
]
[requirements]

View file

@ -1,6 +1,7 @@
pub type Album {
Album(
name: String,
name_lower: String,
tracks: List(Int),
artist_id: Int,
trashed: Bool,

View file

@ -1,3 +1,9 @@
pub type Artist {
Artist(name: String, tracks: List(Int), trashed: Bool, rating: Int)
Artist(
name: String,
name_lower: String,
tracks: List(Int),
trashed: Bool,
rating: Int,
)
}

View file

@ -1,3 +1,4 @@
import gleam/string
import ibroadcast/library/library.{Album as APIAlbum}
import elekf/library/album.{Album}
@ -5,6 +6,7 @@ import elekf/library/album.{Album}
pub fn from(album: APIAlbum) {
Album(
name: album.name,
name_lower: string.lowercase(album.name),
tracks: album.tracks,
artist_id: album.artist_id,
trashed: album.trashed,

View file

@ -1,3 +1,4 @@
import gleam/string
import ibroadcast/library/library.{Artist as APIArtist}
import elekf/library/artist.{Artist}
@ -5,6 +6,7 @@ import elekf/library/artist.{Artist}
pub fn from(artist: APIArtist) {
Artist(
name: artist.name,
name_lower: string.lowercase(artist.name),
tracks: artist.tracks,
trashed: artist.trashed,
rating: artist.rating,

View file

@ -6,9 +6,10 @@ import gleam/list
import gleam/option
import gleam/javascript/promise
import lustre/element.{text}
import lustre/element/html.{div, p}
import lustre/element/html.{button, div, nav, p}
import lustre/attribute
import lustre/effect
import lustre/event
import elekf/web/common.{PlayQueue}
import ibroadcast/library/library as library_api
import ibroadcast/authed_request.{RequestConfig}
@ -18,9 +19,13 @@ import elekf/library.{Library}
import elekf/library/track.{Track}
import elekf/transfer/library as library_transfer
import elekf/web/components/player
import elekf/web/components/library_view
import elekf/web/components/library_views/tracks_view
import elekf/web/components/library_views/artists_view
import elekf/web/components/library_views/albums_view
import elekf/web/events/start_play
import elekf/web/utils
import elekf/web/components/icon.{Alt, icon}
pub type PlayInfo {
PlayInfo(
@ -44,6 +49,7 @@ pub type Model {
settings: option.Option(common.Settings),
request_config: RequestConfig,
play_status: PlayStatus,
library_view: library_view.View,
)
}
@ -52,6 +58,7 @@ pub type Msg {
LibraryResult(Result(library_api.ResponseData, http.ResponseError))
PlayerMsg(player.Msg)
StartPlay(PlayQueue, Int)
ChangeView(library_view.View)
}
pub fn init(auth_data: common.AuthData) {
@ -62,6 +69,7 @@ pub fn init(auth_data: common.AuthData) {
settings: option.None,
request_config: form_request_config(auth_data),
play_status: NoTracks,
library_view: library_view.Tracks,
)
#(model, load_library(model))
@ -124,11 +132,14 @@ pub fn update(model: Model, msg) {
let #(status, effect) = handle_start_play(model, tracks, position)
#(Model(..model, play_status: status), effect)
}
PlayerMsg(player.NextTrack) -> {
PlayerMsg(player.NextTrack) | PlayerMsg(player.PrevTrack) -> {
if_player(
model,
fn(info) {
let next_index = info.play_index + 1
let next_index = case msg {
PlayerMsg(player.NextTrack) -> info.play_index + 1
_ -> info.play_index - 1
}
case list.at(info.play_queue, next_index) {
Ok(_) -> {
let #(status, effect) =
@ -157,6 +168,9 @@ pub fn update(model: Model, msg) {
},
)
}
ChangeView(view) -> {
#(Model(..model, library_view: view), effect.none())
}
}
}
@ -176,10 +190,49 @@ pub fn view(model: Model) {
div(
[attribute.id("authed-view-library")],
[
tracks_view.render(
model.library,
[attribute.id("tracks-view"), start_play.on(StartPlay)],
nav(
[attribute.id("library-top-nav")],
[
button(
[
attribute.type_("button"),
event.on_click(ChangeView(library_view.Tracks)),
],
[icon("music-note-beamed", Alt("Tracks"))],
),
button(
[
attribute.type_("button"),
event.on_click(ChangeView(library_view.Artists)),
],
[icon("file-person", Alt("Artists"))],
),
button(
[
attribute.type_("button"),
event.on_click(ChangeView(library_view.Albums)),
],
[icon("disc", Alt("Albums"))],
),
],
),
case model.library_view {
library_view.Tracks ->
tracks_view.render(
model.library,
[attribute.id("tracks-view"), start_play.on(StartPlay)],
)
library_view.Artists ->
artists_view.render(
model.library,
[attribute.id("artists-view"), start_play.on(StartPlay)],
)
library_view.Albums ->
albums_view.render(
model.library,
[attribute.id("albums-view"), start_play.on(StartPlay)],
)
},
],
),
div(

View file

@ -12,8 +12,6 @@ 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
import elekf/web/components/search
@ -40,19 +38,19 @@ pub type SearchFilter(a) =
/// Shuffler takes all of the items, finds all of the tracks in them, and
/// shuffles them for playback.
pub type Shuffler(a) =
fn(List(LibraryItem(a))) -> List(LibraryItem(Track))
fn(Library, List(LibraryItem(a))) -> List(LibraryItem(Track))
pub type View {
/// All albums.
Albums
/// Tracks of a single album.
SingleAlbum(Int, Album)
/// SingleAlbum(Int, Album)
/// All artists.
Artists
/// Albums of a single artist.
SingleArtist(Int, Artist)
/// SingleArtist(Int, Artist)
/// Tracks of a single artist.
SingleArtistTracks(Int, Artist)
/// SingleArtistTracks(Int, Artist)
/// All tracks.
Tracks
}
@ -167,7 +165,7 @@ fn generate_view(item_view: ItemView(a), search_filter: SearchFilter(a)) {
}
fn shuffle_all(model: Model(a)) {
let tracks = model.shuffler(model.data)
let tracks = model.shuffler(model.library, model.data)
start_play.emit(tracks, 0)
}

View file

@ -0,0 +1,84 @@
//// A library view to all of the albums in the library.
import gleam/string
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/album.{Album}
import elekf/web/components/library_view.{LibraryItem, StartPlay}
const component_name = "albums-view"
/// Register the albums view as a custom element.
pub fn register() {
library_view.register(
component_name,
data_getter,
item_view,
shuffler,
search_filter,
)
}
/// Render the albums view.
pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) {
library_view.render(component_name, library, extra_attrs)
}
fn data_getter(library: Library) {
map.to_list(library.albums)
}
fn shuffler(library, items: List(LibraryItem(Album))) {
items
|> list.shuffle()
|> list.map(fn(album) { get_tracks(library, album.1) })
|> list.flatten()
}
fn search_filter(item: Album, search_text: String) {
string.contains(item.name_lower, search_text)
}
fn item_view(
library: Library,
_items: List(LibraryItem(Album)),
_index: Int,
item: LibraryItem(Album),
) {
let #(album_id, album) = item
let tracks = get_tracks(library, album)
let artist_name = case album.artist_id {
0 -> "Unknown artist"
id -> library.assert_artist(library, id).name
}
div(
[
attribute.id("album-list-" <> int.to_string(album_id)),
attribute.type_("button"),
event.on_click(StartPlay(tracks, 0)),
attribute.attribute("role", "button"),
],
[
h3([attribute.class("album-title")], [text(album.name)]),
p([attribute.class("album-artist")], [text(artist_name)]),
p(
[attribute.class("album-tracks")],
[text(int.to_string(list.length(album.tracks)) <> " tracks")],
),
],
)
}
fn get_tracks(library: Library, album: Album) {
list.map(
album.tracks,
fn(track_id) { #(track_id, library.assert_track(library, track_id)) },
)
}

View file

@ -0,0 +1,87 @@
//// A library view to all of the artists in the library.
import gleam/string
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/artist.{Artist}
import elekf/web/components/library_view.{LibraryItem, StartPlay}
const component_name = "artists-view"
/// Register the artists view as a custom element.
pub fn register() {
library_view.register(
component_name,
data_getter,
item_view,
shuffler,
search_filter,
)
}
/// Render the artists view.
pub fn render(library: Library, extra_attrs: List(attribute.Attribute(msg))) {
library_view.render(component_name, library, extra_attrs)
}
fn data_getter(library: Library) {
library.artists
|> map.fold(
[],
fn(acc, key, val) {
case val.tracks {
[] -> acc
_ -> [#(key, val), ..acc]
}
},
)
}
fn shuffler(library, items: List(LibraryItem(Artist))) {
items
|> list.shuffle()
|> list.map(fn(artist) { get_tracks(library, artist.1) })
|> list.flatten()
}
fn search_filter(item: Artist, search_text: String) {
string.contains(item.name_lower, search_text)
}
fn item_view(
library: Library,
_items: List(LibraryItem(Artist)),
_index: Int,
item: LibraryItem(Artist),
) {
let #(artist_id, artist) = item
let tracks = get_tracks(library, artist)
div(
[
attribute.id("artist-list-" <> int.to_string(artist_id)),
attribute.type_("button"),
event.on_click(StartPlay(tracks, 0)),
attribute.attribute("role", "button"),
],
[
h3([attribute.class("artist-title")], [text(artist.name)]),
p(
[attribute.class("artist-tracks")],
[text(int.to_string(list.length(artist.tracks)) <> " tracks")],
),
],
)
}
fn get_tracks(library: Library, artist: Artist) {
list.map(
artist.tracks,
fn(track_id) { #(track_id, library.assert_track(library, track_id)) },
)
}

View file

@ -34,7 +34,7 @@ fn data_getter(library: Library) {
map.to_list(library.tracks)
}
fn shuffler(items) {
fn shuffler(_library, items) {
list.shuffle(items)
}

View file

@ -151,7 +151,11 @@ pub fn view(model: Model) {
[attribute.id("player-controls")],
[
button(
[attribute.id("player-previous"), attribute.type_("button")],
[
attribute.id("player-previous"),
attribute.type_("button"),
event.on_click(PrevTrack),
],
[icon("skip-backward-fill", Alt("Previous"))],
),
case is_playing {

View file

@ -13,6 +13,8 @@ import elekf/web/authed_view
import elekf/web/view
import elekf/web/utils
import elekf/web/components/library_views/tracks_view
import elekf/web/components/library_views/artists_view
import elekf/web/components/library_views/albums_view
import elekf/api/auth/storage as auth_storage
import elekf/api/auth/models as auth_models
import elekf/utils/option as option_utils
@ -35,6 +37,8 @@ type Msg {
pub fn main() {
let assert Ok(_) = tracks_view.register()
let assert Ok(_) = artists_view.register()
let assert Ok(_) = albums_view.register()
let app = lustre.application(init, update, view)
let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil)

View file

@ -30,6 +30,19 @@ main {
overflow-y: auto;
}
#library-top-nav {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: stretch;
}
#library-top-nav button {
display: block;
width: 100%;
font-size: 2rem;
}
#search-bar {
position: absolute;
bottom: 100px;