Dig the project out form naphthalene, bump deps, update to Lustre v4
This commit is contained in:
parent
eb30b0f971
commit
8570d4e39e
37 changed files with 760 additions and 571 deletions
|
@ -1,2 +1,2 @@
|
|||
gleam 1.0.0
|
||||
nodejs 20.10.0
|
||||
gleam 1.4.1
|
||||
nodejs 22.4.1
|
||||
|
|
82
common.css
Normal file
82
common.css
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import "./normalize.css";
|
||||
@import "./vendor/bootstrap-icons/bootstrap-icons.min.css";
|
||||
@import "./settings.css";
|
||||
|
||||
.glass-bg,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
background: var(--glass-background);
|
||||
}
|
||||
|
||||
.glass-button:active {
|
||||
background: rgba(255, 255, 255, var(--glass-opacity-active));
|
||||
}
|
||||
|
||||
.glass-shadow,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.glass-blur,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.glass-border,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
border: var(--glass-border);
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
border-radius: var(--button-border-radius);
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-small,
|
||||
.glass-input {
|
||||
border-radius: var(--button-border-radius-small);
|
||||
}
|
||||
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
padding: var(--side-margin);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.button-group > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button-group .glass-button {
|
||||
border-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-group .glass-button:first-child {
|
||||
border-radius: var(--button-border-radius) 0 0 var(--button-border-radius);
|
||||
}
|
||||
|
||||
.button-group .glass-button:last-child {
|
||||
border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
|
||||
border-right: var(--glass-border);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
}
|
20
gleam.toml
20
gleam.toml
|
@ -1,7 +1,7 @@
|
|||
name = "elektrofoni"
|
||||
version = "1.0.0"
|
||||
target = "javascript"
|
||||
gleam = ">= 0.33.0"
|
||||
gleam = ">= 1.4.0"
|
||||
|
||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||
# your project to the Hex package manager.
|
||||
|
@ -12,15 +12,15 @@ gleam = ">= 0.33.0"
|
|||
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = "~> 0.34"
|
||||
gleam_json = "~> 0.7"
|
||||
gleam_stdlib = "0.40.0"
|
||||
gleam_json = "~> 1.0"
|
||||
gleam_http = "~> 3.5"
|
||||
gleam_javascript = "~> 0.7"
|
||||
gleam_fetch = "~> 0.3"
|
||||
plinth = "~> 0.1"
|
||||
varasto = "~> 2.0"
|
||||
lustre = "~> 3.1"
|
||||
birl = "~> 1.3"
|
||||
gleam_javascript = "0.11.0"
|
||||
gleam_fetch = "~> 1.0"
|
||||
plinth = "0.4.12"
|
||||
varasto = ">= 3.0.1 and < 4.0.0"
|
||||
lustre = "~> 4.3"
|
||||
birl = "~> 1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = "~> 1.0"
|
||||
gleeunit = "~> 1.2"
|
||||
|
|
|
@ -26,8 +26,6 @@
|
|||
<link rel="icon" type="image/png" sizes="16x16" href="./priv/assets/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="./priv/assets/favicon/site.webmanifest">
|
||||
|
||||
<link rel="stylesheet" href="./normalize.css" type="text/css">
|
||||
<link rel="stylesheet" href="./vendor/bootstrap-icons/bootstrap-icons.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="./style.css" type="text/css">
|
||||
|
||||
<script type="module">
|
||||
|
|
|
@ -2,33 +2,31 @@
|
|||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
|
||||
{ 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_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", "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_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "B17CDA56C2CD5BEFABF299E14FD8797E7FE6ABE1B861CA1C9E07441AE6FDDE6B" },
|
||||
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
|
||||
{ name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" },
|
||||
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
|
||||
{ name = "gleam_fetch", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "4AE60B21A9A664137A79B1BEB93F751CB27F1DDED4086CA00C0260F5FFACBD80" },
|
||||
{ name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
|
||||
{ name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" },
|
||||
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
|
||||
{ name = "gleam_otp", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "517FFB679E44AD71D059F3EF6A17BA6EFC8CB94FA174D52E22FB6768CF684D78" },
|
||||
{ name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" },
|
||||
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
|
||||
{ name = "lustre", version = "4.3.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BED65BD6A23439FB155A5BE166ED3C6BC20DED844FA9BB21840860951BC8E153" },
|
||||
{ name = "plinth", version = "0.4.12", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "3727B517C05BD4D49A4AF34BA7E63E03CA77B4E8AD2A4DCE60266B28717C1F9A" },
|
||||
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
|
||||
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
|
||||
{ name = "varasto", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "CEAB60C17DD9461E14E9DB8E6FF2A76E45ECCF704EC0555CE0398512068B01C1" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
birl = { version = "~> 1.3" }
|
||||
gleam_fetch = { version = "~> 0.3" }
|
||||
birl = { version = "~> 1.7" }
|
||||
gleam_fetch = { version = "~> 1.0" }
|
||||
gleam_http = { version = "~> 3.5" }
|
||||
gleam_javascript = { version = "~> 0.7" }
|
||||
gleam_json = { version = "~> 0.7" }
|
||||
gleam_stdlib = { version = "~> 0.34" }
|
||||
gleeunit = { version = "~> 1.0" }
|
||||
lustre = { version = "~> 3.1" }
|
||||
plinth = { version = "~> 0.1" }
|
||||
varasto = { version = "~> 2.0" }
|
||||
gleam_javascript = { version = "0.11.0" }
|
||||
gleam_json = { version = "~> 1.0" }
|
||||
gleam_stdlib = { version = "0.40.0" }
|
||||
gleeunit = { version = "~> 1.2" }
|
||||
lustre = { version = "~> 4.3" }
|
||||
plinth = { version = "0.4.12" }
|
||||
varasto = { version = ">= 3.0.1 and < 4.0.0" }
|
||||
|
|
42
settings.css
Normal file
42
settings.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("./priv/assets/fonts/Open_Sans/OpenSans-VariableFont_wdth\,wght.ttf")
|
||||
format("truetype") tech("variations");
|
||||
src: url("./priv/assets/fonts/Open_Sans/OpenSans-VariableFont_wdth\,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 300 800;
|
||||
font-stretch: 75% 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-family: "Open Sans", ui-sans-serif;
|
||||
|
||||
--background-gradient-bottom: rgb(131, 58, 180, 1);
|
||||
--background-gradient-middle: rgba(253, 29, 29, 1);
|
||||
--background-gradient-top: rgba(252, 176, 69, 1);
|
||||
--text-color: #123456;
|
||||
|
||||
--glass-opacity: 0.5;
|
||||
--glass-opacity-active: 0.2;
|
||||
--glass-shadow-opacity: 0.1;
|
||||
--glass-background: rgba(255, 255, 255, var(--glass-opacity));
|
||||
--glass-blur: 10px;
|
||||
--glass-border-color: rgba(255, 255, 255, var(--glass-opacity));
|
||||
--glass-border: 1px solid var(--glass-border-color);
|
||||
--glass-shadow: 0 4px 30px rgba(0, 0, 0, var(--glass-shadow-opacity));
|
||||
|
||||
--border-radius: 10px;
|
||||
|
||||
--button-border-radius-small: calc(var(--border-radius) / 2);
|
||||
--button-border-radius: var(--border-radius);
|
||||
|
||||
--side-margin: 5px;
|
||||
--double-margin: calc(var(--side-margin) * 2);
|
||||
|
||||
--track-thumb-size: 20px;
|
||||
--track-scale-factor: 4;
|
||||
|
||||
--library-top-nav-height: 50px;
|
||||
|
||||
--search-size: 1.6rem;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Ok, Error } from "./gleam.mjs";
|
||||
|
||||
export function newEvent(name, data) {
|
||||
return new CustomEvent(name, { detail: data });
|
||||
return new CustomEvent(name, { detail: data, composed: true });
|
||||
}
|
||||
|
||||
export function getDetail(e) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import gleam/list
|
||||
import gleam/dict.{type Dict}
|
||||
import ibroadcast/library/library.{type Library as APILibrary} as api_library
|
||||
import elekf/library.{Library}
|
||||
import elekf/transfer/album
|
||||
import elekf/transfer/artist
|
||||
import elekf/transfer/track
|
||||
import gleam/dict.{type Dict}
|
||||
import gleam/list
|
||||
import ibroadcast/library/library.{type Library as APILibrary} as api_library
|
||||
|
||||
/// Converts API library response to library format.
|
||||
pub fn from(library: APILibrary) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import gleam/string
|
||||
import gleam/option
|
||||
import gleam/float
|
||||
import gleam/result
|
||||
import ibroadcast/library/library.{type Track as APITrack}
|
||||
import elekf/library/track.{Track}
|
||||
import gleam/float
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import ibroadcast/library/library.{type Track as APITrack}
|
||||
|
||||
/// Converts API track response to library format.
|
||||
pub fn from(track: APITrack) {
|
||||
|
@ -20,7 +20,7 @@ pub fn from(track: APITrack) {
|
|||
genre: track.genre,
|
||||
length: track.length,
|
||||
album_id: track.album_id,
|
||||
artwork_id: artwork_id,
|
||||
artwork_id:,
|
||||
artist_id: track.artist_id,
|
||||
enid: track.enid,
|
||||
uploaded_on: track.uploaded_on,
|
||||
|
|
|
@ -1,48 +1,49 @@
|
|||
//// The authed view manages the user's request config and displays the base
|
||||
//// view for the app.
|
||||
|
||||
import gleam/io
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/float
|
||||
import gleam/int
|
||||
import gleam/result
|
||||
import gleam/set
|
||||
import gleam/javascript/promise
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{div, nav, p}
|
||||
import lustre/attribute
|
||||
import lustre/effect
|
||||
import lustre/event
|
||||
import birl
|
||||
import ibroadcast/library/library as library_api
|
||||
import ibroadcast/authed_request.{type RequestConfig, RequestConfig}
|
||||
import elekf/web/common.{type PlayQueue}
|
||||
import elekf/api/base_request_config.{base_request_config}
|
||||
import elekf/api/history
|
||||
import elekf/utils/http
|
||||
import elekf/utils/lustre
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/artist.{type Artist}
|
||||
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/player/model as player_model
|
||||
import elekf/web/components/player/actions as player_actions
|
||||
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/utils
|
||||
import elekf/utils/browser
|
||||
import elekf/utils/http
|
||||
import elekf/utils/lustre
|
||||
import elekf/web/common
|
||||
import elekf/web/components/button_group
|
||||
import elekf/web/components/icon.{Alt, icon}
|
||||
import elekf/web/components/library_view
|
||||
import elekf/web/components/library_views/albums_view
|
||||
import elekf/web/components/library_views/artists_view
|
||||
import elekf/web/components/library_views/single_album_view
|
||||
import elekf/web/components/library_views/single_artist_view
|
||||
import elekf/web/components/library_views/tracks_view
|
||||
import elekf/web/components/link
|
||||
import elekf/web/components/player
|
||||
import elekf/web/components/player/actions as player_actions
|
||||
import elekf/web/components/player/model as player_model
|
||||
import elekf/web/events/start_play
|
||||
import elekf/web/play_queue
|
||||
import elekf/web/router
|
||||
import elekf/web/utils
|
||||
import elektrofoni
|
||||
import forbidden_stdlib/list as forbidden_list
|
||||
import gleam/float
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/javascript/promise
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/set
|
||||
import ibroadcast/authed_request.{type RequestConfig, RequestConfig}
|
||||
import ibroadcast/library/library as library_api
|
||||
import lustre/attribute
|
||||
import lustre/effect
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{div, nav, p}
|
||||
import lustre/event
|
||||
|
||||
/// Status of the current track that is being played.
|
||||
pub type CurrentTrackStatus {
|
||||
|
@ -56,7 +57,7 @@ pub type PlayInfo {
|
|||
PlayInfo(
|
||||
track_id: Int,
|
||||
track: Track,
|
||||
play_queue: PlayQueue,
|
||||
play_queue: play_queue.PlayQueue,
|
||||
play_index: Int,
|
||||
player: player_model.Model,
|
||||
current_track_status: CurrentTrackStatus,
|
||||
|
@ -83,7 +84,7 @@ pub type Msg {
|
|||
UpdateAuthData(common.AuthData)
|
||||
LibraryResult(Result(library_api.ResponseData, http.ResponseError))
|
||||
PlayerMsg(player.Msg)
|
||||
StartPlay(PlayQueue, Int)
|
||||
StartPlay(play_queue.PlayQueue, Int)
|
||||
Router(router.Msg)
|
||||
LibraryViewScrollToTopRequested
|
||||
}
|
||||
|
@ -183,7 +184,7 @@ pub fn update(model: Model, msg) {
|
|||
info.play_index + 1
|
||||
_ -> info.play_index - 1
|
||||
}
|
||||
case list.at(info.play_queue, next_index) {
|
||||
case forbidden_list.at(info.play_queue, next_index) {
|
||||
Ok(_) -> {
|
||||
let #(status, effect) =
|
||||
handle_start_play(model, info.play_queue, next_index)
|
||||
|
@ -337,8 +338,8 @@ pub fn view(model: Model) {
|
|||
])
|
||||
}
|
||||
|
||||
fn handle_start_play(model: Model, queue: PlayQueue, position: Int) {
|
||||
let assert Ok(#(track_id, track)) = list.at(queue, position)
|
||||
fn handle_start_play(model: Model, queue: play_queue.PlayQueue, position: Int) {
|
||||
let assert Ok(#(track_id, track)) = forbidden_list.at(queue, position)
|
||||
|
||||
let #(player_model, maybe_init_effect) = case model.play_status {
|
||||
HasTracks(PlayInfo(player: p, ..)) -> #(p, effect.none())
|
||||
|
@ -476,7 +477,7 @@ 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)
|
||||
False -> Error([])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
16
src/elekf/web/codec/play_queue.gleam
Normal file
16
src/elekf/web/codec/play_queue.gleam
Normal file
|
@ -0,0 +1,16 @@
|
|||
import elekf/web/codec/track
|
||||
import elekf/web/play_queue
|
||||
import gleam/dynamic
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
|
||||
pub fn decode(data: dynamic.Dynamic) {
|
||||
dynamic.list(dynamic.tuple2(dynamic.int, track.decode))(data)
|
||||
}
|
||||
|
||||
pub fn encode(queue: play_queue.PlayQueue) {
|
||||
list.map(queue, fn(value) {
|
||||
json.preprocessed_array([json.int(value.0), track.encode(value.1)])
|
||||
})
|
||||
|> json.preprocessed_array()
|
||||
}
|
89
src/elekf/web/codec/track.gleam
Normal file
89
src/elekf/web/codec/track.gleam
Normal file
|
@ -0,0 +1,89 @@
|
|||
import elekf/library/track.{type Track, Track}
|
||||
import gleam/dynamic
|
||||
import gleam/json
|
||||
import gleam/result
|
||||
|
||||
pub fn decode(data: dynamic.Dynamic) {
|
||||
use number <- result.try(dynamic.field("number", dynamic.int)(data))
|
||||
use year <- result.try(dynamic.field("year", dynamic.int)(data))
|
||||
use title <- result.try(dynamic.field("title", dynamic.string)(data))
|
||||
use title_lower <- result.try(dynamic.field("title_lower", dynamic.string)(
|
||||
data,
|
||||
))
|
||||
use genre <- result.try(dynamic.field("genre", dynamic.string)(data))
|
||||
use length <- result.try(dynamic.field("length", dynamic.int)(data))
|
||||
use album_id <- result.try(dynamic.field("album_id", dynamic.int)(data))
|
||||
use artwork_id <- result.try(dynamic.field(
|
||||
"artwork_id",
|
||||
dynamic.optional(dynamic.int),
|
||||
)(data))
|
||||
use artist_id <- result.try(dynamic.field("artist_id", dynamic.int)(data))
|
||||
use enid <- result.try(dynamic.field("enid", dynamic.int)(data))
|
||||
use uploaded_on <- result.try(dynamic.field("uploaded_on", dynamic.string)(
|
||||
data,
|
||||
))
|
||||
use trashed <- result.try(dynamic.field("trashed", dynamic.bool)(data))
|
||||
use size <- result.try(dynamic.field("size", dynamic.int)(data))
|
||||
use path <- result.try(dynamic.field("path", dynamic.string)(data))
|
||||
use uid <- result.try(dynamic.field("uid", dynamic.string)(data))
|
||||
use rating <- result.try(dynamic.field("rating", dynamic.int)(data))
|
||||
use plays <- result.try(dynamic.field("plays", dynamic.int)(data))
|
||||
use file <- result.try(dynamic.field("file", dynamic.string)(data))
|
||||
use type_ <- result.try(dynamic.field("type", dynamic.string)(data))
|
||||
use replay_gain <- result.try(dynamic.field("replay_gain", dynamic.float)(
|
||||
data,
|
||||
))
|
||||
use uploaded_time <- result.try(dynamic.field("uploaded_time", dynamic.string)(
|
||||
data,
|
||||
))
|
||||
|
||||
Ok(Track(
|
||||
number:,
|
||||
year:,
|
||||
title:,
|
||||
title_lower:,
|
||||
genre:,
|
||||
length:,
|
||||
album_id:,
|
||||
artwork_id:,
|
||||
artist_id:,
|
||||
enid:,
|
||||
uploaded_on:,
|
||||
trashed:,
|
||||
size:,
|
||||
path:,
|
||||
uid:,
|
||||
rating:,
|
||||
plays:,
|
||||
file:,
|
||||
type_:,
|
||||
replay_gain:,
|
||||
uploaded_time:,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn encode(track: Track) {
|
||||
json.object([
|
||||
#("number", json.int(track.number)),
|
||||
#("year", json.int(track.year)),
|
||||
#("title", json.string(track.title)),
|
||||
#("title_lower", json.string(track.title_lower)),
|
||||
#("genre", json.string(track.genre)),
|
||||
#("length", json.int(track.length)),
|
||||
#("album_id", json.int(track.album_id)),
|
||||
#("artwork_id", json.nullable(track.artwork_id, json.int)),
|
||||
#("artist_id", json.int(track.artist_id)),
|
||||
#("enid", json.int(track.enid)),
|
||||
#("uploaded_on", json.string(track.uploaded_on)),
|
||||
#("trashed", json.bool(track.trashed)),
|
||||
#("size", json.int(track.size)),
|
||||
#("path", json.string(track.path)),
|
||||
#("uid", json.string(track.uid)),
|
||||
#("rating", json.int(track.rating)),
|
||||
#("plays", json.int(track.plays)),
|
||||
#("file", json.string(track.file)),
|
||||
#("type", json.string(track.type_)),
|
||||
#("replay_gain", json.float(track.replay_gain)),
|
||||
#("uploaded_time", json.string(track.uploaded_time)),
|
||||
])
|
||||
}
|
|
@ -1,10 +1,5 @@
|
|||
import elekf/library/track.{type Track}
|
||||
import elekf/api/auth/models as auth_models
|
||||
|
||||
/// A queue of tracks to play, with their IDs.
|
||||
pub type PlayQueue =
|
||||
List(#(Int, Track))
|
||||
|
||||
/// Authentication data for the user.
|
||||
pub type AuthData {
|
||||
AuthData(user: auth_models.User, device: auth_models.Device)
|
||||
|
|
|
@ -2,10 +2,6 @@ import lustre/attribute
|
|||
import lustre/element
|
||||
import lustre/element/html.{div}
|
||||
|
||||
/// An item in the library with its ID.
|
||||
pub type LibraryItem(a) =
|
||||
#(Int, a)
|
||||
|
||||
pub fn view(
|
||||
class: String,
|
||||
extra_attributes: List(attribute.Attribute(a)),
|
||||
|
|
107
src/elekf/web/components/library_view.css
Normal file
107
src/elekf/web/components/library_view.css
Normal file
|
@ -0,0 +1,107 @@
|
|||
@import "../../../../settings.css";
|
||||
@import "../../../../common.css";
|
||||
|
||||
#search-bar {
|
||||
position: absolute;
|
||||
top: calc(var(--library-top-nav-height) + var(--side-margin) * 3);
|
||||
right: var(--double-margin);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
gap: var(--double-margin);
|
||||
}
|
||||
|
||||
#search-bar button {
|
||||
border-radius: 100%;
|
||||
width: calc(var(--search-size) * 2);
|
||||
height: calc(var(--search-size) * 2);
|
||||
|
||||
font-size: var(--search-size);
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper input {
|
||||
/* 3 double margins: left side, flex gap, right side */
|
||||
width: calc(100vw - var(--double-margin) * 3 - var(--search-size) * 2);
|
||||
height: 100%;
|
||||
|
||||
font-size: var(--search-size);
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper input::-webkit-search-cancel-button {
|
||||
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;
|
||||
padding-bottom: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
grid-auto-flow: dense;
|
||||
|
||||
/* Ensure the grid doesn't stretch when there's only a few elements */
|
||||
grid-auto-rows: min-content;
|
||||
|
||||
gap: var(--double-margin);
|
||||
padding: var(--double-margin);
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.library-list-shuffle-all {
|
||||
grid-column: 1 / -1;
|
||||
padding: var(--double-margin) 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.library-item-thumbnail {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library-item:active {
|
||||
background: var(--glass-background);
|
||||
}
|
||||
|
||||
.library-item h3 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
|
@ -2,32 +2,34 @@
|
|||
//// component cannot be used directly, this should be extended by the actual
|
||||
//// components that focus on a specific type of view to the library.
|
||||
|
||||
import gleam/dynamic
|
||||
import gleam/list
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/track.{type Track}
|
||||
import elekf/utils/lustre as lustre_utils
|
||||
import elekf/utils/order.{type Sorter}
|
||||
import elekf/web/common
|
||||
import elekf/web/components/search
|
||||
import elekf/web/components/shuffle_all
|
||||
import elekf/web/components/track_length
|
||||
import elekf/web/events/scroll_to
|
||||
import elekf/web/events/start_play
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import elekf/web/play_queue
|
||||
import elekf/web/storage/history/storage as history_store
|
||||
import forbidden_stdlib/dynamic as forbidden_dynamic
|
||||
import gleam/dict
|
||||
import gleam/option
|
||||
import gleam/string
|
||||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/set
|
||||
import gleam/string
|
||||
import lustre
|
||||
import lustre/attribute
|
||||
import lustre/effect
|
||||
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
|
||||
import elekf/utils/order.{type Sorter}
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/track.{type Track}
|
||||
import elekf/web/events/start_play
|
||||
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
|
||||
|
||||
/// Function to get the data of the view from the library.
|
||||
pub type DataGetter(a, filter) =
|
||||
|
@ -88,7 +90,7 @@ pub type Msg(filter) {
|
|||
LibraryUpdated(Library)
|
||||
SettingsUpdated(option.Option(common.Settings))
|
||||
ShuffleAll
|
||||
StartPlay(List(LibraryItem(Track)), Int)
|
||||
StartPlay(play_queue.PlayQueue, Int)
|
||||
Search(search.Msg)
|
||||
FilterUpdated(filter)
|
||||
ListScrolled(Float)
|
||||
|
@ -108,9 +110,12 @@ pub type Model(a, filter) {
|
|||
settings: option.Option(common.Settings),
|
||||
history: history_store.StorageFormat,
|
||||
history_api: history_store.HistoryStorage,
|
||||
styles_path: String,
|
||||
)
|
||||
}
|
||||
|
||||
pub const default_styles_path = "./src/elekf/web/components/library_view.css"
|
||||
|
||||
/// Register the component as a custom element in the app.
|
||||
///
|
||||
/// This must be implemented by the client component, passing suitable values.
|
||||
|
@ -130,14 +135,16 @@ pub fn register(
|
|||
filter: Filter(filter),
|
||||
search_filter: SearchFilter(a),
|
||||
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
||||
styles_path: String,
|
||||
) {
|
||||
let app =
|
||||
lustre.component(
|
||||
name,
|
||||
fn() { init(name, filter, data_getter, shuffler, sorter) },
|
||||
fn(_) { init(name, filter, data_getter, shuffler, sorter, styles_path) },
|
||||
update,
|
||||
generate_view(header_view, item_view, search_filter),
|
||||
dict.merge(generic_attributes(), extra_attrs),
|
||||
)
|
||||
lustre.register(app, name)
|
||||
}
|
||||
|
||||
/// Get the generic properties common to all library views, that can be input
|
||||
|
@ -167,7 +174,7 @@ pub fn render(
|
|||
@external(javascript, "../../../library_view_ffi.mjs", "requestScroll")
|
||||
pub fn request_scroll(pos: Float) -> Nil
|
||||
|
||||
pub fn init(id, filter, data_getter, shuffler, sorter) {
|
||||
pub fn init(id, filter, data_getter, shuffler, sorter, styles_path) {
|
||||
let scrollend_effect =
|
||||
effect.from(fn(dispatch) {
|
||||
lustre_utils.after_next_render(fn() {
|
||||
|
@ -189,17 +196,18 @@ pub fn init(id, filter, data_getter, shuffler, sorter) {
|
|||
|
||||
#(
|
||||
Model(
|
||||
id,
|
||||
filter,
|
||||
NoLibrary,
|
||||
[],
|
||||
data_getter,
|
||||
shuffler,
|
||||
sorter,
|
||||
search.init(),
|
||||
option.None,
|
||||
history,
|
||||
history_api,
|
||||
id:,
|
||||
filter:,
|
||||
library_status: NoLibrary,
|
||||
data: [],
|
||||
data_getter:,
|
||||
shuffler:,
|
||||
sorter:,
|
||||
search: search.init(),
|
||||
settings: option.None,
|
||||
history:,
|
||||
history_api:,
|
||||
styles_path:,
|
||||
),
|
||||
effect.batch([scrollend_effect, scroll_to_effect]),
|
||||
)
|
||||
|
@ -252,6 +260,13 @@ pub fn library_view(
|
|||
item_view: ItemView(a, filter),
|
||||
search_filter: SearchFilter(a),
|
||||
) {
|
||||
let styles =
|
||||
html.link([
|
||||
attribute.rel("stylesheet"),
|
||||
attribute.type_("text/css"),
|
||||
attribute.href(model.styles_path),
|
||||
])
|
||||
|
||||
case model.library_status, model.filter {
|
||||
HaveLibrary(lib), option.Some(f) -> {
|
||||
let items = case model.search.search_text {
|
||||
|
@ -262,7 +277,8 @@ pub fn library_view(
|
|||
|> list.filter(fn(item) { search_filter(item.1, search_txt) })
|
||||
}
|
||||
}
|
||||
|
||||
element.fragment([
|
||||
styles,
|
||||
div(
|
||||
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
|
||||
list.append(
|
||||
|
@ -280,13 +296,20 @@ pub fn library_view(
|
|||
})
|
||||
|> list.flatten(),
|
||||
),
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
_, _ ->
|
||||
element.fragment([
|
||||
styles,
|
||||
div(
|
||||
[attribute.id("library-list"), attribute.class("library-list-loading")],
|
||||
[
|
||||
attribute.id("library-list"),
|
||||
attribute.class("library-list-loading"),
|
||||
],
|
||||
[text("")],
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,14 +374,14 @@ fn shuffle_all(model: Model(a, filter)) {
|
|||
case model.library_status {
|
||||
HaveLibrary(lib) -> {
|
||||
let tracks = model.shuffler(lib, model.data)
|
||||
start_play.emit(tracks, 0)
|
||||
start_play.emit(play_queue.from_list(tracks), 0)
|
||||
}
|
||||
NoLibrary -> effect.none()
|
||||
}
|
||||
}
|
||||
|
||||
fn library_decode(data: dynamic.Dynamic) {
|
||||
let library: option.Option(Library) = dynamic.unsafe_coerce(data)
|
||||
let library: option.Option(Library) = forbidden_dynamic.unsafe_coerce(data)
|
||||
case library {
|
||||
option.Some(lib) -> Ok(LibraryUpdated(lib))
|
||||
option.None -> Error([])
|
||||
|
@ -366,7 +389,8 @@ fn library_decode(data: dynamic.Dynamic) {
|
|||
}
|
||||
|
||||
fn settings_decode(data: dynamic.Dynamic) {
|
||||
let settings: option.Option(common.Settings) = dynamic.unsafe_coerce(data)
|
||||
let settings: option.Option(common.Settings) =
|
||||
forbidden_dynamic.unsafe_coerce(data)
|
||||
Ok(SettingsUpdated(settings))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
//// Library item view for a single album.
|
||||
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/int
|
||||
import lustre/element/html.{h3, p}
|
||||
import lustre/element.{text}
|
||||
import lustre/attribute
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/album.{type Album}
|
||||
import elekf/library/album_utils
|
||||
import elekf/web/components/library_item.{type LibraryItem}
|
||||
import elekf/web/components/thumbnail
|
||||
import elekf/web/components/link
|
||||
import elekf/web/common.{type Settings}
|
||||
import elekf/web/components/link
|
||||
import elekf/web/components/thumbnail
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import elekf/web/router
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{h3, p}
|
||||
|
||||
pub fn view(
|
||||
library: Library,
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
//// A library view to all of the albums in the library.
|
||||
|
||||
import gleam/string
|
||||
import gleam/list
|
||||
import gleam/dict
|
||||
import gleam/option
|
||||
import lustre/attribute
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/album.{type Album}
|
||||
import elekf/library/album_utils
|
||||
import elekf/web/components/library_view.{type Model}
|
||||
import elekf/web/components/library_item.{type LibraryItem}
|
||||
import elekf/web/components/library_views/album_item
|
||||
import elekf/web/common
|
||||
import elekf/web/components/library_view.{type Model}
|
||||
import elekf/web/components/library_views/album_item
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import gleam/dict
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
|
||||
const component_name = "albums-view"
|
||||
|
||||
|
@ -27,6 +27,7 @@ pub fn register() {
|
|||
option.Some(Nil),
|
||||
search_filter,
|
||||
dict.new(),
|
||||
library_view.default_styles_path,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
//// A library view to all of the artists in the library.
|
||||
|
||||
import gleam/string
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/dict
|
||||
import gleam/option
|
||||
import lustre/element/html.{h3, p}
|
||||
import lustre/element.{text}
|
||||
import lustre/attribute
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/artist.{type Artist}
|
||||
import elekf/library/artist_utils
|
||||
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/library_view.{type Model}
|
||||
import elekf/web/components/link
|
||||
import elekf/web/components/thumbnail
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import elekf/web/router
|
||||
import gleam/dict
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{h3, p}
|
||||
|
||||
const component_name = "artists-view"
|
||||
|
||||
|
@ -32,6 +32,7 @@ pub fn register() {
|
|||
option.Some(Nil),
|
||||
search_filter,
|
||||
dict.new(),
|
||||
library_view.default_styles_path,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
//// A library view to the current play queue.
|
||||
|
||||
import gleam/string
|
||||
import gleam/list
|
||||
import gleam/dict
|
||||
import gleam/option
|
||||
import lustre/attribute
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/track.{type Track}
|
||||
import elekf/utils/order
|
||||
import elekf/web/components/library_view.{type Model} as library_view
|
||||
import elekf/web/components/library_item.{type LibraryItem}
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/codec/play_queue as play_queue_codec
|
||||
import elekf/web/common
|
||||
import elekf/web/components/library_view.{type Model}
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import elekf/web/play_queue.{type PlayQueue}
|
||||
import gleam/dict
|
||||
import gleam/dynamic
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
|
||||
const component_name = "play-queue-view"
|
||||
|
||||
|
@ -20,13 +24,14 @@ pub fn register() {
|
|||
library_view.register(
|
||||
component_name,
|
||||
data_getter,
|
||||
library_view.empty_header,
|
||||
fn(m, l, f, i) { library_view.stats_header("Play queue", m, l, f, i) },
|
||||
item_view,
|
||||
shuffler,
|
||||
order.noop,
|
||||
option.None,
|
||||
search_filter,
|
||||
dict.new(),
|
||||
dict.from_list([#("tracks", queue_decoder)]),
|
||||
library_view.default_styles_path,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -39,8 +44,8 @@ pub fn render(
|
|||
library_view.render(component_name, library, settings, extra_attrs)
|
||||
}
|
||||
|
||||
fn data_getter(library: Library, _filter: Nil) {
|
||||
dict.to_list(library.tracks)
|
||||
fn data_getter(_library: Library, filter: PlayQueue) {
|
||||
play_queue.to_list(filter)
|
||||
}
|
||||
|
||||
fn shuffler(_library, items) {
|
||||
|
@ -52,7 +57,7 @@ fn search_filter(item: Track, search_text: String) {
|
|||
}
|
||||
|
||||
fn item_view(
|
||||
_model: Model(Track, Nil),
|
||||
_model: Model(Track, PlayQueue),
|
||||
library: Library,
|
||||
items: List(LibraryItem(Track)),
|
||||
index: Int,
|
||||
|
@ -60,3 +65,8 @@ fn item_view(
|
|||
) {
|
||||
track_item.view(library, items, index, item, "track-list")
|
||||
}
|
||||
|
||||
fn queue_decoder(queue: dynamic.Dynamic) {
|
||||
use queue <- result.try(play_queue_codec.decode(queue))
|
||||
Ok(library_view.FilterUpdated(queue))
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
//// 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/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
|
||||
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/library_view
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/components/track_length
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import gleam/dict
|
||||
import gleam/dynamic
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{div, h1, p}
|
||||
|
||||
const component_name = "single-album-view"
|
||||
|
||||
|
@ -32,6 +33,7 @@ pub fn register() {
|
|||
option.None,
|
||||
search_filter,
|
||||
dict.from_list([#("album-id", id_decode)]),
|
||||
library_view.default_styles_path,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -110,6 +112,6 @@ fn item_view(
|
|||
}
|
||||
|
||||
fn id_decode(data: dynamic.Dynamic) {
|
||||
let album_id: Int = dynamic.unsafe_coerce(data)
|
||||
use album_id <- result.try(dynamic.int(data))
|
||||
Ok(library_view.FilterUpdated(album_id))
|
||||
}
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
//// A library view to a single artist's albums and non-album tracks.
|
||||
|
||||
import gleam/string
|
||||
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
|
||||
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/library_view
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/components/track_length
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import gleam/dict
|
||||
import gleam/dynamic
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/result
|
||||
import gleam/set
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{div, h1, p}
|
||||
|
||||
const component_name = "single-artist-view"
|
||||
|
||||
|
@ -33,6 +34,7 @@ pub fn register() {
|
|||
option.None,
|
||||
search_filter,
|
||||
dict.from_list([#("artist-id", id_decode)]),
|
||||
library_view.default_styles_path,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,6 +122,6 @@ fn item_view(
|
|||
}
|
||||
|
||||
fn id_decode(data: dynamic.Dynamic) {
|
||||
let artist_id: Int = dynamic.unsafe_coerce(data)
|
||||
use artist_id <- result.try(dynamic.int(data))
|
||||
Ok(library_view.FilterUpdated(artist_id))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import gleam/int
|
||||
import lustre/attribute
|
||||
import lustre/event
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{h3, p}
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/track.{type Track}
|
||||
import elekf/web/components/library_item.{type LibraryItem}
|
||||
import elekf/web/components/library_item as library_item_component
|
||||
import elekf/web/components/library_view.{StartPlay}
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import elekf/web/play_queue
|
||||
import gleam/int
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{h3, p}
|
||||
import lustre/event
|
||||
|
||||
pub fn view(
|
||||
library: Library,
|
||||
|
@ -18,13 +20,14 @@ pub fn view(
|
|||
let #(track_id, track) = item
|
||||
let album = library.assert_album(library, track.album_id)
|
||||
let artist = library.assert_artist(library, track.artist_id)
|
||||
let queue = play_queue.from_list(items)
|
||||
[
|
||||
library_item.view(
|
||||
library_item_component.view(
|
||||
"track-item",
|
||||
[
|
||||
attribute.id(id_prefix <> "-" <> int.to_string(track_id)),
|
||||
attribute.type_("button"),
|
||||
event.on_click(StartPlay(items, index)),
|
||||
event.on_click(StartPlay(queue, index)),
|
||||
attribute.attribute("role", "button"),
|
||||
],
|
||||
[
|
||||
|
|
5
src/elekf/web/components/library_views/tracks_view.css
Normal file
5
src/elekf/web/components/library_views/tracks_view.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "../library_view.css";
|
||||
|
||||
#library-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
//// A library view to all of the tracks in the library.
|
||||
|
||||
import gleam/string
|
||||
import gleam/list
|
||||
import gleam/dict
|
||||
import gleam/option
|
||||
import lustre/attribute
|
||||
import elekf/library.{type Library}
|
||||
import elekf/library/track.{type Track}
|
||||
import elekf/library/track_utils
|
||||
import elekf/web/components/library_view.{type Model}
|
||||
import elekf/web/components/library_item.{type LibraryItem}
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/common
|
||||
import elekf/web/components/library_view.{type Model}
|
||||
import elekf/web/components/library_views/track_item
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
import gleam/dict
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/string
|
||||
import lustre/attribute
|
||||
|
||||
const component_name = "tracks-view"
|
||||
|
||||
|
@ -27,6 +27,7 @@ pub fn register() {
|
|||
option.Some(Nil),
|
||||
search_filter,
|
||||
dict.new(),
|
||||
"./src/elekf/web/components/library_views/tracks_view.css",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
//// A track for visualising the current song's progress and skipping it back
|
||||
//// and forth, and additional timestamps.
|
||||
|
||||
import gleam/int
|
||||
import gleam/dynamic
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{input, p}
|
||||
import lustre/attribute
|
||||
import lustre/event
|
||||
import elekf/web/components/track_length.{track_length}
|
||||
import elekf/web/components/player/model
|
||||
import elekf/web/components/player/actions.{
|
||||
Commit, EndUserSkip, Ephemeral, SelectPosition, StartUserSkip,
|
||||
}
|
||||
import elekf/web/components/player/model
|
||||
import elekf/web/components/track_length.{track_length}
|
||||
import gleam/int
|
||||
import lustre/attribute
|
||||
import lustre/element.{text}
|
||||
import lustre/element/html.{input, p}
|
||||
import lustre/event
|
||||
|
||||
pub fn view(model: model.Model) {
|
||||
let current_time_padding = case model.track.length > 3600 {
|
||||
|
@ -19,7 +18,7 @@ pub fn view(model: model.Model) {
|
|||
False -> track_length.Auto
|
||||
}
|
||||
|
||||
let track_pos = dynamic.from(int.to_string(model.position))
|
||||
let track_pos = int.to_string(model.position)
|
||||
|
||||
[
|
||||
p(
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
//// 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
|
||||
import elekf/web/components/icon.{Alt, icon}
|
||||
import elekf/web/components/button
|
||||
import elekf/utils/lustre
|
||||
import elekf/web/components/button
|
||||
import elekf/web/components/icon.{Alt, icon}
|
||||
import lustre/attribute
|
||||
import lustre/element/html.{div, input}
|
||||
import lustre/event
|
||||
|
||||
const search_bar_input_id = "search-bar-input"
|
||||
|
||||
|
@ -51,7 +50,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)),
|
||||
attribute.value(model.search_text),
|
||||
event.on_input(UpdateSearch),
|
||||
]),
|
||||
])
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import lustre/event
|
||||
import elekf/utils/custom_event.{type CustomEvent}
|
||||
import forbidden_stdlib/dynamic as forbidden_dynamic
|
||||
import gleam/dynamic
|
||||
import gleam/json
|
||||
import gleam/result
|
||||
import lustre/event
|
||||
|
||||
pub const event_name = "scroll-to"
|
||||
|
||||
|
@ -10,14 +12,14 @@ pub type EventData {
|
|||
}
|
||||
|
||||
pub fn emit(pos: Float) {
|
||||
event.emit(event_name, EventData(pos))
|
||||
event.emit(event_name, json.float(pos))
|
||||
}
|
||||
|
||||
pub fn on(msg: fn(Float) -> b) {
|
||||
event.on(event_name, fn(data) {
|
||||
data
|
||||
|> decoder
|
||||
|> result.map(fn(e) { msg(e.pos) })
|
||||
|> result.map(fn(pos) { msg(pos) })
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -26,16 +28,14 @@ pub fn js_custom_event(pos: Float) {
|
|||
}
|
||||
|
||||
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(),
|
||||
let e: CustomEvent = forbidden_dynamic.unsafe_coerce(data)
|
||||
use detail <- result.try(
|
||||
custom_event.get_detail(e)
|
||||
|> result.replace_error([
|
||||
dynamic.DecodeError("event detail", "no detail", []),
|
||||
]),
|
||||
)
|
||||
use event_data <- result.try(dynamic.float(detail))
|
||||
|
||||
Ok(event_data)
|
||||
}
|
||||
|
||||
fn data_decoder() {
|
||||
dynamic.decode1(EventData, dynamic.field("pos", dynamic.float))
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import lustre/event
|
||||
import elekf/web/common.{type PlayQueue}
|
||||
import elekf/utils/custom_event.{type CustomEvent}
|
||||
import elekf/web/codec/play_queue as play_queue_codec
|
||||
import elekf/web/play_queue.{type PlayQueue}
|
||||
import forbidden_stdlib/dynamic as forbidden_dynamic
|
||||
import gleam/dynamic
|
||||
import gleam/json
|
||||
import gleam/result
|
||||
import lustre/event
|
||||
|
||||
const event_name = "start-play"
|
||||
|
||||
|
@ -11,7 +14,13 @@ pub type EventData {
|
|||
}
|
||||
|
||||
pub fn emit(tracks: PlayQueue, position: Int) {
|
||||
event.emit(event_name, EventData(tracks, position))
|
||||
event.emit(
|
||||
event_name,
|
||||
json.object([
|
||||
#("queue", play_queue_codec.encode(tracks)),
|
||||
#("position", json.int(position)),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on(msg: fn(PlayQueue, Int) -> b) {
|
||||
|
@ -23,11 +32,18 @@ pub fn on(msg: fn(PlayQueue, Int) -> b) {
|
|||
}
|
||||
|
||||
fn decoder(data: dynamic.Dynamic) {
|
||||
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
||||
let assert Ok(detail) = custom_event.get_detail(e)
|
||||
let event_data: EventData =
|
||||
detail
|
||||
|> dynamic.unsafe_coerce()
|
||||
let e: CustomEvent = forbidden_dynamic.unsafe_coerce(data)
|
||||
use detail <- result.try(
|
||||
custom_event.get_detail(e)
|
||||
|> result.replace_error([
|
||||
dynamic.DecodeError("event detail", "no detail", []),
|
||||
]),
|
||||
)
|
||||
use event_data <- result.try(dynamic.decode2(
|
||||
EventData,
|
||||
dynamic.field("queue", play_queue_codec.decode),
|
||||
dynamic.field("position", dynamic.int),
|
||||
)(detail))
|
||||
|
||||
Ok(event_data)
|
||||
}
|
||||
|
|
3
src/elekf/web/library_item.gleam
Normal file
3
src/elekf/web/library_item.gleam
Normal file
|
@ -0,0 +1,3 @@
|
|||
/// An item in the library with its ID.
|
||||
pub type LibraryItem(a) =
|
||||
#(Int, a)
|
14
src/elekf/web/play_queue.gleam
Normal file
14
src/elekf/web/play_queue.gleam
Normal file
|
@ -0,0 +1,14 @@
|
|||
import elekf/library/track
|
||||
import elekf/web/library_item.{type LibraryItem}
|
||||
|
||||
/// A queue of tracks to play, with their IDs.
|
||||
pub type PlayQueue =
|
||||
List(LibraryItem(track.Track))
|
||||
|
||||
pub fn from_list(tracks: List(LibraryItem(track.Track))) -> PlayQueue {
|
||||
tracks
|
||||
}
|
||||
|
||||
pub fn to_list(queue: PlayQueue) -> List(LibraryItem(track.Track)) {
|
||||
queue
|
||||
}
|
2
src/forbidden_stdlib/dynamic.gleam
Normal file
2
src/forbidden_stdlib/dynamic.gleam
Normal file
|
@ -0,0 +1,2 @@
|
|||
@external(javascript, "../forbidden_stdlib_ffi.mjs", "unsafeCoerce")
|
||||
pub fn unsafe_coerce(val: a) -> b
|
11
src/forbidden_stdlib/list.gleam
Normal file
11
src/forbidden_stdlib/list.gleam
Normal file
|
@ -0,0 +1,11 @@
|
|||
pub fn at(list: List(a), index: Int) -> Result(a, Nil) {
|
||||
do_at(list, index, 0)
|
||||
}
|
||||
|
||||
fn do_at(list: List(a), index: Int, curr: Int) {
|
||||
case list {
|
||||
[] -> Error(Nil)
|
||||
[item, ..] if curr == index -> Ok(item)
|
||||
[_, ..rest] -> do_at(rest, index, curr + 1)
|
||||
}
|
||||
}
|
3
src/forbidden_stdlib_ffi.mjs
Normal file
3
src/forbidden_stdlib_ffi.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function unsafeCoerce(val) {
|
||||
return val;
|
||||
}
|
|
@ -4,19 +4,18 @@
|
|||
////
|
||||
//// See https://devguide.ibroadcast.com/?p=api#Play-History
|
||||
|
||||
import birl
|
||||
import gleam/dict.{type Dict}
|
||||
import gleam/json
|
||||
import gleam/dynamic
|
||||
import gleam/int
|
||||
import gleam/javascript/promise
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import gleam/option
|
||||
import gleam/javascript/promise
|
||||
import birl
|
||||
import ibroadcast/request.{DecodeFailed}
|
||||
import ibroadcast/auth/status
|
||||
import ibroadcast/authed_request.{type RequestConfig}
|
||||
import ibroadcast/http.{type Requestor}
|
||||
import ibroadcast/auth/status
|
||||
import ibroadcast/request.{DecodeFailed}
|
||||
import ibroadcast/request_params
|
||||
import ibroadcast/servers
|
||||
import ibroadcast/time
|
||||
|
@ -77,15 +76,18 @@ pub fn add_to_day(day: HistoryDay, track_id: String, event: Event) -> HistoryDay
|
|||
Skip(_) -> day.plays
|
||||
}
|
||||
|
||||
let detail =
|
||||
dict.update(day.detail, track_id, fn(old_detail) {
|
||||
case old_detail {
|
||||
option.Some(old_detail) -> list.append(old_detail, [event])
|
||||
option.None -> [event]
|
||||
}
|
||||
})
|
||||
let old_detail = dict.get(day.detail, track_id)
|
||||
|
||||
HistoryDay(..day, plays: plays, detail: detail)
|
||||
let detail = case old_detail {
|
||||
Ok(old_detail) -> list.append(old_detail, [event])
|
||||
Error(_) -> [event]
|
||||
}
|
||||
|
||||
HistoryDay(
|
||||
..day,
|
||||
plays: plays,
|
||||
detail: dict.insert(day.detail, track_id, detail),
|
||||
)
|
||||
}
|
||||
|
||||
fn payload_decoder() {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import gleam/javascript/promise
|
||||
import gleam/json
|
||||
import gleam/dict.{type Dict}
|
||||
import gleam/dynamic
|
||||
import gleam/result
|
||||
import gleam/javascript/promise
|
||||
import gleam/json
|
||||
import gleam/option
|
||||
import ibroadcast/servers
|
||||
import ibroadcast/request.{DecodeFailed}
|
||||
import gleam/result
|
||||
import ibroadcast/authed_request.{type RequestConfig}
|
||||
import ibroadcast/request_params.{type RequestParams}
|
||||
import ibroadcast/http.{type Requestor}
|
||||
import ibroadcast/library_format
|
||||
import ibroadcast/request.{DecodeFailed}
|
||||
import ibroadcast/request_params.{type RequestParams}
|
||||
import ibroadcast/servers
|
||||
|
||||
pub type Album {
|
||||
Album(
|
||||
|
@ -189,26 +189,26 @@ fn track_decoder() {
|
|||
use uploaded_time <- result.try(dynamic.element(19, dynamic.string)(data))
|
||||
|
||||
Ok(Track(
|
||||
number: number,
|
||||
year: year,
|
||||
title: title,
|
||||
genre: genre,
|
||||
length: length,
|
||||
album_id: album_id,
|
||||
artwork_id: artwork_id,
|
||||
artist_id: artist_id,
|
||||
enid: enid,
|
||||
uploaded_on: uploaded_on,
|
||||
trashed: trashed,
|
||||
size: size,
|
||||
path: path,
|
||||
uid: uid,
|
||||
rating: rating,
|
||||
plays: plays,
|
||||
file: file,
|
||||
type_: type_,
|
||||
replay_gain: replay_gain,
|
||||
uploaded_time: uploaded_time,
|
||||
number:,
|
||||
year:,
|
||||
title:,
|
||||
genre:,
|
||||
length:,
|
||||
album_id:,
|
||||
artwork_id:,
|
||||
artist_id:,
|
||||
enid:,
|
||||
uploaded_on:,
|
||||
trashed:,
|
||||
size:,
|
||||
path:,
|
||||
uid:,
|
||||
rating:,
|
||||
plays:,
|
||||
file:,
|
||||
type_:,
|
||||
replay_gain:,
|
||||
uploaded_time:,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
237
style.css
237
style.css
|
@ -1,119 +1,5 @@
|
|||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("./priv/assets/fonts/Open_Sans/OpenSans-VariableFont_wdth\,wght.ttf")
|
||||
format("truetype") tech("variations");
|
||||
src: url("./priv/assets/fonts/Open_Sans/OpenSans-VariableFont_wdth\,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 300 800;
|
||||
font-stretch: 75% 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-family: "Open Sans", ui-sans-serif;
|
||||
|
||||
--background-gradient-bottom: rgb(131, 58, 180, 1);
|
||||
--background-gradient-middle: rgba(253, 29, 29, 1);
|
||||
--background-gradient-top: rgba(252, 176, 69, 1);
|
||||
--text-color: #123456;
|
||||
|
||||
--glass-opacity: 0.5;
|
||||
--glass-opacity-active: 0.2;
|
||||
--glass-shadow-opacity: 0.1;
|
||||
--glass-background: rgba(255, 255, 255, var(--glass-opacity));
|
||||
--glass-blur: 10px;
|
||||
--glass-border-color: rgba(255, 255, 255, var(--glass-opacity));
|
||||
--glass-border: 1px solid var(--glass-border-color);
|
||||
--glass-shadow: 0 4px 30px rgba(0, 0, 0, var(--glass-shadow-opacity));
|
||||
|
||||
--border-radius: 10px;
|
||||
|
||||
--button-border-radius-small: calc(var(--border-radius) / 2);
|
||||
--button-border-radius: var(--border-radius);
|
||||
|
||||
--side-margin: 5px;
|
||||
--double-margin: calc(var(--side-margin) * 2);
|
||||
|
||||
--track-thumb-size: 20px;
|
||||
--track-scale-factor: 4;
|
||||
|
||||
--library-top-nav-height: 50px;
|
||||
|
||||
--search-size: 1.6rem;
|
||||
}
|
||||
|
||||
.glass-bg,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
background: var(--glass-background);
|
||||
}
|
||||
|
||||
.glass-button:active {
|
||||
background: rgba(255, 255, 255, var(--glass-opacity-active));
|
||||
}
|
||||
|
||||
.glass-shadow,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.glass-blur,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.glass-border,
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
border: var(--glass-border);
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
border-radius: var(--button-border-radius);
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-small,
|
||||
.glass-input {
|
||||
border-radius: var(--button-border-radius-small);
|
||||
}
|
||||
|
||||
.glass-button,
|
||||
.glass-input {
|
||||
padding: var(--side-margin);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.button-group > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button-group .glass-button {
|
||||
border-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-group .glass-button:first-child {
|
||||
border-radius: var(--button-border-radius) 0 0 var(--button-border-radius);
|
||||
}
|
||||
|
||||
.button-group .glass-button:last-child {
|
||||
border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
|
||||
border-right: var(--glass-border);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@import "./settings.css";
|
||||
@import "./common.css";
|
||||
|
||||
html {
|
||||
font-family: var(--font-family);
|
||||
|
@ -136,11 +22,6 @@ main {
|
|||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
tracks-view,
|
||||
albums-view,
|
||||
artists-view,
|
||||
|
@ -364,117 +245,3 @@ single-album-view {
|
|||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
#search-bar {
|
||||
position: absolute;
|
||||
top: calc(var(--library-top-nav-height) + var(--side-margin) * 3);
|
||||
right: var(--double-margin);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
gap: var(--double-margin);
|
||||
}
|
||||
|
||||
#search-bar button {
|
||||
border-radius: 100%;
|
||||
width: calc(var(--search-size) * 2);
|
||||
height: calc(var(--search-size) * 2);
|
||||
|
||||
font-size: var(--search-size);
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper input {
|
||||
/* 3 double margins: left side, flex gap, right side */
|
||||
width: calc(100vw - var(--double-margin) * 3 - var(--search-size) * 2);
|
||||
height: 100%;
|
||||
|
||||
font-size: var(--search-size);
|
||||
}
|
||||
|
||||
#search-bar-input-wrapper input::-webkit-search-cancel-button {
|
||||
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;
|
||||
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,
|
||||
#tracks-view #library-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
grid-auto-flow: dense;
|
||||
|
||||
/* Ensure the grid doesn't stretch when there's only a few elements */
|
||||
grid-auto-rows: min-content;
|
||||
|
||||
gap: var(--double-margin);
|
||||
padding: var(--double-margin);
|
||||
}
|
||||
|
||||
#tracks-view #library-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.library-list-shuffle-all {
|
||||
grid-column: 1 / -1;
|
||||
padding: var(--double-margin) 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.library-item-thumbnail {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library-item:active {
|
||||
background: var(--glass-background);
|
||||
}
|
||||
|
||||
.library-item h3 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue