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
|
gleam 1.4.1
|
||||||
nodejs 20.10.0
|
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"
|
name = "elektrofoni"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
target = "javascript"
|
target = "javascript"
|
||||||
gleam = ">= 0.33.0"
|
gleam = ">= 1.4.0"
|
||||||
|
|
||||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
# your project to the Hex package manager.
|
# your project to the Hex package manager.
|
||||||
|
@ -12,15 +12,15 @@ gleam = ">= 0.33.0"
|
||||||
# links = [{ title = "Website", href = "https://gleam.run" }]
|
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gleam_stdlib = "~> 0.34"
|
gleam_stdlib = "0.40.0"
|
||||||
gleam_json = "~> 0.7"
|
gleam_json = "~> 1.0"
|
||||||
gleam_http = "~> 3.5"
|
gleam_http = "~> 3.5"
|
||||||
gleam_javascript = "~> 0.7"
|
gleam_javascript = "0.11.0"
|
||||||
gleam_fetch = "~> 0.3"
|
gleam_fetch = "~> 1.0"
|
||||||
plinth = "~> 0.1"
|
plinth = "0.4.12"
|
||||||
varasto = "~> 2.0"
|
varasto = ">= 3.0.1 and < 4.0.0"
|
||||||
lustre = "~> 3.1"
|
lustre = "~> 4.3"
|
||||||
birl = "~> 1.3"
|
birl = "~> 1.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[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="icon" type="image/png" sizes="16x16" href="./priv/assets/favicon/favicon-16x16.png">
|
||||||
<link rel="manifest" href="./priv/assets/favicon/site.webmanifest">
|
<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">
|
<link rel="stylesheet" href="./style.css" type="text/css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
|
@ -2,33 +2,31 @@
|
||||||
# You typically do not need to edit this file
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
|
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
|
||||||
{ name = "birl", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "23BFE5AB0D7D9E4ECC5BB89B7ABDDF8E976D98C65D2E173D116E6AAFBF24E633" },
|
{ 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_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_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
|
||||||
{ 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 = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "4AE60B21A9A664137A79B1BEB93F751CB27F1DDED4086CA00C0260F5FFACBD80" },
|
||||||
{ 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.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
|
||||||
{ 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.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" },
|
||||||
{ 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 = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
|
||||||
{ 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_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.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
{ name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" },
|
||||||
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
|
||||||
{ 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 = "4.3.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BED65BD6A23439FB155A5BE166ED3C6BC20DED844FA9BB21840860951BC8E153" },
|
||||||
{ 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.4.12", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "3727B517C05BD4D49A4AF34BA7E63E03CA77B4E8AD2A4DCE60266B28717C1F9A" },
|
||||||
{ 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.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
|
||||||
{ name = "ranger", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "28E615AE7590ED922AF1510DDF606A2ECBBC2A9609AF36D412EDC925F06DFD20" },
|
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
|
||||||
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
|
{ name = "varasto", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "CEAB60C17DD9461E14E9DB8E6FF2A76E45ECCF704EC0555CE0398512068B01C1" },
|
||||||
{ 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" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[requirements]
|
[requirements]
|
||||||
birl = { version = "~> 1.3" }
|
birl = { version = "~> 1.7" }
|
||||||
gleam_fetch = { version = "~> 0.3" }
|
gleam_fetch = { version = "~> 1.0" }
|
||||||
gleam_http = { version = "~> 3.5" }
|
gleam_http = { version = "~> 3.5" }
|
||||||
gleam_javascript = { version = "~> 0.7" }
|
gleam_javascript = { version = "0.11.0" }
|
||||||
gleam_json = { version = "~> 0.7" }
|
gleam_json = { version = "~> 1.0" }
|
||||||
gleam_stdlib = { version = "~> 0.34" }
|
gleam_stdlib = { version = "0.40.0" }
|
||||||
gleeunit = { version = "~> 1.0" }
|
gleeunit = { version = "~> 1.2" }
|
||||||
lustre = { version = "~> 3.1" }
|
lustre = { version = "~> 4.3" }
|
||||||
plinth = { version = "~> 0.1" }
|
plinth = { version = "0.4.12" }
|
||||||
varasto = { version = "~> 2.0" }
|
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";
|
import { Ok, Error } from "./gleam.mjs";
|
||||||
|
|
||||||
export function newEvent(name, data) {
|
export function newEvent(name, data) {
|
||||||
return new CustomEvent(name, { detail: data });
|
return new CustomEvent(name, { detail: data, composed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDetail(e) {
|
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/library.{Library}
|
||||||
import elekf/transfer/album
|
import elekf/transfer/album
|
||||||
import elekf/transfer/artist
|
import elekf/transfer/artist
|
||||||
import elekf/transfer/track
|
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.
|
/// Converts API library response to library format.
|
||||||
pub fn from(library: APILibrary) {
|
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 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.
|
/// Converts API track response to library format.
|
||||||
pub fn from(track: APITrack) {
|
pub fn from(track: APITrack) {
|
||||||
|
@ -20,7 +20,7 @@ pub fn from(track: APITrack) {
|
||||||
genre: track.genre,
|
genre: track.genre,
|
||||||
length: track.length,
|
length: track.length,
|
||||||
album_id: track.album_id,
|
album_id: track.album_id,
|
||||||
artwork_id: artwork_id,
|
artwork_id:,
|
||||||
artist_id: track.artist_id,
|
artist_id: track.artist_id,
|
||||||
enid: track.enid,
|
enid: track.enid,
|
||||||
uploaded_on: track.uploaded_on,
|
uploaded_on: track.uploaded_on,
|
||||||
|
|
|
@ -1,48 +1,49 @@
|
||||||
//// The authed view manages the user's request config and displays the base
|
//// The authed view manages the user's request config and displays the base
|
||||||
//// view for the app.
|
//// 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 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/base_request_config.{base_request_config}
|
||||||
import elekf/api/history
|
import elekf/api/history
|
||||||
import elekf/utils/http
|
|
||||||
import elekf/utils/lustre
|
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/artist.{type Artist}
|
import elekf/library/artist.{type Artist}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/transfer/library as library_transfer
|
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/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/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 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.
|
/// Status of the current track that is being played.
|
||||||
pub type CurrentTrackStatus {
|
pub type CurrentTrackStatus {
|
||||||
|
@ -56,7 +57,7 @@ pub type PlayInfo {
|
||||||
PlayInfo(
|
PlayInfo(
|
||||||
track_id: Int,
|
track_id: Int,
|
||||||
track: Track,
|
track: Track,
|
||||||
play_queue: PlayQueue,
|
play_queue: play_queue.PlayQueue,
|
||||||
play_index: Int,
|
play_index: Int,
|
||||||
player: player_model.Model,
|
player: player_model.Model,
|
||||||
current_track_status: CurrentTrackStatus,
|
current_track_status: CurrentTrackStatus,
|
||||||
|
@ -83,7 +84,7 @@ pub type Msg {
|
||||||
UpdateAuthData(common.AuthData)
|
UpdateAuthData(common.AuthData)
|
||||||
LibraryResult(Result(library_api.ResponseData, http.ResponseError))
|
LibraryResult(Result(library_api.ResponseData, http.ResponseError))
|
||||||
PlayerMsg(player.Msg)
|
PlayerMsg(player.Msg)
|
||||||
StartPlay(PlayQueue, Int)
|
StartPlay(play_queue.PlayQueue, Int)
|
||||||
Router(router.Msg)
|
Router(router.Msg)
|
||||||
LibraryViewScrollToTopRequested
|
LibraryViewScrollToTopRequested
|
||||||
}
|
}
|
||||||
|
@ -183,7 +184,7 @@ pub fn update(model: Model, msg) {
|
||||||
info.play_index + 1
|
info.play_index + 1
|
||||||
_ -> 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(_) -> {
|
Ok(_) -> {
|
||||||
let #(status, effect) =
|
let #(status, effect) =
|
||||||
handle_start_play(model, info.play_queue, next_index)
|
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) {
|
fn handle_start_play(model: Model, queue: play_queue.PlayQueue, position: Int) {
|
||||||
let assert Ok(#(track_id, track)) = list.at(queue, position)
|
let assert Ok(#(track_id, track)) = forbidden_list.at(queue, position)
|
||||||
|
|
||||||
let #(player_model, maybe_init_effect) = case model.play_status {
|
let #(player_model, maybe_init_effect) = case model.play_status {
|
||||||
HasTracks(PlayInfo(player: p, ..)) -> #(p, effect.none())
|
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(_) {
|
event.on("click", fn(_) {
|
||||||
case current_view == target_view {
|
case current_view == target_view {
|
||||||
True -> Ok(LibraryViewScrollToTopRequested)
|
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
|
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.
|
/// Authentication data for the user.
|
||||||
pub type AuthData {
|
pub type AuthData {
|
||||||
AuthData(user: auth_models.User, device: auth_models.Device)
|
AuthData(user: auth_models.User, device: auth_models.Device)
|
||||||
|
|
|
@ -2,10 +2,6 @@ import lustre/attribute
|
||||||
import lustre/element
|
import lustre/element
|
||||||
import lustre/element/html.{div}
|
import lustre/element/html.{div}
|
||||||
|
|
||||||
/// An item in the library with its ID.
|
|
||||||
pub type LibraryItem(a) =
|
|
||||||
#(Int, a)
|
|
||||||
|
|
||||||
pub fn view(
|
pub fn view(
|
||||||
class: String,
|
class: String,
|
||||||
extra_attributes: List(attribute.Attribute(a)),
|
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
|
//// component cannot be used directly, this should be extended by the actual
|
||||||
//// components that focus on a specific type of view to the library.
|
//// components that focus on a specific type of view to the library.
|
||||||
|
|
||||||
import gleam/dynamic
|
import elekf/library.{type Library}
|
||||||
import gleam/list
|
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/dict
|
||||||
import gleam/option
|
import gleam/dynamic
|
||||||
import gleam/string
|
|
||||||
import gleam/result
|
|
||||||
import gleam/int
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option
|
||||||
|
import gleam/result
|
||||||
import gleam/set
|
import gleam/set
|
||||||
|
import gleam/string
|
||||||
import lustre
|
import lustre
|
||||||
|
import lustre/attribute
|
||||||
import lustre/effect
|
import lustre/effect
|
||||||
import lustre/element.{type Element, text}
|
import lustre/element.{type Element, text}
|
||||||
import lustre/element/html.{div, h1, p}
|
import lustre/element/html.{div, h1, p}
|
||||||
import lustre/attribute
|
|
||||||
import lustre/event
|
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.
|
/// Function to get the data of the view from the library.
|
||||||
pub type DataGetter(a, filter) =
|
pub type DataGetter(a, filter) =
|
||||||
|
@ -88,7 +90,7 @@ pub type Msg(filter) {
|
||||||
LibraryUpdated(Library)
|
LibraryUpdated(Library)
|
||||||
SettingsUpdated(option.Option(common.Settings))
|
SettingsUpdated(option.Option(common.Settings))
|
||||||
ShuffleAll
|
ShuffleAll
|
||||||
StartPlay(List(LibraryItem(Track)), Int)
|
StartPlay(play_queue.PlayQueue, Int)
|
||||||
Search(search.Msg)
|
Search(search.Msg)
|
||||||
FilterUpdated(filter)
|
FilterUpdated(filter)
|
||||||
ListScrolled(Float)
|
ListScrolled(Float)
|
||||||
|
@ -108,9 +110,12 @@ pub type Model(a, filter) {
|
||||||
settings: option.Option(common.Settings),
|
settings: option.Option(common.Settings),
|
||||||
history: history_store.StorageFormat,
|
history: history_store.StorageFormat,
|
||||||
history_api: history_store.HistoryStorage,
|
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.
|
/// Register the component as a custom element in the app.
|
||||||
///
|
///
|
||||||
/// This must be implemented by the client component, passing suitable values.
|
/// This must be implemented by the client component, passing suitable values.
|
||||||
|
@ -130,14 +135,16 @@ pub fn register(
|
||||||
filter: Filter(filter),
|
filter: Filter(filter),
|
||||||
search_filter: SearchFilter(a),
|
search_filter: SearchFilter(a),
|
||||||
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
extra_attrs: dict.Dict(String, dynamic.Decoder(Msg(filter))),
|
||||||
|
styles_path: String,
|
||||||
) {
|
) {
|
||||||
lustre.component(
|
let app =
|
||||||
name,
|
lustre.component(
|
||||||
fn() { init(name, filter, data_getter, shuffler, sorter) },
|
fn(_) { init(name, filter, data_getter, shuffler, sorter, styles_path) },
|
||||||
update,
|
update,
|
||||||
generate_view(header_view, item_view, search_filter),
|
generate_view(header_view, item_view, search_filter),
|
||||||
dict.merge(generic_attributes(), extra_attrs),
|
dict.merge(generic_attributes(), extra_attrs),
|
||||||
)
|
)
|
||||||
|
lustre.register(app, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the generic properties common to all library views, that can be input
|
/// 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")
|
@external(javascript, "../../../library_view_ffi.mjs", "requestScroll")
|
||||||
pub fn request_scroll(pos: Float) -> Nil
|
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 =
|
let scrollend_effect =
|
||||||
effect.from(fn(dispatch) {
|
effect.from(fn(dispatch) {
|
||||||
lustre_utils.after_next_render(fn() {
|
lustre_utils.after_next_render(fn() {
|
||||||
|
@ -189,17 +196,18 @@ pub fn init(id, filter, data_getter, shuffler, sorter) {
|
||||||
|
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
id,
|
id:,
|
||||||
filter,
|
filter:,
|
||||||
NoLibrary,
|
library_status: NoLibrary,
|
||||||
[],
|
data: [],
|
||||||
data_getter,
|
data_getter:,
|
||||||
shuffler,
|
shuffler:,
|
||||||
sorter,
|
sorter:,
|
||||||
search.init(),
|
search: search.init(),
|
||||||
option.None,
|
settings: option.None,
|
||||||
history,
|
history:,
|
||||||
history_api,
|
history_api:,
|
||||||
|
styles_path:,
|
||||||
),
|
),
|
||||||
effect.batch([scrollend_effect, scroll_to_effect]),
|
effect.batch([scrollend_effect, scroll_to_effect]),
|
||||||
)
|
)
|
||||||
|
@ -252,6 +260,13 @@ pub fn library_view(
|
||||||
item_view: ItemView(a, filter),
|
item_view: ItemView(a, filter),
|
||||||
search_filter: SearchFilter(a),
|
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 {
|
case model.library_status, model.filter {
|
||||||
HaveLibrary(lib), option.Some(f) -> {
|
HaveLibrary(lib), option.Some(f) -> {
|
||||||
let items = case model.search.search_text {
|
let items = case model.search.search_text {
|
||||||
|
@ -262,31 +277,39 @@ pub fn library_view(
|
||||||
|> list.filter(fn(item) { search_filter(item.1, search_txt) })
|
|> list.filter(fn(item) { search_filter(item.1, search_txt) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
element.fragment([
|
||||||
div(
|
styles,
|
||||||
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
|
div(
|
||||||
list.append(
|
[attribute.id("library-list"), scroll_to.on(ScrollRequested)],
|
||||||
[
|
list.append(
|
||||||
search.view(model.search)
|
[
|
||||||
|> element.map(Search),
|
search.view(model.search)
|
||||||
div(
|
|> element.map(Search),
|
||||||
[attribute.id("library-list-header")],
|
div(
|
||||||
header_view(model, lib, f, items),
|
[attribute.id("library-list-header")],
|
||||||
),
|
header_view(model, lib, f, items),
|
||||||
shuffle_all.view([event.on_click(ShuffleAll)]),
|
),
|
||||||
],
|
shuffle_all.view([event.on_click(ShuffleAll)]),
|
||||||
list.index_map(items, fn(item, i) {
|
],
|
||||||
|
list.index_map(items, fn(item, i) {
|
||||||
item_view(model, lib, items, i, item)
|
item_view(model, lib, items, i, item)
|
||||||
})
|
})
|
||||||
|> list.flatten(),
|
|> list.flatten(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
_, _ ->
|
_, _ ->
|
||||||
div(
|
element.fragment([
|
||||||
[attribute.id("library-list"), attribute.class("library-list-loading")],
|
styles,
|
||||||
[text("")],
|
div(
|
||||||
)
|
[
|
||||||
|
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 {
|
case model.library_status {
|
||||||
HaveLibrary(lib) -> {
|
HaveLibrary(lib) -> {
|
||||||
let tracks = model.shuffler(lib, model.data)
|
let tracks = model.shuffler(lib, model.data)
|
||||||
start_play.emit(tracks, 0)
|
start_play.emit(play_queue.from_list(tracks), 0)
|
||||||
}
|
}
|
||||||
NoLibrary -> effect.none()
|
NoLibrary -> effect.none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn library_decode(data: dynamic.Dynamic) {
|
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 {
|
case library {
|
||||||
option.Some(lib) -> Ok(LibraryUpdated(lib))
|
option.Some(lib) -> Ok(LibraryUpdated(lib))
|
||||||
option.None -> Error([])
|
option.None -> Error([])
|
||||||
|
@ -366,7 +389,8 @@ fn library_decode(data: dynamic.Dynamic) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_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))
|
Ok(SettingsUpdated(settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,8 +400,8 @@ fn update_data(model: Model(a, filter)) {
|
||||||
Model(
|
Model(
|
||||||
..model,
|
..model,
|
||||||
data: l
|
data: l
|
||||||
|> model.data_getter(f)
|
|> model.data_getter(f)
|
||||||
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
|> list.sort(fn(a, b) { model.sorter(a.1, b.1) }),
|
||||||
)
|
)
|
||||||
_, _ -> model
|
_, _ -> model
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
//// Library item view for a single album.
|
//// 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.{type Library}
|
||||||
import elekf/library/album.{type Album}
|
import elekf/library/album.{type Album}
|
||||||
import elekf/library/album_utils
|
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/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 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(
|
pub fn view(
|
||||||
library: Library,
|
library: Library,
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
//// A library view to all of the albums in the library.
|
//// 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.{type Library}
|
||||||
import elekf/library/album.{type Album}
|
import elekf/library/album.{type Album}
|
||||||
import elekf/library/album_utils
|
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/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"
|
const component_name = "albums-view"
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ pub fn register() {
|
||||||
option.Some(Nil),
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
|
library_view.default_styles_path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
//// A library view to all of the artists in the library.
|
//// 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.{type Library}
|
||||||
import elekf/library/artist.{type Artist}
|
import elekf/library/artist.{type Artist}
|
||||||
import elekf/library/artist_utils
|
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/common
|
||||||
import elekf/web/router
|
import elekf/web/components/library_view.{type Model}
|
||||||
import elekf/web/components/link
|
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"
|
const component_name = "artists-view"
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ pub fn register() {
|
||||||
option.Some(Nil),
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
dict.new(),
|
||||||
|
library_view.default_styles_path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
//// A library view to the current play queue.
|
//// 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.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/utils/order
|
import elekf/utils/order
|
||||||
import elekf/web/components/library_view.{type Model} as library_view
|
import elekf/web/codec/play_queue as play_queue_codec
|
||||||
import elekf/web/components/library_item.{type LibraryItem}
|
|
||||||
import elekf/web/components/library_views/track_item
|
|
||||||
import elekf/web/common
|
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"
|
const component_name = "play-queue-view"
|
||||||
|
|
||||||
|
@ -20,13 +24,14 @@ pub fn register() {
|
||||||
library_view.register(
|
library_view.register(
|
||||||
component_name,
|
component_name,
|
||||||
data_getter,
|
data_getter,
|
||||||
library_view.empty_header,
|
fn(m, l, f, i) { library_view.stats_header("Play queue", m, l, f, i) },
|
||||||
item_view,
|
item_view,
|
||||||
shuffler,
|
shuffler,
|
||||||
order.noop,
|
order.noop,
|
||||||
option.None,
|
option.None,
|
||||||
search_filter,
|
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)
|
library_view.render(component_name, library, settings, extra_attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_getter(library: Library, _filter: Nil) {
|
fn data_getter(_library: Library, filter: PlayQueue) {
|
||||||
dict.to_list(library.tracks)
|
play_queue.to_list(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shuffler(_library, items) {
|
fn shuffler(_library, items) {
|
||||||
|
@ -52,7 +57,7 @@ fn search_filter(item: Track, search_text: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_view(
|
fn item_view(
|
||||||
_model: Model(Track, Nil),
|
_model: Model(Track, PlayQueue),
|
||||||
library: Library,
|
library: Library,
|
||||||
items: List(LibraryItem(Track)),
|
items: List(LibraryItem(Track)),
|
||||||
index: Int,
|
index: Int,
|
||||||
|
@ -60,3 +65,8 @@ fn item_view(
|
||||||
) {
|
) {
|
||||||
track_item.view(library, items, index, item, "track-list")
|
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.
|
//// 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.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
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/common
|
||||||
|
import elekf/web/components/library_view
|
||||||
|
import elekf/web/components/library_views/track_item
|
||||||
import elekf/web/components/track_length
|
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"
|
const component_name = "single-album-view"
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ pub fn register() {
|
||||||
option.None,
|
option.None,
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.from_list([#("album-id", id_decode)]),
|
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) {
|
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))
|
Ok(library_view.FilterUpdated(album_id))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
//// A library view to a single artist's albums and non-album tracks.
|
//// 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.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
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/common
|
||||||
|
import elekf/web/components/library_view
|
||||||
|
import elekf/web/components/library_views/track_item
|
||||||
import elekf/web/components/track_length
|
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"
|
const component_name = "single-artist-view"
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ pub fn register() {
|
||||||
option.None,
|
option.None,
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.from_list([#("artist-id", id_decode)]),
|
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) {
|
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))
|
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.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
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/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(
|
pub fn view(
|
||||||
library: Library,
|
library: Library,
|
||||||
|
@ -18,13 +20,14 @@ pub fn view(
|
||||||
let #(track_id, track) = item
|
let #(track_id, track) = item
|
||||||
let album = library.assert_album(library, track.album_id)
|
let album = library.assert_album(library, track.album_id)
|
||||||
let artist = library.assert_artist(library, track.artist_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",
|
"track-item",
|
||||||
[
|
[
|
||||||
attribute.id(id_prefix <> "-" <> int.to_string(track_id)),
|
attribute.id(id_prefix <> "-" <> int.to_string(track_id)),
|
||||||
attribute.type_("button"),
|
attribute.type_("button"),
|
||||||
event.on_click(StartPlay(items, index)),
|
event.on_click(StartPlay(queue, index)),
|
||||||
attribute.attribute("role", "button"),
|
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.
|
//// 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.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/track_utils
|
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/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"
|
const component_name = "tracks-view"
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ pub fn register() {
|
||||||
option.Some(Nil),
|
option.Some(Nil),
|
||||||
search_filter,
|
search_filter,
|
||||||
dict.new(),
|
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
|
//// A track for visualising the current song's progress and skipping it back
|
||||||
//// and forth, and additional timestamps.
|
//// 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.{
|
import elekf/web/components/player/actions.{
|
||||||
Commit, EndUserSkip, Ephemeral, SelectPosition, StartUserSkip,
|
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) {
|
pub fn view(model: model.Model) {
|
||||||
let current_time_padding = case model.track.length > 3600 {
|
let current_time_padding = case model.track.length > 3600 {
|
||||||
|
@ -19,7 +18,7 @@ pub fn view(model: model.Model) {
|
||||||
False -> track_length.Auto
|
False -> track_length.Auto
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_pos = dynamic.from(int.to_string(model.position))
|
let track_pos = int.to_string(model.position)
|
||||||
|
|
||||||
[
|
[
|
||||||
p(
|
p(
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
//// The search view renders a search bar, other views must do the searching
|
//// The search view renders a search bar, other views must do the searching
|
||||||
//// based on the emitted messages.
|
//// based on the emitted messages.
|
||||||
|
|
||||||
import gleam/dynamic
|
|
||||||
import lustre/element/html.{div, input}
|
|
||||||
import lustre/attribute
|
|
||||||
import lustre/event
|
|
||||||
import elekf/web/components/icon.{Alt, icon}
|
|
||||||
import elekf/web/components/button
|
|
||||||
import elekf/utils/lustre
|
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"
|
const search_bar_input_id = "search-bar-input"
|
||||||
|
|
||||||
|
@ -51,7 +50,7 @@ pub fn view(model: Model) {
|
||||||
attribute.type_("search"),
|
attribute.type_("search"),
|
||||||
attribute.placeholder("Search"),
|
attribute.placeholder("Search"),
|
||||||
attribute.attribute("aria-label", "Search through content"),
|
attribute.attribute("aria-label", "Search through content"),
|
||||||
attribute.value(dynamic.from(model.search_text)),
|
attribute.value(model.search_text),
|
||||||
event.on_input(UpdateSearch),
|
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 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"
|
pub const event_name = "scroll-to"
|
||||||
|
|
||||||
|
@ -10,14 +12,14 @@ pub type EventData {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(pos: Float) {
|
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) {
|
pub fn on(msg: fn(Float) -> b) {
|
||||||
event.on(event_name, fn(data) {
|
event.on(event_name, fn(data) {
|
||||||
data
|
data
|
||||||
|> decoder
|
|> 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) {
|
fn decoder(data: dynamic.Dynamic) {
|
||||||
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
let e: CustomEvent = forbidden_dynamic.unsafe_coerce(data)
|
||||||
use detail <- result.try(custom_event.get_detail(e))
|
use detail <- result.try(
|
||||||
use event_data <- result.try(
|
custom_event.get_detail(e)
|
||||||
data_decoder()(detail)
|
|> result.replace_error([
|
||||||
|> result.nil_error(),
|
dynamic.DecodeError("event detail", "no detail", []),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
|
use event_data <- result.try(dynamic.float(detail))
|
||||||
|
|
||||||
Ok(event_data)
|
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/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"
|
const event_name = "start-play"
|
||||||
|
|
||||||
|
@ -11,7 +14,13 @@ pub type EventData {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(tracks: PlayQueue, position: Int) {
|
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) {
|
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) {
|
fn decoder(data: dynamic.Dynamic) {
|
||||||
let e: CustomEvent = dynamic.unsafe_coerce(data)
|
let e: CustomEvent = forbidden_dynamic.unsafe_coerce(data)
|
||||||
let assert Ok(detail) = custom_event.get_detail(e)
|
use detail <- result.try(
|
||||||
let event_data: EventData =
|
custom_event.get_detail(e)
|
||||||
detail
|
|> result.replace_error([
|
||||||
|> dynamic.unsafe_coerce()
|
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)
|
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
|
//// See https://devguide.ibroadcast.com/?p=api#Play-History
|
||||||
|
|
||||||
|
import birl
|
||||||
import gleam/dict.{type Dict}
|
import gleam/dict.{type Dict}
|
||||||
import gleam/json
|
import gleam/dynamic
|
||||||
import gleam/int
|
import gleam/int
|
||||||
|
import gleam/javascript/promise
|
||||||
|
import gleam/json
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/dynamic
|
import ibroadcast/auth/status
|
||||||
import gleam/option
|
|
||||||
import gleam/javascript/promise
|
|
||||||
import birl
|
|
||||||
import ibroadcast/request.{DecodeFailed}
|
|
||||||
import ibroadcast/authed_request.{type RequestConfig}
|
import ibroadcast/authed_request.{type RequestConfig}
|
||||||
import ibroadcast/http.{type Requestor}
|
import ibroadcast/http.{type Requestor}
|
||||||
import ibroadcast/auth/status
|
import ibroadcast/request.{DecodeFailed}
|
||||||
import ibroadcast/request_params
|
import ibroadcast/request_params
|
||||||
import ibroadcast/servers
|
import ibroadcast/servers
|
||||||
import ibroadcast/time
|
import ibroadcast/time
|
||||||
|
@ -77,15 +76,18 @@ pub fn add_to_day(day: HistoryDay, track_id: String, event: Event) -> HistoryDay
|
||||||
Skip(_) -> day.plays
|
Skip(_) -> day.plays
|
||||||
}
|
}
|
||||||
|
|
||||||
let detail =
|
let old_detail = dict.get(day.detail, track_id)
|
||||||
dict.update(day.detail, track_id, fn(old_detail) {
|
|
||||||
case old_detail {
|
|
||||||
option.Some(old_detail) -> list.append(old_detail, [event])
|
|
||||||
option.None -> [event]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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() {
|
fn payload_decoder() {
|
||||||
|
@ -98,16 +100,16 @@ fn day_serializer(day: HistoryDay) -> json.Json {
|
||||||
"day",
|
"day",
|
||||||
json.string(
|
json.string(
|
||||||
int.to_string(day.day.year)
|
int.to_string(day.day.year)
|
||||||
<> "-"
|
<> "-"
|
||||||
<> {
|
<> {
|
||||||
int.to_string(day.day.month)
|
int.to_string(day.day.month)
|
||||||
|> time.pad_time_part()
|
|> time.pad_time_part()
|
||||||
}
|
}
|
||||||
<> "-"
|
<> "-"
|
||||||
<> {
|
<> {
|
||||||
int.to_string(day.day.date)
|
int.to_string(day.day.date)
|
||||||
|> time.pad_time_part()
|
|> time.pad_time_part()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
#(
|
#(
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import gleam/javascript/promise
|
|
||||||
import gleam/json
|
|
||||||
import gleam/dict.{type Dict}
|
import gleam/dict.{type Dict}
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
import gleam/result
|
import gleam/javascript/promise
|
||||||
|
import gleam/json
|
||||||
import gleam/option
|
import gleam/option
|
||||||
import ibroadcast/servers
|
import gleam/result
|
||||||
import ibroadcast/request.{DecodeFailed}
|
|
||||||
import ibroadcast/authed_request.{type RequestConfig}
|
import ibroadcast/authed_request.{type RequestConfig}
|
||||||
import ibroadcast/request_params.{type RequestParams}
|
|
||||||
import ibroadcast/http.{type Requestor}
|
import ibroadcast/http.{type Requestor}
|
||||||
import ibroadcast/library_format
|
import ibroadcast/library_format
|
||||||
|
import ibroadcast/request.{DecodeFailed}
|
||||||
|
import ibroadcast/request_params.{type RequestParams}
|
||||||
|
import ibroadcast/servers
|
||||||
|
|
||||||
pub type Album {
|
pub type Album {
|
||||||
Album(
|
Album(
|
||||||
|
@ -189,26 +189,26 @@ fn track_decoder() {
|
||||||
use uploaded_time <- result.try(dynamic.element(19, dynamic.string)(data))
|
use uploaded_time <- result.try(dynamic.element(19, dynamic.string)(data))
|
||||||
|
|
||||||
Ok(Track(
|
Ok(Track(
|
||||||
number: number,
|
number:,
|
||||||
year: year,
|
year:,
|
||||||
title: title,
|
title:,
|
||||||
genre: genre,
|
genre:,
|
||||||
length: length,
|
length:,
|
||||||
album_id: album_id,
|
album_id:,
|
||||||
artwork_id: artwork_id,
|
artwork_id:,
|
||||||
artist_id: artist_id,
|
artist_id:,
|
||||||
enid: enid,
|
enid:,
|
||||||
uploaded_on: uploaded_on,
|
uploaded_on:,
|
||||||
trashed: trashed,
|
trashed:,
|
||||||
size: size,
|
size:,
|
||||||
path: path,
|
path:,
|
||||||
uid: uid,
|
uid:,
|
||||||
rating: rating,
|
rating:,
|
||||||
plays: plays,
|
plays:,
|
||||||
file: file,
|
file:,
|
||||||
type_: type_,
|
type_:,
|
||||||
replay_gain: replay_gain,
|
replay_gain:,
|
||||||
uploaded_time: uploaded_time,
|
uploaded_time:,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
237
style.css
237
style.css
|
@ -1,119 +1,5 @@
|
||||||
@font-face {
|
@import "./settings.css";
|
||||||
font-family: "Open Sans";
|
@import "./common.css";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
@ -136,11 +22,6 @@ main {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks-view,
|
tracks-view,
|
||||||
albums-view,
|
albums-view,
|
||||||
artists-view,
|
artists-view,
|
||||||
|
@ -364,117 +245,3 @@ single-album-view {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.5rem;
|
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