Add single album view

This commit is contained in:
Mikko Ahlroth 2024-02-27 10:17:03 +02:00
parent 451b619f5b
commit 830918c344
13 changed files with 421 additions and 197 deletions

View file

@ -25,16 +25,15 @@ import elekf/library/track.{type Track}
import elekf/transfer/library as library_transfer
import elekf/web/router
import elekf/web/components/player
import elekf/web/components/library_item.{type LibraryItem}
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/components/library_views/single_artist_view
import elekf/web/components/library_views/single_album_view
import elekf/web/components/button_group
import elekf/web/components/link
import elekf/web/events/start_play
import elekf/web/events/show_artist
import elekf/web/utils
import elekf/web/components/icon.{Alt, icon}
import elektrofoni
@ -83,20 +82,29 @@ pub type Msg {
}
pub fn init(auth_data: common.AuthData) {
let model =
Model(
loading_library: True,
library: library.empty(),
settings: option.None,
request_config: form_request_config(auth_data),
play_status: NoTracks,
view: library_view.Tracks,
)
let initial_library = library.empty()
let router_effect =
effect.from(fn(dispatch) { router.init(dispatch) })
|> effect.map(Router)
let initial_view =
router.get_current_path()
|> router.parse()
|> result.unwrap(router.TrackList)
|> route_to_view()
|> result.unwrap(library_view.Tracks)
let model =
Model(
loading_library: True,
library: initial_library,
settings: option.None,
request_config: form_request_config(auth_data),
play_status: NoTracks,
view: initial_view,
)
#(model, effect.batch([load_library(model), router_effect]))
}
@ -183,8 +191,14 @@ pub fn update(model: Model, msg) {
})
}
Router(router.RouteChanged(route)) -> {
let view = result.unwrap(route_to_view(route), library_view.Tracks)
#(Model(..model, view: view), effect.none())
case route_to_view(route) {
Ok(view) -> #(Model(..model, view: view), effect.none())
Error(_) -> {
io.println_error("Unable to change to route:")
io.debug(route)
#(model, effect.none())
}
}
}
}
}
@ -205,13 +219,13 @@ pub fn view(model: Model) {
div([attribute.id("authed-view-library")], [
nav([attribute.id("library-top-nav")], [
button_group.view("", [], [
link.view(router.to_hash(router.TrackList), "", [], [
link.button(router.to_hash(router.TrackList), "", [], [
icon("music-note-beamed", Alt("Tracks")),
]),
link.view(router.to_hash(router.ArtistList), "", [], [
link.button(router.to_hash(router.ArtistList), "", [], [
icon("file-person", Alt("Artists")),
]),
link.view(router.to_hash(router.AlbumList), "", [], [
link.button(router.to_hash(router.AlbumList), "", [], [
icon("disc", Alt("Albums")),
]),
]),
@ -234,12 +248,18 @@ pub fn view(model: Model) {
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
library_view.SingleArtist(artist_info) ->
single_artist_view.render(model.library, artist_info, model.settings, [
library_view.SingleArtist(id) ->
single_artist_view.render(model.library, id, model.settings, [
attribute.id("single-artist-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
library_view.SingleAlbum(id) ->
single_album_view.render(model.library, id, model.settings, [
attribute.id("single-album-view"),
attribute.class("glass-bg"),
start_play.on(StartPlay),
])
},
]),
div(
@ -382,5 +402,10 @@ fn route_to_view(route: router.Route) {
router.TrackList -> Ok(library_view.Tracks)
router.ArtistList -> Ok(library_view.Artists)
router.AlbumList -> Ok(library_view.Albums)
router.Artist(id) -> Ok(library_view.SingleArtist(id))
// TODO
router.Album(id) -> Ok(library_view.SingleAlbum(id))
// TODO
router.Settings -> Error(Nil)
}
}

View file

@ -4,7 +4,7 @@
import gleam/dynamic
import gleam/list
import gleam/map
import gleam/dict
import gleam/option
import gleam/string
import lustre
@ -13,7 +13,7 @@ import lustre/element
import lustre/element/html.{div}
import lustre/attribute
import lustre/event
import elekf/utils/order.{type Sorter} as order_utils
import elekf/utils/order.{type Sorter}
import elekf/library.{type Library}
import elekf/library/track.{type Track}
import elekf/library/artist.{type Artist}
@ -54,11 +54,11 @@ pub type View {
/// All albums.
Albums
/// Tracks of a single album.
/// SingleAlbum(Int, Album)
SingleAlbum(Int)
/// All artists.
Artists
/// Albums of a single artist.
SingleArtist(LibraryItem(Artist))
SingleArtist(Int)
/// Tracks of a single artist.
/// SingleArtistTracks(Int, Artist)
/// All tracks.
@ -73,11 +73,13 @@ pub type Msg {
ShowArtist(LibraryItem(Artist))
Search(search.Msg)
AlbumExpandToggled(LibraryItem(Album))
FilterUpdated
}
pub type Model(a) {
Model(
library: Library,
library_loading: Bool,
data: List(LibraryItem(a)),
data_getter: DataGetter(a),
shuffler: Shuffler(a),
@ -106,7 +108,7 @@ pub fn register(
) {
lustre.component(
name,
fn() { init(library.empty(), data_getter, shuffler, sorter) },
fn() { init(library.empty(), True, data_getter, shuffler, sorter) },
update,
generate_view(item_view, search_filter),
generic_attributes(),
@ -116,7 +118,7 @@ pub fn register(
/// Get the generic properties common to all library views, that can be input
/// into the Lustre component attribute change
pub fn generic_attributes() {
map.from_list([#("library", library_decode), #("settings", settings_decode)])
dict.from_list([#("library", library_decode), #("settings", settings_decode)])
}
/// Render the component using a custom element.
@ -137,10 +139,11 @@ pub fn render(
)
}
pub fn init(library, data_getter, shuffler, sorter) {
pub fn init(library, library_loading, data_getter, shuffler, sorter) {
#(
Model(
library,
library_loading,
data_getter(library),
data_getter,
shuffler,
@ -155,13 +158,8 @@ pub fn init(library, data_getter, shuffler, sorter) {
pub fn update(model, msg) {
case msg {
LibraryUpdated(library) -> #(
Model(
..model,
library: library,
data: library
|> model.data_getter()
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
),
Model(..model, library: library, library_loading: False)
|> update_data(library),
effect.none(),
)
SettingsUpdated(settings) -> #(
@ -178,6 +176,8 @@ pub fn update(model, msg) {
// Base case, this should be handled in an implementing view
AlbumExpandToggled(_album) -> #(model, effect.none())
FilterUpdated -> #(update_data(model, model.library), effect.none())
}
}
@ -227,3 +227,12 @@ fn settings_decode(data: dynamic.Dynamic) {
let settings: option.Option(common.Settings) = dynamic.unsafe_coerce(data)
Ok(SettingsUpdated(settings))
}
fn update_data(model: Model(a), library: Library) {
Model(
..model,
data: library
|> model.data_getter()
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
)
}

View file

@ -17,11 +17,9 @@ import elekf/web/components/library_item.{type LibraryItem}
import elekf/web/components/library_views/track_item
import elekf/web/components/thumbnail
import elekf/web/components/shuffle_all
import elekf/web/components/link
import elekf/web/common.{type Settings}
const base_classes = "library-item album-item"
const showing_tracks_class = "library-item-expanded"
import elekf/web/router
pub fn view(
library: Library,
@ -37,32 +35,12 @@ pub fn view(
}
let assert Ok(first_track) = list.first(tracks)
let class = case show_tracks {
True -> base_classes <> " " <> showing_tracks_class
False -> base_classes
}
let expanded = case show_tracks {
True -> "true"
False -> "false"
}
list.append(
[
div(
[
attribute.id("album-list-" <> int.to_string(album_id)),
attribute.class(class),
],
[
div(
[
attribute.class("album-item-expander"),
event.on_click(AlbumExpandToggled(item)),
event.on_keydown(fn(_) { AlbumExpandToggled(item) }),
attribute.attribute("role", "button"),
attribute.attribute("aria-expanded", expanded),
attribute.attribute("tabindex", "0"),
],
link.link(
router.to_hash(router.Album(album_id)),
"library-item album-item",
[attribute.id("album-list-" <> int.to_string(album_id))],
[
thumbnail.maybe_item_thumbnail(
settings,
@ -77,8 +55,6 @@ pub fn view(
],
),
],
),
],
case show_tracks {
True -> [view_tracks(library, tracks)]
False -> []
@ -99,7 +75,7 @@ pub fn view_tracks(library: Library, tracks: List(LibraryItem(Track))) {
list.flatten([
[shuffle_all.view([event.on_click(ShuffleAll)])],
list.index_map(tracks, fn(track_item, index) {
track_item.view(library, tracks, track_item, index, "album-tracks-list")
track_item.view(library, tracks, index, track_item, "album-tracks-list")
})
|> list.flatten(),
]),

View file

@ -60,6 +60,7 @@ fn init() {
let #(lib_m, lib_e) =
library_view.init(
library.empty(),
True,
data_getter,
shuffler,
album_utils.sort_by_name,

View file

@ -5,17 +5,18 @@ import gleam/int
import gleam/list
import gleam/dict
import gleam/option
import lustre/element/html.{div, h3, p}
import lustre/element/html.{h3, p}
import lustre/element.{text}
import lustre/attribute
import lustre/event
import elekf/library.{type Library}
import elekf/library/artist.{type Artist}
import elekf/library/artist_utils
import elekf/web/components/library_view.{type Model, ShowArtist}
import elekf/web/components/library_view.{type Model}
import elekf/web/components/library_item.{type LibraryItem}
import elekf/web/components/thumbnail
import elekf/web/common
import elekf/web/router
import elekf/web/components/link
const component_name = "artists-view"
@ -69,14 +70,10 @@ fn item_view(
) {
let #(artist_id, artist) = item
[
div(
[
attribute.id("artist-list-" <> int.to_string(artist_id)),
attribute.class("library-item artist-item"),
attribute.type_("button"),
event.on_click(ShowArtist(item)),
attribute.attribute("role", "button"),
],
link.link(
router.to_hash(router.Artist(artist_id)),
"library-item artist-item",
[attribute.id("artist-list-" <> int.to_string(artist_id))],
[
thumbnail.maybe_item_thumbnail(
model.settings,

View file

@ -0,0 +1,200 @@
//// A library view to a single album's tracks.
import gleam/string
import gleam/list
import gleam/dict
import gleam/option
import gleam/dynamic
import gleam/result
import gleam/int
import lustre
import lustre/element/html.{div, header}
import lustre/element.{text}
import lustre/attribute
import lustre/effect
import elekf/library.{type Library}
import elekf/library/album.{type Album}
import elekf/library/track.{type Track}
import elekf/library/track_utils
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
const component_name = "single-album-view"
type Model {
Model(
library_view: library_view.Model(Track),
album_id: Int,
album: option.Option(Album),
)
}
type Msg {
LibraryViewMsg(library_view.Msg)
AlbumUpdated(Int)
}
/// Register the single album view as a custom element.
pub fn register() {
lustre.component(
component_name,
init,
update,
generate_view(search_filter),
dict.merge(
library_view.generic_attributes()
|> dict.map_values(fn(_key, decoder) {
fn(data: dynamic.Dynamic) {
data
|> decoder()
|> result.map(LibraryViewMsg)
}
}),
dict.from_list([#("album-id", id_decode)]),
),
)
}
/// Render the single album view.
pub fn render(
library: Library,
album_id: Int,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
library_view.render(component_name, library, settings, [
attribute.property("album-id", album_id),
..extra_attrs
])
}
fn init() {
let #(lib_m, lib_e) =
library_view.init(
library.empty(),
True,
fn(_) -> List(LibraryItem(Track)) { [] },
shuffler,
track_utils.sort_by_name,
)
#(
Model(album_id: library.invalid_id, album: option.None, library_view: lib_m),
effect.map(lib_e, LibraryViewMsg),
)
}
fn update(model: Model, msg) {
case msg {
AlbumUpdated(id) -> {
let new_getter = data_getter(_, id)
let album =
load_album(model.library_view.library, id)
|> option.from_result()
#(
Model(
album_id: id,
album: album,
library_view: library_view.Model(
..model.library_view,
data_getter: new_getter,
),
),
effect.map(
effect.from(fn(dispatch) { dispatch(library_view.FilterUpdated) }),
LibraryViewMsg,
),
)
}
LibraryViewMsg(lib_msg) -> {
let #(lib_m, lib_e) = library_view.update(model.library_view, lib_msg)
// Update album when library is updated
let album = case lib_msg {
library_view.LibraryUpdated(new_lib) ->
load_album(new_lib, model.album_id)
|> option.from_result()
_ -> model.album
}
#(
Model(..model, album: album, library_view: lib_m),
effect.map(lib_e, LibraryViewMsg),
)
}
}
}
fn data_getter(library: Library, album_id: Int) {
library.tracks
|> dict.fold([], fn(acc, key, val) {
case val.album_id == album_id {
True -> [#(key, val), ..acc]
False -> acc
}
})
}
fn shuffler(_library, items: List(LibraryItem(Track))) {
items
|> list.shuffle()
}
fn search_filter(item: Track, search_text: String) {
string.contains(item.title_lower, search_text)
}
fn generate_view(search_filter: library_view.SearchFilter(Track)) {
fn(model: Model) {
case model.library_view.library_loading, model.album {
True, _ ->
div([attribute.class("library-view-loading")], [
text("Loading library…"),
])
False, option.None ->
div([attribute.class("library-view-not-specified")], [
text("Album not found with ID: " <> int.to_string(model.album_id)),
])
False, option.Some(album) ->
view(model, #(model.album_id, album), search_filter)
}
}
}
fn view(
model: Model,
album: LibraryItem(Album),
search_filter: library_view.SearchFilter(Track),
) {
div([], [
header([attribute.class("library-header")], [text({ album.1 }.name)]),
library_view.library_view(
model.library_view,
fn(library, items, index, item) {
track_item.view(
library.library,
items,
index,
item,
"album-tracks-list",
)
},
search_filter,
)
|> element.map(LibraryViewMsg),
])
}
fn id_decode(data: dynamic.Dynamic) {
let album_id: Int = dynamic.unsafe_coerce(data)
Ok(AlbumUpdated(album_id))
}
fn load_album(library: Library, id: Int) {
library.get_album(library, id)
}

View file

@ -2,21 +2,21 @@
import gleam/string
import gleam/list
import gleam/map
import gleam/dict
import gleam/option
import gleam/dynamic
import gleam/result
import gleam/int
import lustre
import lustre/element/html.{div, header}
import lustre/element.{text}
import lustre/attribute
import lustre/effect
import elekf/utils/misc
import elekf/library.{type Library}
import elekf/library/album.{type Album}
import elekf/library/album_utils
import elekf/library/artist.{type Artist}
import elekf/web/components/library_view.{AlbumExpandToggled}
import elekf/web/components/library_view
import elekf/web/components/library_item.{type LibraryItem}
import elekf/web/components/library_views/album_item
import elekf/web/common
@ -26,14 +26,14 @@ const component_name = "single-artist-view"
type Model {
Model(
library_view: library_view.Model(Album),
artist: option.Option(LibraryItem(Artist)),
expanded_album: Int,
artist_id: Int,
artist: option.Option(Artist),
)
}
type Msg {
LibraryViewMsg(library_view.Msg)
ArtistUpdated(option.Option(LibraryItem(Artist)))
ArtistUpdated(Int)
}
/// Register the single artist view as a custom element.
@ -43,16 +43,16 @@ pub fn register() {
init,
update,
generate_view(search_filter),
map.merge(
dict.merge(
library_view.generic_attributes()
|> map.map_values(fn(_key, decoder) {
|> dict.map_values(fn(_key, decoder) {
fn(data: dynamic.Dynamic) {
data
|> decoder()
|> result.map(LibraryViewMsg)
}
}),
map.from_list([#("artist", artist_decode)]),
dict.from_list([#("artist-id", id_decode)]),
),
)
}
@ -60,22 +60,21 @@ pub fn register() {
/// Render the single artist view.
pub fn render(
library: Library,
artist: LibraryItem(Artist),
artist_id: Int,
settings: option.Option(common.Settings),
extra_attrs: List(attribute.Attribute(msg)),
) {
library_view.render(
component_name,
library,
settings,
[attribute.property("artist", option.Some(artist)), ..extra_attrs],
)
library_view.render(component_name, library, settings, [
attribute.property("artist-id", artist_id),
..extra_attrs
])
}
fn init() {
let #(lib_m, lib_e) =
library_view.init(
library.empty(),
True,
fn(_) -> List(LibraryItem(Album)) { [] },
shuffler,
album_utils.sort_by_year,
@ -83,8 +82,8 @@ fn init() {
#(
Model(
artist_id: library.invalid_id,
artist: option.None,
expanded_album: library.invalid_id,
library_view: lib_m,
),
effect.map(lib_e, LibraryViewMsg),
@ -93,55 +92,56 @@ fn init() {
fn update(model: Model, msg) {
case msg {
ArtistUpdated(option.Some(artist)) -> #(
ArtistUpdated(id) -> {
let new_getter = data_getter(_, id)
let artist =
load_artist(model.library_view.library, id)
|> option.from_result()
#(
Model(
artist: option.Some(artist),
expanded_album: library.invalid_id,
artist_id: id,
artist: artist,
library_view: library_view.Model(
..model.library_view,
data: data_getter(model.library_view.library, artist)
|> list.sort(fn(a, b) { model.library_view.sorter(a.1, b.1) }),
data_getter: new_getter,
),
),
effect.none(),
)
ArtistUpdated(option.None) -> #(
Model(
artist: option.None,
expanded_album: library.invalid_id,
library_view: library_view.Model(..model.library_view, data: []),
),
effect.none(),
)
LibraryViewMsg(AlbumExpandToggled(album)) -> #(
Model(
..model,
expanded_album: misc.toggle(
model.expanded_album,
album.0,
library.invalid_id,
),
),
effect.none(),
effect.map(
effect.from(fn(dispatch) { dispatch(library_view.FilterUpdated) }),
LibraryViewMsg,
),
)
}
LibraryViewMsg(lib_msg) -> {
let #(lib_m, lib_e) = library_view.update(model.library_view, lib_msg)
#(Model(..model, library_view: lib_m), effect.map(lib_e, LibraryViewMsg))
// Update artist when library is updated
let artist = case lib_msg {
library_view.LibraryUpdated(new_lib) ->
load_artist(new_lib, model.artist_id)
|> option.from_result()
_ -> model.artist
}
#(
Model(..model, artist: artist, library_view: lib_m),
effect.map(lib_e, LibraryViewMsg),
)
}
}
}
fn data_getter(library: Library, artist: LibraryItem(Artist)) {
fn data_getter(library: Library, artist_id: Int) {
library.albums
|> map.fold(
[],
fn(acc, key, val) {
case val.artist_id == artist.0 {
|> dict.fold([], fn(acc, key, val) {
case val.artist_id == artist_id {
True -> [#(key, val), ..acc]
False -> acc
}
},
)
})
}
fn shuffler(library, items: List(LibraryItem(Album))) {
@ -157,13 +157,17 @@ fn search_filter(item: Album, search_text: String) {
fn generate_view(search_filter: library_view.SearchFilter(Album)) {
fn(model: Model) {
case model.artist {
option.None ->
div(
[attribute.class("library-view-not-specified")],
[text("Artist not specified.")],
)
option.Some(artist) -> view(model, artist, search_filter)
case model.library_view.library_loading, model.artist {
True, _ ->
div([attribute.class("library-view-loading")], [
text("Loading library…"),
])
False, option.None ->
div([attribute.class("library-view-not-specified")], [
text("Artist not found with ID: " <> int.to_string(model.artist_id)),
])
False, option.Some(artist) ->
view(model, #(model.artist_id, artist), search_filter)
}
}
}
@ -173,9 +177,7 @@ fn view(
artist: LibraryItem(Artist),
search_filter: library_view.SearchFilter(Album),
) {
div(
[],
[
div([], [
header([attribute.class("library-header")], [text({ artist.1 }.name)]),
library_view.library_view(
model.library_view,
@ -183,8 +185,7 @@ fn view(
search_filter,
)
|> element.map(LibraryViewMsg),
],
)
])
}
fn album_view(model: Model, item: LibraryItem(Album)) {
@ -192,11 +193,15 @@ fn album_view(model: Model, item: LibraryItem(Album)) {
model.library_view.library,
model.library_view.settings,
item,
item.0 == model.expanded_album,
False,
)
}
fn artist_decode(data: dynamic.Dynamic) {
let artist: option.Option(LibraryItem(Artist)) = dynamic.unsafe_coerce(data)
Ok(ArtistUpdated(artist))
fn id_decode(data: dynamic.Dynamic) {
let artist_id: Int = dynamic.unsafe_coerce(data)
Ok(ArtistUpdated(artist_id))
}
fn load_artist(library: Library, id: Int) {
library.get_artist(library, id)
}

View file

@ -11,8 +11,8 @@ import elekf/web/components/library_view.{StartPlay}
pub fn view(
library: Library,
items: List(LibraryItem(Track)),
item: LibraryItem(Track),
index: Int,
item: LibraryItem(Track),
id_prefix: String,
) {
let #(track_id, track) = item

View file

@ -54,5 +54,5 @@ fn item_view(
index: Int,
item: LibraryItem(Track),
) {
track_item.view(model.library, items, item, index, "track-list")
track_item.view(model.library, items, index, item, "track-list")
}

View file

@ -2,7 +2,7 @@ import lustre/attribute
import lustre/element
import lustre/element/html.{a}
pub fn view(
pub fn button(
href: String,
class: String,
extra_attributes: List(attribute.Attribute(a)),
@ -17,3 +17,12 @@ pub fn view(
content,
)
}
pub fn link(
href: String,
class: String,
extra_attributes: List(attribute.Attribute(a)),
content: List(element.Element(a)),
) {
a([attribute.href(href), attribute.class(class), ..extra_attributes], content)
}

View file

@ -16,6 +16,7 @@ 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/components/library_views/single_artist_view
import elekf/web/components/library_views/single_album_view
import elekf/api/auth/storage as auth_storage
import elekf/api/auth/models as auth_models
import elekf/utils/option as option_utils
@ -41,6 +42,7 @@ pub fn main() {
let assert Ok(_) = artists_view.register()
let assert Ok(_) = albums_view.register()
let assert Ok(_) = single_artist_view.register()
let assert Ok(_) = single_album_view.register()
let app = lustre.application(init, update, view)
let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil)
@ -128,29 +130,20 @@ fn update(model: Model, msg) {
}
fn view(model: Model) {
html.main(
[],
[
html.main([], [
case model.current_view {
view.LoginView ->
div(
[attribute.id("login-view")],
[
div([attribute.id("login-view")], [
login_view.view(model.login_view)
|> element.map(LoginView),
],
)
])
view.AuthedView ->
div(
[attribute.id("authed-view")],
[
div([attribute.id("authed-view")], [
authed_view.view(option_utils.assert_some(model.authed_view))
|> element.map(AuthedView),
],
)
])
},
],
)
])
}
fn logged_in(model: Model, token: String, session: login_token.Session) {

View file

@ -30,7 +30,7 @@ pub fn init(dispatch: fn(Msg) -> Nil) {
}
pub fn run(dispatch: fn(Msg) -> Nil) {
use hash <- result.try(window.get_hash())
let hash = get_current_path()
use route <- result.try(parse(hash))
Ok(dispatch(RouteChanged(route)))
}
@ -39,7 +39,7 @@ pub fn to_hash(route: Route) {
"#" <> to_string(route)
}
fn parse(route: String) {
pub fn parse(route: String) {
let parts =
route
|> string.drop_left(1)
@ -62,6 +62,10 @@ fn parse(route: String) {
}
}
pub fn get_current_path() {
result.unwrap(window.get_hash(), "/")
}
fn to_string(route: Route) {
let parts = case route {
TrackList -> []

View file

@ -144,7 +144,8 @@ a {
tracks-view,
albums-view,
artists-view,
single-artist-view {
single-artist-view,
single-album-view {
display: block;
margin-top: calc(var(--library-top-nav-height) + var(--side-margin));
@ -166,11 +167,6 @@ single-artist-view {
overflow-y: auto;
}
/* Ensure that the view stack only shows the topmost view and the nav bar. */
#authed-view-library > *:not(:first-child, :last-child) {
display: none;
}
#authed-view-player {
position: absolute;
bottom: 0;
@ -362,6 +358,15 @@ single-artist-view {
font-size: var(--search-size);
}
.library-header {
padding: var(--side-margin);
overflow-x: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
font-size: 2rem;
}
.library-list {
height: 100%;
overflow-y: auto;