Keep the last scroll position of views
This commit is contained in:
parent
6889053db3
commit
b107d3e6b8
14 changed files with 265 additions and 28 deletions
0
src/authed_view_ffi.mjs
Normal file
0
src/authed_view_ffi.mjs
Normal file
|
@ -1,3 +1,13 @@
|
||||||
export function getDetail(e) {
|
import { Ok, Error } from "./gleam.mjs";
|
||||||
return e.detail;
|
|
||||||
|
export function newEvent(name, data) {
|
||||||
|
return new CustomEvent(name, { detail: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetail(e) {
|
||||||
|
if ("detail" in e) {
|
||||||
|
return new Ok(e.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(undefined);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
import gleam/dynamic
|
||||||
|
|
||||||
pub type CustomEvent
|
pub type CustomEvent
|
||||||
|
|
||||||
|
@external(javascript, "../../custom_event_ffi.mjs", "newEvent")
|
||||||
|
pub fn new(name: String, data: any) -> CustomEvent
|
||||||
|
|
||||||
@external(javascript, "../../custom_event_ffi.mjs", "getDetail")
|
@external(javascript, "../../custom_event_ffi.mjs", "getDetail")
|
||||||
pub fn get_detail(e: CustomEvent) -> a
|
pub fn get_detail(e: CustomEvent) -> Result(dynamic.Dynamic, Nil)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import lustre/element.{text}
|
||||||
import lustre/element/html.{div, nav, p}
|
import lustre/element/html.{div, nav, p}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/effect
|
import lustre/effect
|
||||||
|
import lustre/event
|
||||||
import birl
|
import birl
|
||||||
import ibroadcast/library/library as library_api
|
import ibroadcast/library/library as library_api
|
||||||
import ibroadcast/authed_request.{type RequestConfig, RequestConfig}
|
import ibroadcast/authed_request.{type RequestConfig, RequestConfig}
|
||||||
|
@ -84,6 +85,7 @@ pub type Msg {
|
||||||
PlayerMsg(player.Msg)
|
PlayerMsg(player.Msg)
|
||||||
StartPlay(PlayQueue, Int)
|
StartPlay(PlayQueue, Int)
|
||||||
Router(router.Msg)
|
Router(router.Msg)
|
||||||
|
LibraryViewScrollToTopRequested
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(auth_data: common.AuthData) {
|
pub fn init(auth_data: common.AuthData) {
|
||||||
|
@ -240,6 +242,11 @@ pub fn update(model: Model, msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibraryViewScrollToTopRequested -> {
|
||||||
|
library_view.request_scroll(0.0)
|
||||||
|
#(model, effect.none())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,19 +268,19 @@ pub fn view(model: Model) {
|
||||||
link.button(
|
link.button(
|
||||||
router.to_hash(router.queryless(router.TrackList)),
|
router.to_hash(router.queryless(router.TrackList)),
|
||||||
"",
|
"",
|
||||||
[],
|
[maybe_scroll(model.view, library_view.Tracks)],
|
||||||
[icon("music-note-beamed", Alt("Tracks"))],
|
[icon("music-note-beamed", Alt("Tracks"))],
|
||||||
),
|
),
|
||||||
link.button(
|
link.button(
|
||||||
router.to_hash(router.queryless(router.ArtistList)),
|
router.to_hash(router.queryless(router.ArtistList)),
|
||||||
"",
|
"",
|
||||||
[],
|
[maybe_scroll(model.view, library_view.Artists)],
|
||||||
[icon("file-person", Alt("Artists"))],
|
[icon("file-person", Alt("Artists"))],
|
||||||
),
|
),
|
||||||
link.button(
|
link.button(
|
||||||
router.to_hash(router.queryless(router.AlbumList)),
|
router.to_hash(router.queryless(router.AlbumList)),
|
||||||
"",
|
"",
|
||||||
[],
|
[maybe_scroll(model.view, library_view.Albums)],
|
||||||
[icon("disc", Alt("Albums"))],
|
[icon("disc", Alt("Albums"))],
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
@ -446,3 +453,12 @@ fn route_to_view(route: router.Route) {
|
||||||
router.Settings -> Error(Nil)
|
router.Settings -> Error(Nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_scroll(current_view: library_view.View, target_view: library_view.View) {
|
||||||
|
event.on("click", fn(_) {
|
||||||
|
case current_view == target_view {
|
||||||
|
True -> Ok(LibraryViewScrollToTopRequested)
|
||||||
|
False -> Error(Nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -7,20 +7,24 @@ import gleam/list
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import gleam/string
|
import gleam/string
|
||||||
|
import gleam/result
|
||||||
import lustre
|
import lustre
|
||||||
import lustre/effect
|
import lustre/effect
|
||||||
import lustre/element
|
import lustre/element
|
||||||
import lustre/element/html.{div}
|
import lustre/element/html.{div}
|
||||||
import lustre/attribute
|
import lustre/attribute
|
||||||
import lustre/event
|
import lustre/event
|
||||||
|
import elekf/utils/lustre as lustre_utils
|
||||||
import elekf/utils/order.{type Sorter}
|
import elekf/utils/order.{type Sorter}
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/web/events/start_play
|
import elekf/web/events/start_play
|
||||||
|
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/common
|
import elekf/web/common
|
||||||
|
import elekf/web/storage/history/storage as history_store
|
||||||
|
|
||||||
/// Function to get the data of the view from the library.
|
/// Function to get the data of the view from the library.
|
||||||
pub type DataGetter(a) =
|
pub type DataGetter(a) =
|
||||||
|
@ -69,10 +73,13 @@ pub type Msg {
|
||||||
StartPlay(List(LibraryItem(Track)), Int)
|
StartPlay(List(LibraryItem(Track)), Int)
|
||||||
Search(search.Msg)
|
Search(search.Msg)
|
||||||
FilterUpdated
|
FilterUpdated
|
||||||
|
ListScrolled(Float)
|
||||||
|
ScrollRequested(Float)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Model(a) {
|
pub type Model(a) {
|
||||||
Model(
|
Model(
|
||||||
|
id: String,
|
||||||
library: Library,
|
library: Library,
|
||||||
library_loading: Bool,
|
library_loading: Bool,
|
||||||
data: List(LibraryItem(a)),
|
data: List(LibraryItem(a)),
|
||||||
|
@ -81,6 +88,8 @@ pub type Model(a) {
|
||||||
sorter: Sorter(a),
|
sorter: Sorter(a),
|
||||||
search: search.Model,
|
search: search.Model,
|
||||||
settings: option.Option(common.Settings),
|
settings: option.Option(common.Settings),
|
||||||
|
history: history_store.StorageFormat,
|
||||||
|
history_api: history_store.HistoryStorage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +112,7 @@ pub fn register(
|
||||||
) {
|
) {
|
||||||
lustre.component(
|
lustre.component(
|
||||||
name,
|
name,
|
||||||
fn() { init(library.empty(), True, data_getter, shuffler, sorter) },
|
fn() { init(name, library.empty(), True, data_getter, shuffler, sorter) },
|
||||||
update,
|
update,
|
||||||
generate_view(item_view, search_filter),
|
generate_view(item_view, search_filter),
|
||||||
generic_attributes(),
|
generic_attributes(),
|
||||||
|
@ -134,9 +143,32 @@ pub fn render(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(library, library_loading, data_getter, shuffler, sorter) {
|
@external(javascript, "../../../library_view_ffi.mjs", "requestScroll")
|
||||||
|
pub fn request_scroll(pos: Float) -> Nil
|
||||||
|
|
||||||
|
pub fn init(id, library, library_loading, data_getter, shuffler, sorter) {
|
||||||
|
let scrollend_effect =
|
||||||
|
effect.from(fn(dispatch) {
|
||||||
|
lustre_utils.after_next_render(fn() {
|
||||||
|
add_scrollend_listener(fn(pos) { dispatch(ListScrolled(pos)) })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let history_api = history_store.get_api()
|
||||||
|
let history = get_view_history(history_api)
|
||||||
|
|
||||||
|
let scroll_to = dict.get(history.scrolls, id)
|
||||||
|
let scroll_to_effect = case scroll_to {
|
||||||
|
Ok(pos) if pos >. 0.0 ->
|
||||||
|
effect.from(fn(dispatch) {
|
||||||
|
lustre_utils.after_next_render(fn() { dispatch(ScrollRequested(pos)) })
|
||||||
|
})
|
||||||
|
_ -> effect.none()
|
||||||
|
}
|
||||||
|
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
|
id,
|
||||||
library,
|
library,
|
||||||
library_loading,
|
library_loading,
|
||||||
data_getter(library),
|
data_getter(library),
|
||||||
|
@ -145,8 +177,10 @@ pub fn init(library, library_loading, data_getter, shuffler, sorter) {
|
||||||
sorter,
|
sorter,
|
||||||
search.init(),
|
search.init(),
|
||||||
option.None,
|
option.None,
|
||||||
|
history,
|
||||||
|
history_api,
|
||||||
),
|
),
|
||||||
effect.none(),
|
effect.batch([scrollend_effect, scroll_to_effect]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,6 +203,23 @@ pub fn update(model, msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterUpdated -> #(update_data(model, model.library), effect.none())
|
FilterUpdated -> #(update_data(model, model.library), effect.none())
|
||||||
|
|
||||||
|
ListScrolled(pos) -> {
|
||||||
|
let view_history = get_view_history(model.history_api)
|
||||||
|
let updated_history =
|
||||||
|
history_store.StorageFormat(scrolls: dict.insert(
|
||||||
|
view_history.scrolls,
|
||||||
|
model.id,
|
||||||
|
pos,
|
||||||
|
))
|
||||||
|
let _ = history_store.write(model.history_api, updated_history)
|
||||||
|
#(model, effect.none())
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollRequested(pos) -> {
|
||||||
|
scroll_to(pos)
|
||||||
|
#(model, effect.none())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +238,7 @@ pub fn library_view(
|
||||||
}
|
}
|
||||||
|
|
||||||
div(
|
div(
|
||||||
[attribute.class("library-list")],
|
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
|
||||||
list.append(
|
list.append(
|
||||||
[
|
[
|
||||||
search.view(model.search)
|
search.view(model.search)
|
||||||
|
@ -227,3 +278,13 @@ fn update_data(model: Model(a), library: Library) {
|
||||||
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_view_history(history_api) {
|
||||||
|
result.unwrap(history_store.read(history_api), history_store.new())
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "../../../library_view_ffi.mjs", "addScrollendListener")
|
||||||
|
fn add_scrollend_listener(callback: fn(Float) -> Nil) -> Nil
|
||||||
|
|
||||||
|
@external(javascript, "../../../library_view_ffi.mjs", "scrollTo")
|
||||||
|
fn scroll_to(pos: Float) -> Nil
|
||||||
|
|
|
@ -58,6 +58,7 @@ pub fn render(
|
||||||
fn init() {
|
fn init() {
|
||||||
let #(lib_m, lib_e) =
|
let #(lib_m, lib_e) =
|
||||||
library_view.init(
|
library_view.init(
|
||||||
|
component_name,
|
||||||
library.empty(),
|
library.empty(),
|
||||||
True,
|
True,
|
||||||
data_getter,
|
data_getter,
|
||||||
|
|
|
@ -73,6 +73,7 @@ pub fn render(
|
||||||
fn init() {
|
fn init() {
|
||||||
let #(lib_m, lib_e) =
|
let #(lib_m, lib_e) =
|
||||||
library_view.init(
|
library_view.init(
|
||||||
|
component_name,
|
||||||
library.empty(),
|
library.empty(),
|
||||||
True,
|
True,
|
||||||
fn(_) -> List(LibraryItem(Track)) { [] },
|
fn(_) -> List(LibraryItem(Track)) { [] },
|
||||||
|
|
|
@ -73,6 +73,7 @@ pub fn render(
|
||||||
fn init() {
|
fn init() {
|
||||||
let #(lib_m, lib_e) =
|
let #(lib_m, lib_e) =
|
||||||
library_view.init(
|
library_view.init(
|
||||||
|
component_name,
|
||||||
library.empty(),
|
library.empty(),
|
||||||
True,
|
True,
|
||||||
fn(_) -> List(LibraryItem(Album)) { [] },
|
fn(_) -> List(LibraryItem(Album)) { [] },
|
||||||
|
|
41
src/elekf/web/events/scroll_to.gleam
Normal file
41
src/elekf/web/events/scroll_to.gleam
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import gleam/result
|
||||||
|
import gleam/dynamic
|
||||||
|
import lustre/event
|
||||||
|
import elekf/utils/custom_event.{type CustomEvent}
|
||||||
|
|
||||||
|
pub const event_name = "scroll-to"
|
||||||
|
|
||||||
|
pub type EventData {
|
||||||
|
EventData(pos: Float)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit(pos: Float) {
|
||||||
|
event.emit(event_name, EventData(pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on(msg: fn(Float) -> b) {
|
||||||
|
event.on(event_name, fn(data) {
|
||||||
|
data
|
||||||
|
|> decoder
|
||||||
|
|> result.map(fn(e) { msg(e.pos) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn js_custom_event(pos: Float) {
|
||||||
|
custom_event.new(event_name, EventData(pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoder(data: dynamic.Dynamic) {
|
||||||
|
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
||||||
|
use detail <- result.try(custom_event.get_detail(e))
|
||||||
|
use event_data <- result.try(
|
||||||
|
data_decoder()(detail)
|
||||||
|
|> result.nil_error(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Ok(event_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_decoder() {
|
||||||
|
dynamic.decode1(EventData, dynamic.field("pos", dynamic.float))
|
||||||
|
}
|
|
@ -15,22 +15,18 @@ pub fn emit(tracks: PlayQueue, position: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on(msg: fn(PlayQueue, Int) -> b) {
|
pub fn on(msg: fn(PlayQueue, Int) -> b) {
|
||||||
event.on(
|
event.on(event_name, fn(data) {
|
||||||
event_name,
|
data
|
||||||
fn(data) {
|
|> decoder
|
||||||
data
|
|> result.map(fn(e) { msg(e.tracks, e.position) })
|
||||||
|> decoder
|
})
|
||||||
|> result.map(fn(e) { msg(e.tracks, e.position) })
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decoder(data: dynamic.Dynamic) {
|
fn decoder(data: dynamic.Dynamic) {
|
||||||
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
||||||
let detail = custom_event.get_detail(e)
|
let assert Ok(detail) = custom_event.get_detail(e)
|
||||||
let event_data: EventData =
|
let event_data: EventData =
|
||||||
detail
|
detail
|
||||||
|> dynamic.from()
|
|
||||||
|> dynamic.unsafe_coerce()
|
|> dynamic.unsafe_coerce()
|
||||||
|
|
||||||
Ok(event_data)
|
Ok(event_data)
|
||||||
|
|
4
src/elekf/web/storage/history/models.gleam
Normal file
4
src/elekf/web/storage/history/models.gleam
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import gleam/dict
|
||||||
|
|
||||||
|
pub type ScrollPositions =
|
||||||
|
dict.Dict(String, Float)
|
68
src/elekf/web/storage/history/storage.gleam
Normal file
68
src/elekf/web/storage/history/storage.gleam
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
//// The history storage stores information about visited views for restoring
|
||||||
|
//// when the user next opens them.
|
||||||
|
|
||||||
|
import gleam/dynamic
|
||||||
|
import gleam/json
|
||||||
|
import gleam/dict
|
||||||
|
import gleam/list
|
||||||
|
import plinth/javascript/storage
|
||||||
|
import varasto
|
||||||
|
import elekf/web/storage/history/models
|
||||||
|
|
||||||
|
const storage_key = "__elektrofoni_history_storage"
|
||||||
|
|
||||||
|
const scrolls_key = "scroll-positions"
|
||||||
|
|
||||||
|
/// Storage format of the view history data in local storage.
|
||||||
|
pub type StorageFormat {
|
||||||
|
StorageFormat(scrolls: models.ScrollPositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local storage API used for storing view history details.
|
||||||
|
pub type HistoryStorage =
|
||||||
|
varasto.TypedStorage(StorageFormat)
|
||||||
|
|
||||||
|
/// Gets the `varasto` instance to use for reading and writing auth storage.
|
||||||
|
pub fn get_api() {
|
||||||
|
let assert Ok(local) = storage.local()
|
||||||
|
varasto.new(local, reader(), writer())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the previously stored value, if available.
|
||||||
|
pub fn read(storage: HistoryStorage) {
|
||||||
|
varasto.get(storage, storage_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes new value to the storage.
|
||||||
|
pub fn write(storage: HistoryStorage, data: StorageFormat) {
|
||||||
|
varasto.set(storage, storage_key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an empty storage model.
|
||||||
|
pub fn new() {
|
||||||
|
StorageFormat(scrolls: dict.new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reader() {
|
||||||
|
dynamic.decode1(StorageFormat, dynamic.field(scrolls_key, scrolls_decoder()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writer() {
|
||||||
|
fn(val: StorageFormat) {
|
||||||
|
json.object([#(scrolls_key, scrolls_encoder(val.scrolls))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrolls_encoder(scrolls: models.ScrollPositions) {
|
||||||
|
scrolls
|
||||||
|
|> dict.to_list()
|
||||||
|
|> list.map(fn(item) {
|
||||||
|
let #(key, val) = item
|
||||||
|
#(key, json.float(val))
|
||||||
|
})
|
||||||
|
|> json.object()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrolls_decoder() {
|
||||||
|
dynamic.dict(dynamic.string, dynamic.float)
|
||||||
|
}
|
36
src/library_view_ffi.mjs
Normal file
36
src/library_view_ffi.mjs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { js_custom_event } from "./elekf/web/events/scroll_to.mjs";
|
||||||
|
|
||||||
|
export function requestScroll(pos) {
|
||||||
|
const el = getEl();
|
||||||
|
if (el) {
|
||||||
|
const e = js_custom_event(pos);
|
||||||
|
el.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addScrollendListener(callback) {
|
||||||
|
const el = getEl();
|
||||||
|
if (el) {
|
||||||
|
el.addEventListener(
|
||||||
|
"scrollend",
|
||||||
|
() => {
|
||||||
|
callback(el.scrollTop);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollTo(pos) {
|
||||||
|
const el = getEl();
|
||||||
|
if (el) {
|
||||||
|
el.scrollTo({
|
||||||
|
top: pos,
|
||||||
|
behavior: "instant",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEl() {
|
||||||
|
return document.getElementById("library-list");
|
||||||
|
}
|
13
style.css
13
style.css
|
@ -411,19 +411,19 @@ single-album-view {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-list {
|
#library-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: 0;
|
padding-bottom: 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 {
|
||||||
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;
|
||||||
|
@ -457,9 +457,6 @@ single-album-view {
|
||||||
background: var(--glass-background);
|
background: var(--glass-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-item img {
|
|
||||||
}
|
|
||||||
|
|
||||||
.library-item h3 {
|
.library-item h3 {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue