New fancy header, bump to 1.0.0, fix artist shuffling
This commit is contained in:
parent
4f869df09f
commit
eb30b0f971
17 changed files with 306 additions and 61 deletions
|
@ -1,2 +1,2 @@
|
||||||
gleam 0.34.1
|
gleam 1.0.0
|
||||||
nodejs 20.10.0
|
nodejs 20.10.0
|
||||||
|
|
|
@ -6,19 +6,19 @@ packages = [
|
||||||
{ name = "birl", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "23BFE5AB0D7D9E4ECC5BB89B7ABDDF8E976D98C65D2E173D116E6AAFBF24E633" },
|
{ name = "birl", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "23BFE5AB0D7D9E4ECC5BB89B7ABDDF8E976D98C65D2E173D116E6AAFBF24E633" },
|
||||||
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
|
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
|
||||||
{ name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
|
{ name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
|
||||||
{ name = "gleam_fetch", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_http", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "24F83C6EF5BC274EF4D712B374D7CDB795136883DA64E2BA6435AF8E6C57E6E2" },
|
{ name = "gleam_fetch", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "gleam_javascript"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "24F83C6EF5BC274EF4D712B374D7CDB795136883DA64E2BA6435AF8E6C57E6E2" },
|
||||||
{ name = "gleam_http", version = "3.5.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "C2FC3322203B16F897C1818D9810F5DEFCE347F0751F3B44421E1261277A7373" },
|
{ name = "gleam_http", version = "3.5.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "C2FC3322203B16F897C1818D9810F5DEFCE347F0751F3B44421E1261277A7373" },
|
||||||
{ name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
|
{ name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
|
||||||
{ name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" },
|
{ name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" },
|
||||||
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
||||||
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
||||||
{ name = "glint", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "21AB16D5A50D4EF34DF935915FDBEE06B2DAEDEE3FCC8584C6E635A866566B38" },
|
{ name = "glint", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "snag", "gleam_community_colour", "gleam_stdlib"], otp_app = "glint", source = "hex", outer_checksum = "21AB16D5A50D4EF34DF935915FDBEE06B2DAEDEE3FCC8584C6E635A866566B38" },
|
||||||
{ name = "lustre", version = "3.1.4", build_tools = ["gleam"], requirements = ["argv", "gleam_stdlib", "glint", "gleam_community_ansi"], otp_app = "lustre", source = "hex", outer_checksum = "E651E39189F55473837FB7386C06BAED7276B37B5058302CAC880F89C25CB4E9" },
|
{ name = "lustre", version = "3.1.4", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "argv", "glint"], otp_app = "lustre", source = "hex", outer_checksum = "E651E39189F55473837FB7386C06BAED7276B37B5058302CAC880F89C25CB4E9" },
|
||||||
{ name = "plinth", version = "0.1.9", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_javascript"], otp_app = "plinth", source = "hex", outer_checksum = "89BE43DC719539A676A9515C619315A2A7188A6FF5D7499F8261241BC0B2F1A9" },
|
{ name = "plinth", version = "0.1.9", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "89BE43DC719539A676A9515C619315A2A7188A6FF5D7499F8261241BC0B2F1A9" },
|
||||||
{ name = "ranger", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "28E615AE7590ED922AF1510DDF606A2ECBBC2A9609AF36D412EDC925F06DFD20" },
|
{ name = "ranger", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "28E615AE7590ED922AF1510DDF606A2ECBBC2A9609AF36D412EDC925F06DFD20" },
|
||||||
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
|
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
|
||||||
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
||||||
{ name = "varasto", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "plinth", "gleam_json"], otp_app = "varasto", source = "hex", outer_checksum = "B17CDA56C2CD5BEFABF299E14FD8797E7FE6ABE1B861CA1C9E07441AE6FDDE6B" },
|
{ name = "varasto", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "B17CDA56C2CD5BEFABF299E14FD8797E7FE6ABE1B861CA1C9E07441AE6FDDE6B" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[requirements]
|
[requirements]
|
||||||
|
|
|
@ -296,6 +296,7 @@ pub fn view(model: Model) {
|
||||||
artists_view.render(library_if_loaded(model), model.settings, [
|
artists_view.render(library_if_loaded(model), model.settings, [
|
||||||
attribute.id("artists-view"),
|
attribute.id("artists-view"),
|
||||||
attribute.class("glass-bg"),
|
attribute.class("glass-bg"),
|
||||||
|
start_play.on(StartPlay),
|
||||||
])
|
])
|
||||||
library_view.Albums ->
|
library_view.Albums ->
|
||||||
albums_view.render(library_if_loaded(model), model.settings, [
|
albums_view.render(library_if_loaded(model), model.settings, [
|
||||||
|
|
|
@ -8,10 +8,12 @@ import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import gleam/result
|
import gleam/result
|
||||||
|
import gleam/int
|
||||||
|
import gleam/set
|
||||||
import lustre
|
import lustre
|
||||||
import lustre/effect
|
import lustre/effect
|
||||||
import lustre/element.{text}
|
import lustre/element.{type Element, text}
|
||||||
import lustre/element/html.{div}
|
import lustre/element/html.{div, h1, p}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/event
|
import lustre/event
|
||||||
import elekf/utils/lustre as lustre_utils
|
import elekf/utils/lustre as lustre_utils
|
||||||
|
@ -23,6 +25,7 @@ import elekf/web/events/scroll_to
|
||||||
import elekf/web/components/search
|
import elekf/web/components/search
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
import elekf/web/components/library_item.{type LibraryItem}
|
||||||
import elekf/web/components/shuffle_all
|
import elekf/web/components/shuffle_all
|
||||||
|
import elekf/web/components/track_length
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
import elekf/web/storage/history/storage as history_store
|
import elekf/web/storage/history/storage as history_store
|
||||||
|
|
||||||
|
@ -30,6 +33,11 @@ import elekf/web/storage/history/storage as history_store
|
||||||
pub type DataGetter(a, filter) =
|
pub type DataGetter(a, filter) =
|
||||||
fn(Library, filter) -> List(LibraryItem(a))
|
fn(Library, filter) -> List(LibraryItem(a))
|
||||||
|
|
||||||
|
/// A view to render the header of the library view.
|
||||||
|
pub type HeaderView(a, filter) =
|
||||||
|
fn(Model(a, filter), Library, filter, List(LibraryItem(a))) ->
|
||||||
|
List(Element(Msg(filter)))
|
||||||
|
|
||||||
/// A view that renders a single item from the library.
|
/// A view that renders a single item from the library.
|
||||||
///
|
///
|
||||||
/// It should return a list of elements, which can be used for expanding items:
|
/// It should return a list of elements, which can be used for expanding items:
|
||||||
|
@ -37,19 +45,12 @@ pub type DataGetter(a, filter) =
|
||||||
/// expanded contents.
|
/// expanded contents.
|
||||||
pub type ItemView(a, filter) =
|
pub type ItemView(a, filter) =
|
||||||
fn(Model(a, filter), Library, List(LibraryItem(a)), Int, LibraryItem(a)) ->
|
fn(Model(a, filter), Library, List(LibraryItem(a)), Int, LibraryItem(a)) ->
|
||||||
List(element.Element(Msg(filter)))
|
List(Element(Msg(filter)))
|
||||||
|
|
||||||
/// A filter to restrict the items to be shown.
|
/// A filter that might not yet be loaded. If it's not loaded, no data should be
|
||||||
pub type Filter(filter) {
|
/// fetched yet.
|
||||||
/// The filter has not yet been obtained as the component is initializing. No
|
pub type Filter(filter) =
|
||||||
/// data fetches should be done yet.
|
option.Option(filter)
|
||||||
FilterNotLoaded
|
|
||||||
Filter(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type LibraryAcquired
|
|
||||||
|
|
||||||
pub type LibraryNotAcquired
|
|
||||||
|
|
||||||
pub opaque type LibraryStatus {
|
pub opaque type LibraryStatus {
|
||||||
HaveLibrary(library: Library)
|
HaveLibrary(library: Library)
|
||||||
|
@ -122,17 +123,19 @@ pub type Model(a, filter) {
|
||||||
pub fn register(
|
pub fn register(
|
||||||
name: String,
|
name: String,
|
||||||
data_getter: DataGetter(a, filter),
|
data_getter: DataGetter(a, filter),
|
||||||
|
header_view: HeaderView(a, filter),
|
||||||
item_view: ItemView(a, filter),
|
item_view: ItemView(a, filter),
|
||||||
shuffler: Shuffler(a),
|
shuffler: Shuffler(a),
|
||||||
sorter: Sorter(a),
|
sorter: Sorter(a),
|
||||||
|
filter: Filter(filter),
|
||||||
search_filter: SearchFilter(a),
|
search_filter: SearchFilter(a),
|
||||||
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
||||||
) {
|
) {
|
||||||
lustre.component(
|
lustre.component(
|
||||||
name,
|
name,
|
||||||
fn() { init(name, FilterNotLoaded, data_getter, shuffler, sorter) },
|
fn() { init(name, filter, data_getter, shuffler, sorter) },
|
||||||
update,
|
update,
|
||||||
generate_view(item_view, search_filter),
|
generate_view(header_view, item_view, search_filter),
|
||||||
dict.merge(generic_attributes(), extra_attrs),
|
dict.merge(generic_attributes(), extra_attrs),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -220,7 +223,7 @@ pub fn update(model: Model(a, filter), msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterUpdated(filter) -> {
|
FilterUpdated(filter) -> {
|
||||||
let new_model = Model(..model, filter: Filter(filter))
|
let new_model = Model(..model, filter: option.Some(filter))
|
||||||
#(update_data(new_model), effect.none())
|
#(update_data(new_model), effect.none())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,11 +248,12 @@ pub fn update(model: Model(a, filter), msg) {
|
||||||
|
|
||||||
pub fn library_view(
|
pub fn library_view(
|
||||||
model: Model(a, filter),
|
model: Model(a, filter),
|
||||||
|
header_view: HeaderView(a, filter),
|
||||||
item_view: ItemView(a, filter),
|
item_view: ItemView(a, filter),
|
||||||
search_filter: SearchFilter(a),
|
search_filter: SearchFilter(a),
|
||||||
) {
|
) {
|
||||||
case model.library_status, model.filter == FilterNotLoaded {
|
case model.library_status, model.filter {
|
||||||
HaveLibrary(lib), False -> {
|
HaveLibrary(lib), option.Some(f) -> {
|
||||||
let items = case model.search.search_text {
|
let items = case model.search.search_text {
|
||||||
"" -> model.data
|
"" -> model.data
|
||||||
txt -> {
|
txt -> {
|
||||||
|
@ -264,13 +268,17 @@ pub fn library_view(
|
||||||
list.append(
|
list.append(
|
||||||
[
|
[
|
||||||
search.view(model.search)
|
search.view(model.search)
|
||||||
|> element.map(Search),
|
|> element.map(Search),
|
||||||
|
div(
|
||||||
|
[attribute.id("library-list-header")],
|
||||||
|
header_view(model, lib, f, items),
|
||||||
|
),
|
||||||
shuffle_all.view([event.on_click(ShuffleAll)]),
|
shuffle_all.view([event.on_click(ShuffleAll)]),
|
||||||
],
|
],
|
||||||
list.index_map(items, fn(item, i) {
|
list.index_map(items, fn(item, i) {
|
||||||
item_view(model, lib, items, i, item)
|
item_view(model, lib, items, i, item)
|
||||||
})
|
})
|
||||||
|> list.flatten(),
|
|> list.flatten(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -282,8 +290,61 @@ pub fn library_view(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a)) {
|
/// An empty header view that renders nothing.
|
||||||
library_view(_, item_view, search_filter)
|
pub fn empty_header(_model, _library, _filter, _items) {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A header that renders some statistics about the found tracks.
|
||||||
|
pub fn stats_header(
|
||||||
|
title: String,
|
||||||
|
_model,
|
||||||
|
_library,
|
||||||
|
_filter,
|
||||||
|
items: List(LibraryItem(Track)),
|
||||||
|
) {
|
||||||
|
let #(tracks, seconds, artists, albums) =
|
||||||
|
list.fold(items, #(0, 0, set.new(), set.new()), fn(acc, item) {
|
||||||
|
let #(_i, track) = item
|
||||||
|
let #(tracks, seconds, artists, albums) = acc
|
||||||
|
#(
|
||||||
|
tracks + 1,
|
||||||
|
seconds + track.length,
|
||||||
|
set.insert(artists, track.artist_id),
|
||||||
|
set.insert(albums, track.album_id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
let duration =
|
||||||
|
track_length.humanize_length(
|
||||||
|
seconds,
|
||||||
|
track_length.Auto,
|
||||||
|
track_length.short_delimiters,
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
div([attribute.id("library-list-stats-header")], [
|
||||||
|
h1([], [text(title)]),
|
||||||
|
p([], [
|
||||||
|
text(
|
||||||
|
int.to_string(tracks)
|
||||||
|
<> " tracks by "
|
||||||
|
<> int.to_string(set.size(artists))
|
||||||
|
<> " artists in "
|
||||||
|
<> int.to_string(set.size(albums))
|
||||||
|
<> " albums, "
|
||||||
|
<> duration,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_view(
|
||||||
|
header_view: HeaderView(a, filter),
|
||||||
|
item_view: ItemView(a, filter),
|
||||||
|
search_filter: SearchFilter(a),
|
||||||
|
) {
|
||||||
|
library_view(_, header_view, item_view, search_filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shuffle_all(model: Model(a, filter)) {
|
fn shuffle_all(model: Model(a, filter)) {
|
||||||
|
@ -311,7 +372,7 @@ fn settings_decode(data: dynamic.Dynamic) {
|
||||||
|
|
||||||
fn update_data(model: Model(a, filter)) {
|
fn update_data(model: Model(a, filter)) {
|
||||||
case model.library_status, model.filter {
|
case model.library_status, model.filter {
|
||||||
HaveLibrary(l), Filter(f) ->
|
HaveLibrary(l), option.Some(f) ->
|
||||||
Model(
|
Model(
|
||||||
..model,
|
..model,
|
||||||
data: l
|
data: l
|
||||||
|
|
|
@ -20,9 +20,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
header_view,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
album_utils.sort_by_name,
|
album_utils.sort_by_name,
|
||||||
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
)
|
)
|
||||||
|
@ -52,6 +54,20 @@ fn search_filter(item: Album, search_text: String) {
|
||||||
string.contains(item.name_lower, search_text)
|
string.contains(item.name_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_view(
|
||||||
|
model: Model(Album, Nil),
|
||||||
|
library: Library,
|
||||||
|
filter: Nil,
|
||||||
|
items: List(LibraryItem(Album)),
|
||||||
|
) {
|
||||||
|
let tracks =
|
||||||
|
list.fold(items, [], fn(acc, artist) {
|
||||||
|
list.concat([acc, album_utils.get_tracks(library, artist.1)])
|
||||||
|
})
|
||||||
|
|
||||||
|
library_view.stats_header("Albums", model, library, filter, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
model: Model(Album, Nil),
|
model: Model(Album, Nil),
|
||||||
library: Library,
|
library: Library,
|
||||||
|
|
|
@ -25,9 +25,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
header_view,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
artist_utils.sort_by_name,
|
artist_utils.sort_by_name,
|
||||||
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
)
|
)
|
||||||
|
@ -63,6 +65,20 @@ fn search_filter(item: Artist, search_text: String) {
|
||||||
string.contains(item.name_lower, search_text)
|
string.contains(item.name_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_view(
|
||||||
|
model: Model(Artist, Nil),
|
||||||
|
library: Library,
|
||||||
|
filter: Nil,
|
||||||
|
items: List(LibraryItem(Artist)),
|
||||||
|
) {
|
||||||
|
let tracks =
|
||||||
|
list.fold(items, [], fn(acc, artist) {
|
||||||
|
list.concat([acc, get_tracks(library, artist.1)])
|
||||||
|
})
|
||||||
|
|
||||||
|
library_view.stats_header("Artists", model, library, filter, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
model: Model(Artist, Nil),
|
model: Model(Artist, Nil),
|
||||||
_library: Library,
|
_library: Library,
|
||||||
|
|
|
@ -20,9 +20,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
library_view.empty_header,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
order.noop,
|
order.noop,
|
||||||
|
option.None,
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,10 @@ import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
|
import gleam/int
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
|
import lustre/element.{text}
|
||||||
|
import lustre/element/html.{div, h1, p}
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
import elekf/library/track_utils
|
||||||
|
@ -13,6 +16,7 @@ import elekf/web/components/library_view
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
import elekf/web/components/library_item.{type LibraryItem}
|
||||||
import elekf/web/components/library_views/track_item
|
import elekf/web/components/library_views/track_item
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
|
import elekf/web/components/track_length
|
||||||
|
|
||||||
const component_name = "single-album-view"
|
const component_name = "single-album-view"
|
||||||
|
|
||||||
|
@ -21,9 +25,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
header_view,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
track_utils.sort_by_track_number,
|
track_utils.sort_by_track_number,
|
||||||
|
option.None,
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.from_list([#("album-id", id_decode)]),
|
dict.from_list([#("album-id", id_decode)]),
|
||||||
)
|
)
|
||||||
|
@ -61,6 +67,38 @@ fn search_filter(item: Track, search_text: String) {
|
||||||
string.contains(item.title_lower, search_text)
|
string.contains(item.title_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_view(
|
||||||
|
model: library_view.Model(Track, Int),
|
||||||
|
library: Library,
|
||||||
|
filter: Int,
|
||||||
|
items: List(LibraryItem(Track)),
|
||||||
|
) {
|
||||||
|
case library.get_album(library, filter) {
|
||||||
|
Ok(album) -> {
|
||||||
|
let #(tracks, seconds) =
|
||||||
|
list.fold(items, #(0, 0), fn(acc, item) {
|
||||||
|
let #(_i, track) = item
|
||||||
|
let #(tracks, seconds) = acc
|
||||||
|
#(tracks + 1, seconds + track.length)
|
||||||
|
})
|
||||||
|
let duration =
|
||||||
|
track_length.humanize_length(
|
||||||
|
seconds,
|
||||||
|
track_length.Auto,
|
||||||
|
track_length.short_delimiters,
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
div([attribute.id("library-list-stats-header")], [
|
||||||
|
h1([], [text(album.name)]),
|
||||||
|
p([], [text(int.to_string(tracks) <> " tracks, " <> duration)]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Error(_) -> library_view.empty_header(model, library, filter, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
_model: library_view.Model(Track, Int),
|
_model: library_view.Model(Track, Int),
|
||||||
library: Library,
|
library: Library,
|
||||||
|
|
|
@ -5,7 +5,11 @@ import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
|
import gleam/set
|
||||||
|
import gleam/int
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
|
import lustre/element.{text}
|
||||||
|
import lustre/element/html.{div, h1, p}
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
import elekf/library/track_utils
|
||||||
|
@ -13,6 +17,7 @@ import elekf/web/components/library_view
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
import elekf/web/components/library_item.{type LibraryItem}
|
||||||
import elekf/web/components/library_views/track_item
|
import elekf/web/components/library_views/track_item
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
|
import elekf/web/components/track_length
|
||||||
|
|
||||||
const component_name = "single-artist-view"
|
const component_name = "single-artist-view"
|
||||||
|
|
||||||
|
@ -21,9 +26,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
header_view,
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
track_utils.sort_by_name,
|
track_utils.sort_by_name,
|
||||||
|
option.None,
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.from_list([#("artist-id", id_decode)]),
|
dict.from_list([#("artist-id", id_decode)]),
|
||||||
)
|
)
|
||||||
|
@ -42,8 +49,8 @@ pub fn render(
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_getter(library: Library, artist_id: Int) {
|
fn data_getter(library: Library, filter: Int) {
|
||||||
let artist = library.assert_artist(library, artist_id)
|
let artist = library.assert_artist(library, filter)
|
||||||
list.map(artist.tracks, fn(track_id) {
|
list.map(artist.tracks, fn(track_id) {
|
||||||
#(track_id, library.assert_track(library, track_id))
|
#(track_id, library.assert_track(library, track_id))
|
||||||
})
|
})
|
||||||
|
@ -58,6 +65,50 @@ fn search_filter(item: Track, search_text: String) {
|
||||||
string.contains(item.title_lower, search_text)
|
string.contains(item.title_lower, search_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_view(
|
||||||
|
model: library_view.Model(Track, Int),
|
||||||
|
library: Library,
|
||||||
|
filter: Int,
|
||||||
|
items: List(LibraryItem(Track)),
|
||||||
|
) {
|
||||||
|
case library.get_artist(library, filter) {
|
||||||
|
Ok(artist) -> {
|
||||||
|
let #(tracks, seconds, albums) =
|
||||||
|
list.fold(items, #(0, 0, set.new()), fn(acc, item) {
|
||||||
|
let #(_i, track) = item
|
||||||
|
let #(tracks, seconds, albums) = acc
|
||||||
|
#(
|
||||||
|
tracks + 1,
|
||||||
|
seconds + track.length,
|
||||||
|
set.insert(albums, track.album_id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
let duration =
|
||||||
|
track_length.humanize_length(
|
||||||
|
seconds,
|
||||||
|
track_length.Auto,
|
||||||
|
track_length.short_delimiters,
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
div([attribute.id("library-list-stats-header")], [
|
||||||
|
h1([], [text(artist.name)]),
|
||||||
|
p([], [
|
||||||
|
text(
|
||||||
|
int.to_string(tracks)
|
||||||
|
<> " tracks in "
|
||||||
|
<> int.to_string(set.size(albums))
|
||||||
|
<> " albums, "
|
||||||
|
<> duration,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Error(_) -> library_view.empty_header(model, library, filter, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
_model: library_view.Model(Track, Int),
|
_model: library_view.Model(Track, Int),
|
||||||
library: Library,
|
library: Library,
|
||||||
|
|
|
@ -20,9 +20,11 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
fn(m, l, f, i) { library_view.stats_header("Tracks", m, l, f, i) },
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
track_utils.sort_by_name,
|
track_utils.sort_by_name,
|
||||||
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
|
import lustre/element.{text}
|
||||||
import lustre/element/html.{input, p}
|
import lustre/element/html.{input, p}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/event
|
import lustre/event
|
||||||
|
@ -27,7 +28,7 @@ pub fn view(model: model.Model) {
|
||||||
attribute.class("player-time"),
|
attribute.class("player-time"),
|
||||||
attribute.attribute("aria-label", "Time elapsed"),
|
attribute.attribute("aria-label", "Time elapsed"),
|
||||||
],
|
],
|
||||||
[track_length(model.position, current_time_padding)],
|
[text(track_length(model.position, current_time_padding))],
|
||||||
),
|
),
|
||||||
p(
|
p(
|
||||||
[
|
[
|
||||||
|
@ -35,7 +36,7 @@ pub fn view(model: model.Model) {
|
||||||
attribute.class("player-time"),
|
attribute.class("player-time"),
|
||||||
attribute.attribute("aria-label", "Total time"),
|
attribute.attribute("aria-label", "Total time"),
|
||||||
],
|
],
|
||||||
[track_length(model.track.length, track_length.Auto)],
|
[text(track_length(model.track.length, track_length.Auto))],
|
||||||
),
|
),
|
||||||
input([
|
input([
|
||||||
attribute.id("player-track"),
|
attribute.id("player-track"),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//// The search view renders a search bar, other views must do the searching
|
//// The search view renders a search bar, other views must do the searching
|
||||||
//// based on the emitted messages.
|
//// based on the emitted messages.
|
||||||
|
|
||||||
|
import gleam/dynamic
|
||||||
import lustre/element/html.{div, input}
|
import lustre/element/html.{div, input}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/event
|
import lustre/event
|
||||||
|
@ -50,6 +51,7 @@ pub fn view(model: Model) {
|
||||||
attribute.type_("search"),
|
attribute.type_("search"),
|
||||||
attribute.placeholder("Search"),
|
attribute.placeholder("Search"),
|
||||||
attribute.attribute("aria-label", "Search through content"),
|
attribute.attribute("aria-label", "Search through content"),
|
||||||
|
attribute.value(dynamic.from(model.search_text)),
|
||||||
event.on_input(UpdateSearch),
|
event.on_input(UpdateSearch),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
import lustre/element/html.{div, h3}
|
import lustre/element/html.{div}
|
||||||
import lustre/element.{text}
|
import lustre/element.{text}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import elekf/web/components/icon
|
import elekf/web/components/icon
|
||||||
|
|
||||||
pub fn view(extra_attrs: List(attribute.Attribute(a))) {
|
pub fn view(extra_attrs: List(attribute.Attribute(a))) {
|
||||||
div(
|
div(
|
||||||
[attribute.class("library-item library-list-shuffle-all")],
|
[attribute.class("library-item library-list-shuffle-all"), ..extra_attrs],
|
||||||
[
|
[icon.icon("shuffle", icon.Hidden), text(" Shuffle all")],
|
||||||
h3(
|
|
||||||
[attribute.class("library-item-title"), ..extra_attrs],
|
|
||||||
[icon.icon("shuffle", icon.Hidden), text(" Shuffle all")],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import lustre/element.{text}
|
|
||||||
|
|
||||||
/// Use to select padding mode.
|
/// Use to select padding mode.
|
||||||
pub type PadTo {
|
pub type PadTo {
|
||||||
|
@ -10,17 +9,45 @@ pub type PadTo {
|
||||||
Hours
|
Hours
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a track length (in seconds) as a formatted time duration.
|
/// Delimiters to use when humanizing a track length.
|
||||||
pub fn track_length(length: Int, padding: PadTo) {
|
pub type Delimiters {
|
||||||
text(humanize_length(length, padding))
|
Delimiters(hours: String, minutes: String, seconds: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn humanize_length(length: Int, padding: PadTo) -> String {
|
/// Colons delimiting the duration, in the form of `1:23:45`.
|
||||||
|
pub const colon_delimiters = Delimiters(hours: ":", minutes: ":", seconds: "")
|
||||||
|
|
||||||
|
/// Short units delimiting the duration, in the form of `1 h 23 min 45 s.`.
|
||||||
|
pub const short_delimiters = Delimiters(
|
||||||
|
hours: " h ",
|
||||||
|
minutes: " min ",
|
||||||
|
seconds: " s",
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Get a track length (in seconds) as a formatted time duration with colon
|
||||||
|
/// delimiters.
|
||||||
|
pub fn track_length(length: Int, padding: PadTo) {
|
||||||
|
humanize_length(length, padding, colon_delimiters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a track length (in seconds) as a formatted time duration.
|
||||||
|
pub fn humanize_length(
|
||||||
|
length: Int,
|
||||||
|
padding: PadTo,
|
||||||
|
delimiters: Delimiters,
|
||||||
|
) -> String {
|
||||||
case length {
|
case length {
|
||||||
l if l < 60 -> {
|
l if l < 60 -> {
|
||||||
case padding {
|
case padding {
|
||||||
Auto -> "00:" <> pad_00(length)
|
Auto ->
|
||||||
Hours -> "0:00:" <> pad_00(length)
|
"00" <> delimiters.minutes <> pad_00(length) <> delimiters.seconds
|
||||||
|
Hours ->
|
||||||
|
"0"
|
||||||
|
<> delimiters.hours
|
||||||
|
<> "00"
|
||||||
|
<> delimiters.minutes
|
||||||
|
<> pad_00(length)
|
||||||
|
<> delimiters.seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l if l < 3600 -> {
|
l if l < 3600 -> {
|
||||||
|
@ -29,17 +56,26 @@ fn humanize_length(length: Int, padding: PadTo) -> String {
|
||||||
|
|
||||||
let prefix = case padding {
|
let prefix = case padding {
|
||||||
Auto -> ""
|
Auto -> ""
|
||||||
Hours -> "0:"
|
Hours -> "0" <> delimiters.hours
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix <> pad_00(mins) <> ":" <> pad_00(secs)
|
prefix
|
||||||
|
<> pad_00(mins)
|
||||||
|
<> delimiters.minutes
|
||||||
|
<> pad_00(secs)
|
||||||
|
<> delimiters.seconds
|
||||||
}
|
}
|
||||||
l -> {
|
l -> {
|
||||||
let hours = l / 3600
|
let hours = l / 3600
|
||||||
let hours_secs = hours * 3600
|
let hours_secs = hours * 3600
|
||||||
let mins = { l - hours_secs } / 60
|
let mins = { l - hours_secs } / 60
|
||||||
let secs = l - hours_secs - mins * 60
|
let secs = l - hours_secs - mins * 60
|
||||||
int.to_string(hours) <> ":" <> pad_00(mins) <> ":" <> pad_00(secs)
|
int.to_string(hours)
|
||||||
|
<> delimiters.hours
|
||||||
|
<> pad_00(mins)
|
||||||
|
<> delimiters.minutes
|
||||||
|
<> pad_00(secs)
|
||||||
|
<> delimiters.seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,9 @@ pub fn update(model, msg) {
|
||||||
),
|
),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
|
_ -> {
|
||||||
|
panic as { "Login view got unknown message: " <> string.inspect(msg) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +94,8 @@ pub fn view(model: Model) {
|
||||||
attribute.type_("submit"),
|
attribute.type_("submit"),
|
||||||
attribute.disabled(
|
attribute.disabled(
|
||||||
model.logging_in
|
model.logging_in
|
||||||
|| model.device_name == ""
|
|| model.device_name == ""
|
||||||
|| model.login_token == "",
|
|| model.login_token == "",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[text("Log in")],
|
[text("Log in")],
|
||||||
|
|
|
@ -4,4 +4,6 @@ export function getElem(id) {
|
||||||
|
|
||||||
export function focus(elem) {
|
export function focus(elem) {
|
||||||
elem.focus();
|
elem.focus();
|
||||||
|
elem.selectionStart = elem.value.length;
|
||||||
|
elem.selectionEnd = elem.value.length;
|
||||||
}
|
}
|
||||||
|
|
29
style.css
29
style.css
|
@ -416,13 +416,31 @@ single-album-view {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#library-list-header {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#library-list-header h1 {
|
||||||
|
font-weight: 100;
|
||||||
|
margin: var(--double-margin) 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#library-list-header p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: var(--side-margin) 0;
|
||||||
|
}
|
||||||
|
|
||||||
#authed-view-wrapper[data-player-status="open"] #library-list {
|
#authed-view-wrapper[data-player-status="open"] #library-list {
|
||||||
padding-bottom: 100vh;
|
padding-bottom: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#artists-view #library-list,
|
#artists-view #library-list,
|
||||||
#albums-view #library-list,
|
#albums-view #library-list,
|
||||||
#single-artist-view #library-list {
|
#single-artist-view #library-list,
|
||||||
|
#tracks-view #library-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
grid-auto-flow: dense;
|
grid-auto-flow: dense;
|
||||||
|
@ -434,14 +452,15 @@ single-album-view {
|
||||||
padding: var(--double-margin);
|
padding: var(--double-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item {
|
#tracks-view #library-list {
|
||||||
padding-left: var(--side-margin);
|
grid-template-columns: 1fr;
|
||||||
padding-right: var(--side-margin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-list-shuffle-all {
|
.library-list-shuffle-all {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
padding: var(--double-margin) var(--side-margin);
|
padding: var(--double-margin) 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-item-thumbnail {
|
.library-item-thumbnail {
|
||||||
|
|
Loading…
Reference in a new issue