New fancy header, bump to 1.0.0, fix artist shuffling

This commit is contained in:
Mikko Ahlroth 2024-03-07 20:33:03 +02:00
parent 4f869df09f
commit eb30b0f971
17 changed files with 306 additions and 61 deletions

View file

@ -1,2 +1,2 @@
gleam 0.34.1
gleam 1.0.0
nodejs 20.10.0

View file

@ -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 = "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_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_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_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 = "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 = "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 = "plinth", version = "0.1.9", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_javascript"], otp_app = "plinth", source = "hex", outer_checksum = "89BE43DC719539A676A9515C619315A2A7188A6FF5D7499F8261241BC0B2F1A9" },
{ 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 = ["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_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 = "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 = "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]

View file

@ -296,6 +296,7 @@ pub fn view(model: Model) {
artists_view.render(library_if_loaded(model), model.settings, [
attribute.id("artists-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
library_view.Albums ->
albums_view.render(library_if_loaded(model), model.settings, [

View file

@ -8,10 +8,12 @@ import gleam/dict
import gleam/option
import gleam/string
import gleam/result
import gleam/int
import gleam/set
import lustre
import lustre/effect
import lustre/element.{text}
import lustre/element/html.{div}
import lustre/element.{type Element, text}
import lustre/element/html.{div, h1, p}
import lustre/attribute
import lustre/event
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/library_item.{type LibraryItem}
import elekf/web/components/shuffle_all
import elekf/web/components/track_length
import elekf/web/common
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) =
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.
///
/// 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.
pub type ItemView(a, filter) =
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.
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
/// A filter that might not yet be loaded. If it's not loaded, no data should be
/// fetched yet.
pub type Filter(filter) =
option.Option(filter)
pub opaque type LibraryStatus {
HaveLibrary(library: Library)
@ -122,17 +123,19 @@ pub type Model(a, filter) {
pub fn register(
name: String,
data_getter: DataGetter(a, filter),
header_view: HeaderView(a, filter),
item_view: ItemView(a, filter),
shuffler: Shuffler(a),
sorter: Sorter(a),
filter: Filter(filter),
search_filter: SearchFilter(a),
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
) {
lustre.component(
name,
fn() { init(name, FilterNotLoaded, data_getter, shuffler, sorter) },
fn() { init(name, filter, data_getter, shuffler, sorter) },
update,
generate_view(item_view, search_filter),
generate_view(header_view, item_view, search_filter),
dict.merge(generic_attributes(), extra_attrs),
)
}
@ -220,7 +223,7 @@ pub fn update(model: Model(a, filter), msg) {
}
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())
}
@ -245,11 +248,12 @@ pub fn update(model: Model(a, filter), msg) {
pub fn library_view(
model: Model(a, filter),
header_view: HeaderView(a, filter),
item_view: ItemView(a, filter),
search_filter: SearchFilter(a),
) {
case model.library_status, model.filter == FilterNotLoaded {
HaveLibrary(lib), False -> {
case model.library_status, model.filter {
HaveLibrary(lib), option.Some(f) -> {
let items = case model.search.search_text {
"" -> model.data
txt -> {
@ -264,13 +268,17 @@ pub fn library_view(
list.append(
[
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)]),
],
list.index_map(items, fn(item, i) {
item_view(model, lib, items, i, item)
})
|> list.flatten(),
item_view(model, lib, items, i, item)
})
|> list.flatten(),
),
)
}
@ -282,8 +290,61 @@ pub fn library_view(
}
}
fn generate_view(item_view: ItemView(a, filter), search_filter: SearchFilter(a)) {
library_view(_, item_view, search_filter)
/// An empty header view that renders nothing.
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)) {
@ -311,7 +372,7 @@ fn settings_decode(data: dynamic.Dynamic) {
fn update_data(model: Model(a, filter)) {
case model.library_status, model.filter {
HaveLibrary(l), Filter(f) ->
HaveLibrary(l), option.Some(f) ->
Model(
..model,
data: l

View file

@ -20,9 +20,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
header_view,
item_view,
shuffler,
album_utils.sort_by_name,
option.Some(Nil),
search_filter,
dict.new(),
)
@ -52,6 +54,20 @@ fn search_filter(item: Album, search_text: String) {
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(
model: Model(Album, Nil),
library: Library,

View file

@ -25,9 +25,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
header_view,
item_view,
shuffler,
artist_utils.sort_by_name,
option.Some(Nil),
search_filter,
dict.new(),
)
@ -63,6 +65,20 @@ fn search_filter(item: Artist, search_text: String) {
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(
model: Model(Artist, Nil),
_library: Library,

View file

@ -20,9 +20,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
library_view.empty_header,
item_view,
shuffler,
order.noop,
option.None,
search_filter,
dict.new(),
)

View file

@ -5,7 +5,10 @@ import gleam/list
import gleam/dict
import gleam/option
import gleam/dynamic
import gleam/int
import lustre/attribute
import lustre/element.{text}
import lustre/element/html.{div, h1, p}
import elekf/library.{type Library}
import elekf/library/track.{type Track}
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_views/track_item
import elekf/web/common
import elekf/web/components/track_length
const component_name = "single-album-view"
@ -21,9 +25,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
header_view,
item_view,
shuffler,
track_utils.sort_by_track_number,
option.None,
search_filter,
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)
}
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(
_model: library_view.Model(Track, Int),
library: Library,

View file

@ -5,7 +5,11 @@ import gleam/list
import gleam/dict
import gleam/option
import gleam/dynamic
import gleam/set
import gleam/int
import lustre/attribute
import lustre/element.{text}
import lustre/element/html.{div, h1, p}
import elekf/library.{type Library}
import elekf/library/track.{type Track}
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_views/track_item
import elekf/web/common
import elekf/web/components/track_length
const component_name = "single-artist-view"
@ -21,9 +26,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
header_view,
item_view,
shuffler,
track_utils.sort_by_name,
option.None,
search_filter,
dict.from_list([#("artist-id", id_decode)]),
)
@ -42,8 +49,8 @@ pub fn render(
])
}
fn data_getter(library: Library, artist_id: Int) {
let artist = library.assert_artist(library, artist_id)
fn data_getter(library: Library, filter: Int) {
let artist = library.assert_artist(library, filter)
list.map(artist.tracks, fn(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)
}
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(
_model: library_view.Model(Track, Int),
library: Library,

View file

@ -20,9 +20,11 @@ pub fn register() {
library_view.register(
component_name,
data_getter,
fn(m, l, f, i) { library_view.stats_header("Tracks", m, l, f, i) },
item_view,
shuffler,
track_utils.sort_by_name,
option.Some(Nil),
search_filter,
dict.new(),
)

View file

@ -3,6 +3,7 @@
import gleam/int
import gleam/dynamic
import lustre/element.{text}
import lustre/element/html.{input, p}
import lustre/attribute
import lustre/event
@ -27,7 +28,7 @@ pub fn view(model: model.Model) {
attribute.class("player-time"),
attribute.attribute("aria-label", "Time elapsed"),
],
[track_length(model.position, current_time_padding)],
[text(track_length(model.position, current_time_padding))],
),
p(
[
@ -35,7 +36,7 @@ pub fn view(model: model.Model) {
attribute.class("player-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([
attribute.id("player-track"),

View file

@ -1,6 +1,7 @@
//// The search view renders a search bar, other views must do the searching
//// based on the emitted messages.
import gleam/dynamic
import lustre/element/html.{div, input}
import lustre/attribute
import lustre/event
@ -50,6 +51,7 @@ pub fn view(model: Model) {
attribute.type_("search"),
attribute.placeholder("Search"),
attribute.attribute("aria-label", "Search through content"),
attribute.value(dynamic.from(model.search_text)),
event.on_input(UpdateSearch),
]),
])

View file

@ -1,16 +1,11 @@
import lustre/element/html.{div, h3}
import lustre/element/html.{div}
import lustre/element.{text}
import lustre/attribute
import elekf/web/components/icon
pub fn view(extra_attrs: List(attribute.Attribute(a))) {
div(
[attribute.class("library-item library-list-shuffle-all")],
[
h3(
[attribute.class("library-item-title"), ..extra_attrs],
[icon.icon("shuffle", icon.Hidden), text(" Shuffle all")],
),
],
[attribute.class("library-item library-list-shuffle-all"), ..extra_attrs],
[icon.icon("shuffle", icon.Hidden), text(" Shuffle all")],
)
}

View file

@ -1,6 +1,5 @@
import gleam/int
import gleam/string
import lustre/element.{text}
/// Use to select padding mode.
pub type PadTo {
@ -10,17 +9,45 @@ pub type PadTo {
Hours
}
/// Get a track length (in seconds) as a formatted time duration.
pub fn track_length(length: Int, padding: PadTo) {
text(humanize_length(length, padding))
/// Delimiters to use when humanizing a track length.
pub type Delimiters {
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 {
l if l < 60 -> {
case padding {
Auto -> "00:" <> pad_00(length)
Hours -> "0:00:" <> pad_00(length)
Auto ->
"00" <> delimiters.minutes <> pad_00(length) <> delimiters.seconds
Hours ->
"0"
<> delimiters.hours
<> "00"
<> delimiters.minutes
<> pad_00(length)
<> delimiters.seconds
}
}
l if l < 3600 -> {
@ -29,17 +56,26 @@ fn humanize_length(length: Int, padding: PadTo) -> String {
let prefix = case padding {
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 -> {
let hours = l / 3600
let hours_secs = hours * 3600
let mins = { l - hours_secs } / 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
}
}
}

View file

@ -63,6 +63,9 @@ pub fn update(model, msg) {
),
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.disabled(
model.logging_in
|| model.device_name == ""
|| model.login_token == "",
|| model.device_name == ""
|| model.login_token == "",
),
],
[text("Log in")],

View file

@ -4,4 +4,6 @@ export function getElem(id) {
export function focus(elem) {
elem.focus();
elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length;
}

View file

@ -416,13 +416,31 @@ single-album-view {
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 {
padding-bottom: 100vh;
}
#artists-view #library-list,
#albums-view #library-list,
#single-artist-view #library-list {
#single-artist-view #library-list,
#tracks-view #library-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-auto-flow: dense;
@ -434,14 +452,15 @@ single-album-view {
padding: var(--double-margin);
}
.track-item {
padding-left: var(--side-margin);
padding-right: var(--side-margin);
#tracks-view #library-list {
grid-template-columns: 1fr;
}
.library-list-shuffle-all {
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 {