Compare commits
10 commits
22f268e686
...
5e6541dce4
Author | SHA1 | Date | |
---|---|---|---|
5e6541dce4 | |||
074e7afa52 | |||
2163fe787f | |||
718a789e3c | |||
f3a49f2dc5 | |||
72a045d4bc | |||
251181843f | |||
97efbcec1b | |||
b5c2694bc0 | |||
9224aee6f8 |
64 changed files with 1449 additions and 884 deletions
|
@ -6,6 +6,7 @@ node_modules
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
/scripts
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: svelte-lint
|
- id: svelte-check
|
||||||
name: svelte-lint
|
name: svelte-check
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
entry: npm run lint
|
entry: npm run check
|
||||||
|
files: \.(js|ts|svelte)$
|
||||||
|
- id: svelte-prettier
|
||||||
|
name: prettier
|
||||||
|
language: system
|
||||||
|
entry: npx prettier --write --ignore-unknown
|
||||||
|
types: [text]
|
||||||
|
- id: svelte-lint
|
||||||
|
name: eslint
|
||||||
|
language: system
|
||||||
|
entry: npx eslint
|
||||||
|
files: \.(js|ts|svelte)$
|
||||||
|
|
|
@ -7,7 +7,7 @@ node_modules
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
/src/paraglide
|
/src/i18n
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
5
.typesafe-i18n.json
Normal file
5
.typesafe-i18n.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||||
|
"adapter": "svelte",
|
||||||
|
"baseLocale": "en"
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
# TIRAYA frontend
|
# <img src="https://raw.githubusercontent.com/TirayaMusic/meta/main/dist/header.svg" alt="TIRAYA" height="100"> frontend
|
||||||
|
|
||||||
Frontend for the TIRAYA music streaming service.
|
Frontend for the TIRAYA music streaming service.
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
|
||||||
"hello_world": "Hallo Welt",
|
"hello_world": "Hallo Welt",
|
||||||
"search": "Suche",
|
"search": "Suche",
|
||||||
"home": "Startseite",
|
"home": "Startseite",
|
||||||
|
@ -29,11 +28,11 @@
|
||||||
"unmute": "Stummschaltung aufheben",
|
"unmute": "Stummschaltung aufheben",
|
||||||
"sort_date": "Nach Datum sortieren",
|
"sort_date": "Nach Datum sortieren",
|
||||||
"sort_name": "Alphabetisch sortieren",
|
"sort_name": "Alphabetisch sortieren",
|
||||||
"play_item": "{title} abspielen",
|
"play_item": "{0} abspielen",
|
||||||
"library_add": "Zur Sammlung hinzufügen",
|
"library_add": "Zur Sammlung hinzufügen",
|
||||||
"library_remove": "Von der Sammlung entfernen",
|
"library_remove": "Von der Sammlung entfernen",
|
||||||
"search_no_result": "Es gibt keine {itemType}, die deiner Suche entsprechen",
|
"search_no_result": "Es gibt keine {0|{artist: Künstler, album: Alben, track: Titel, playlist: Playlists, *: Elemente}}, die deiner Suche entsprechen",
|
||||||
"empty_list": "Diese Liste enthält keine {itemType}",
|
"empty_list": "Diese Liste enthält keine {0|{artist: Künstler, album: Alben, track: Titel, playlist: Playlists, *: Elemente}}",
|
||||||
"clear_search": "Suche löschen",
|
"clear_search": "Suche löschen",
|
||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
"album": "Album",
|
"album": "Album",
|
||||||
|
@ -42,8 +41,8 @@
|
||||||
"added_by": "Hinzugefügt von",
|
"added_by": "Hinzugefügt von",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"singles": "Singles",
|
"singles": "Singles",
|
||||||
"search_item": "{item} durchsuchen",
|
"search_item": "{0} durchsuchen",
|
||||||
"top_tracks_of": "Beliebte Titel von {name}",
|
"top_tracks_of": "Beliebte Titel von {0}",
|
||||||
"all_tracks": "Alle Titel",
|
"all_tracks": "Alle Titel",
|
||||||
"notifications": "Benachrichtigungen",
|
"notifications": "Benachrichtigungen",
|
||||||
"playback_history": "Wiedergabeverlauf",
|
"playback_history": "Wiedergabeverlauf",
|
||||||
|
@ -60,7 +59,7 @@
|
||||||
"copy_url": "URL kopieren",
|
"copy_url": "URL kopieren",
|
||||||
"download_audio_files": "Audiodateien herunterladen",
|
"download_audio_files": "Audiodateien herunterladen",
|
||||||
"search_create_playlist": "Playlist suchen/erstellen",
|
"search_create_playlist": "Playlist suchen/erstellen",
|
||||||
"playlist_new_name": "Playlist \"{name}\" erstellen",
|
"playlist_new_name": "Playlist \"{0}\" erstellen",
|
||||||
"view_artist": "Zeige Künstler",
|
"view_artist": "Zeige Künstler",
|
||||||
"view_album": "Zeige Album",
|
"view_album": "Zeige Album",
|
||||||
"view_track_info": "Zeige Titelinfo",
|
"view_track_info": "Zeige Titelinfo",
|
||||||
|
@ -68,5 +67,13 @@
|
||||||
"playlist_remove": "Von Playlist entfernen",
|
"playlist_remove": "Von Playlist entfernen",
|
||||||
"download_audio_file": "Audiodatei herunterladen",
|
"download_audio_file": "Audiodatei herunterladen",
|
||||||
"copy_urls": "URLs kopieren",
|
"copy_urls": "URLs kopieren",
|
||||||
"language": "Sprache"
|
"language": "Sprache",
|
||||||
|
"login": "Anmeldung",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"documentation": "Dokumentation",
|
||||||
|
"frontend": "Frontend",
|
||||||
|
"n_artists": "{0} Künstler",
|
||||||
|
"n_albums": "{0} {{Album|Alben}}",
|
||||||
|
"n_tracks": "{0} Titel",
|
||||||
|
"n_playlists": "{0} Playlist{{|s}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
|
||||||
"hello_world": "Hello World",
|
"hello_world": "Hello World",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
|
@ -29,11 +28,11 @@
|
||||||
"unmute": "Unmute",
|
"unmute": "Unmute",
|
||||||
"sort_date": "Sort by date",
|
"sort_date": "Sort by date",
|
||||||
"sort_name": "Sort by name",
|
"sort_name": "Sort by name",
|
||||||
"play_item": "Play {title}",
|
"play_item": "Play {0}",
|
||||||
"library_add": "Add to library",
|
"library_add": "Add to library",
|
||||||
"library_remove": "Remove from library",
|
"library_remove": "Remove from library",
|
||||||
"search_no_result": "There are no {itemType} matching your search",
|
"search_no_result": "There are no {0|{artist: artists, album: albums, track: tracks, playlist: playlists, *: items}} matching your search",
|
||||||
"empty_list": "There are no {itemType} in this list",
|
"empty_list": "There are no {0|{artist: artists, album: albums, track: tracks, playlist: playlists, *: items}} in this list",
|
||||||
"clear_search": "Clear search",
|
"clear_search": "Clear search",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"album": "Album",
|
"album": "Album",
|
||||||
|
@ -42,8 +41,8 @@
|
||||||
"added_by": "Added by",
|
"added_by": "Added by",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"singles": "Singles",
|
"singles": "Singles",
|
||||||
"search_item": "Search {item}",
|
"search_item": "Search {0}",
|
||||||
"top_tracks_of": "Top tracks of {name}",
|
"top_tracks_of": "Top tracks of {0}",
|
||||||
"all_tracks": "All tracks",
|
"all_tracks": "All tracks",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"playback_history": "Playback history",
|
"playback_history": "Playback history",
|
||||||
|
@ -60,7 +59,7 @@
|
||||||
"copy_url": "Copy URL",
|
"copy_url": "Copy URL",
|
||||||
"download_audio_files": "Download audio files",
|
"download_audio_files": "Download audio files",
|
||||||
"search_create_playlist": "Search/create playlist",
|
"search_create_playlist": "Search/create playlist",
|
||||||
"playlist_new_name": "Create playlist \"{name}\"",
|
"playlist_new_name": "Create playlist \"{0}\"",
|
||||||
"view_artist": "View artist",
|
"view_artist": "View artist",
|
||||||
"view_album": "View album",
|
"view_album": "View album",
|
||||||
"view_track_info": "View track info",
|
"view_track_info": "View track info",
|
||||||
|
@ -68,5 +67,13 @@
|
||||||
"playlist_remove": "Remove from playlist",
|
"playlist_remove": "Remove from playlist",
|
||||||
"download_audio_file": "Download audio file",
|
"download_audio_file": "Download audio file",
|
||||||
"copy_urls": "Copy URLs",
|
"copy_urls": "Copy URLs",
|
||||||
"language": "Language"
|
"language": "Language",
|
||||||
|
"login": "Login",
|
||||||
|
"settings": "Settings",
|
||||||
|
"documentation": "Documentation",
|
||||||
|
"frontend": "Frontend",
|
||||||
|
"n_artists": "{0} artist{{|s}}",
|
||||||
|
"n_albums": "{0} album{{|s}}",
|
||||||
|
"n_tracks": "{0} track{{|s}}",
|
||||||
|
"n_playlists": "{0} playlist{{|s}}"
|
||||||
}
|
}
|
||||||
|
|
24
package.json
24
package.json
|
@ -5,32 +5,33 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "paraglide-js compile --project ./project.inlang && vite build",
|
"build": "npm run typesafe-i18n && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"lint": "prettier --plugin prettier-plugin-svelte --check . && eslint . && npm run check",
|
"lint": "prettier --plugin prettier-plugin-svelte --check . && eslint . && npm run check",
|
||||||
"format": "prettier --plugin prettier-plugin-svelte --write .",
|
"format": "prettier --plugin prettier-plugin-svelte --write .",
|
||||||
"postinstall": "paraglide-js compile --project ./project.inlang"
|
"typesafe-i18n": "tsx scripts/import_translations.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.0.16",
|
"@fontsource-variable/inter": "^5.0.16",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@resreq/event-hub": "^1.6.0",
|
"@resreq/event-hub": "^1.6.0",
|
||||||
"@tirayamusic/melt-ui": "^0.37.5",
|
"@tirayamusic/melt-ui": "^0.37.5",
|
||||||
"daisyui": "^3.9.4",
|
"daisyui": "^4.4.20",
|
||||||
"fast-average-color": "^9.4.0",
|
"fast-average-color": "^9.4.0",
|
||||||
"svelte-inview": "^4.0.1",
|
"svelte-inview": "^4.0.1",
|
||||||
"svelte-local-storage-store": "^0.5.0"
|
"svelte-local-storage-store": "^0.5.0",
|
||||||
|
"typesafe-i18n": "^5.26.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inlang/paraglide-js": "1.0.0-prerelease.19",
|
|
||||||
"@inlang/paraglide-js-adapter-vite": "^1.0.2",
|
|
||||||
"@melt-ui/pp": "^0.1.4",
|
"@melt-ui/pp": "^0.1.4",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-node": "^2.0.0",
|
||||||
"@sveltejs/kit": "^1.27.7",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||||
"@typescript-eslint/parser": "^6.13.2",
|
"@typescript-eslint/parser": "^6.13.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
@ -47,9 +48,10 @@
|
||||||
"svelte-sequential-preprocessor": "^2.0.1",
|
"svelte-sequential-preprocessor": "^2.0.1",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
|
"tsx": "^4.6.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.5.1",
|
"vite": "^5.0.0",
|
||||||
"vite-bundle-visualizer": "^0.10.1",
|
"vite-bundle-visualizer": "^1.0.0",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1348
pnpm-lock.yaml
1348
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://inlang.com/schema/project-settings",
|
|
||||||
"sourceLanguageTag": "en",
|
|
||||||
"languageTags": ["en", "de"],
|
|
||||||
"modules": [
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@latest/dist/index.js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
|
|
||||||
],
|
|
||||||
"plugin.inlang.messageFormat": {
|
|
||||||
"pathPattern": "./messages/{languageTag}.json"
|
|
||||||
}
|
|
||||||
}
|
|
32
scripts/import_translations.ts
Normal file
32
scripts/import_translations.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Import translations from the /messages folder and
|
||||||
|
* convert them to typesafe-i18n files.
|
||||||
|
*/
|
||||||
|
import type { BaseTranslation } from "typesafe-i18n";
|
||||||
|
import {
|
||||||
|
storeTranslationToDisk,
|
||||||
|
type ImportLocaleMapping,
|
||||||
|
} from "typesafe-i18n/importer";
|
||||||
|
import type { Locales } from "../src/i18n/i18n-types";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const importTranslationFile = async (path: string, locale: string) => {
|
||||||
|
const data = fs.readFileSync(path, "utf-8");
|
||||||
|
const translation = JSON.parse(data);
|
||||||
|
|
||||||
|
const localeMapping: ImportLocaleMapping = {
|
||||||
|
locale,
|
||||||
|
translations: translation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await storeTranslationToDisk(localeMapping);
|
||||||
|
|
||||||
|
console.log(`translations imported for locale '${result}'`);
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.readdirSync("messages").forEach((file) => {
|
||||||
|
if (file.endsWith(".json")) {
|
||||||
|
const locale = file.substring(0, file.length - 5);
|
||||||
|
importTranslationFile(`messages/${file}`, locale);
|
||||||
|
}
|
||||||
|
});
|
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
|
@ -2,12 +2,16 @@
|
||||||
|
|
||||||
import type { Coords, Direction } from "$lib/util/types";
|
import type { Coords, Direction } from "$lib/util/types";
|
||||||
import type { MeltEventHandler } from "@tirayamusic/melt-ui/internal/types";
|
import type { MeltEventHandler } from "@tirayamusic/melt-ui/internal/types";
|
||||||
|
import type { Locales, TranslationFunctions } from "$i18n/i18n-types";
|
||||||
|
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
|
locale: Locales;
|
||||||
|
LL: TranslationFunctions;
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="tiraya">
|
<html lang="%lang%" data-theme="tiraya">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
||||||
|
|
34
src/hooks.server.ts
Normal file
34
src/hooks.server.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Locales } from "$i18n/i18n-types";
|
||||||
|
import { detectLocale, i18n, locales } from "$i18n/i18n-util";
|
||||||
|
import { loadAllLocales } from "$i18n/i18n-util.sync";
|
||||||
|
import type { Handle, RequestEvent } from "@sveltejs/kit";
|
||||||
|
import { initAcceptLanguageHeaderDetector } from "typesafe-i18n/detectors";
|
||||||
|
|
||||||
|
loadAllLocales();
|
||||||
|
const L = i18n();
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
const locale = getPreferredLocale(event);
|
||||||
|
const LL = L[locale];
|
||||||
|
|
||||||
|
// bind locale and translation functions to current request
|
||||||
|
event.locals.locale = locale;
|
||||||
|
event.locals.LL = LL;
|
||||||
|
|
||||||
|
// replace html lang attribute with correct language
|
||||||
|
return resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => html.replace("%lang%", locale),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreferredLocale = ({ request, cookies }: RequestEvent) => {
|
||||||
|
const langCookie = cookies.get("lang");
|
||||||
|
if (langCookie && locales.indexOf(langCookie as Locales) !== -1)
|
||||||
|
return langCookie as Locales;
|
||||||
|
|
||||||
|
// detect the preferred language the user has configured in his browser
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
|
||||||
|
const acceptLanguageDetector = initAcceptLanguageHeaderDetector(request);
|
||||||
|
|
||||||
|
return detectLocale(acceptLanguageDetector);
|
||||||
|
};
|
13
src/i18n/formatters.ts
Normal file
13
src/i18n/formatters.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { FormattersInitializer } from "typesafe-i18n";
|
||||||
|
import type { Locales, Formatters } from "./i18n-types";
|
||||||
|
import { date } from "typesafe-i18n/formatters";
|
||||||
|
|
||||||
|
export const initFormatters: FormattersInitializer<Locales, Formatters> = (
|
||||||
|
locale: Locales
|
||||||
|
) => {
|
||||||
|
const formatters: Formatters = {
|
||||||
|
simpleDate: date(locale, { day: "numeric", month: "short", year: "numeric" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatters;
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiPlus } from "@mdi/js";
|
import { mdiPlus } from "@mdi/js";
|
||||||
import { melt } from "@tirayamusic/melt-ui";
|
import { melt } from "@tirayamusic/melt-ui";
|
||||||
import type { MeltEvent } from "@tirayamusic/melt-ui/internal/types";
|
import type { MeltEvent } from "@tirayamusic/melt-ui/internal/types";
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<div class="item noicon">
|
<div class="item noicon">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={m.search_create_playlist()}
|
placeholder={$LL.search_create_playlist()}
|
||||||
on:keydown={onPlaylistSearchKeydown}
|
on:keydown={onPlaylistSearchKeydown}
|
||||||
bind:this={playlistSearchElm}
|
bind:this={playlistSearchElm}
|
||||||
bind:value={playlistSearch}
|
bind:value={playlistSearch}
|
||||||
|
@ -77,8 +77,8 @@
|
||||||
<Icon path={mdiPlus} size={1.4} />
|
<Icon path={mdiPlus} size={1.4} />
|
||||||
<span
|
<span
|
||||||
>{playlistSearch
|
>{playlistSearch
|
||||||
? m.playlist_new_name({ name: playlistSearch })
|
? $LL.playlist_new_name(playlistSearch)
|
||||||
: m.playlist_new()}</span
|
: $LL.playlist_new()}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- Context menu for artists, albums and playlists -->
|
<!-- Context menu for artists, albums and playlists -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiContentCopy,
|
mdiContentCopy,
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
|
@ -44,40 +44,40 @@
|
||||||
<div class="ctxmenu" use:melt={menu}>
|
<div class="ctxmenu" use:melt={menu}>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiPlaylistPlay} size={1.4} />
|
<Icon path={mdiPlaylistPlay} size={1.4} />
|
||||||
<span>{m.play_next()}</span>
|
<span>{$LL.play_next()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiPlaylistPlus} size={1.4} />
|
<Icon path={mdiPlaylistPlus} size={1.4} />
|
||||||
<span>{m.add_to_queue()}</span>
|
<span>{$LL.add_to_queue()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiHeartOutline} size={1.4} />
|
<Icon path={mdiHeartOutline} size={1.4} />
|
||||||
<span>{m.library_add()}</span>
|
<span>{$LL.library_add()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={$playlistMenuTrigger}>
|
<div class="item" use:melt={$playlistMenuTrigger}>
|
||||||
<Icon path={mdiPlaylistMusic} size={1.4} />
|
<Icon path={mdiPlaylistMusic} size={1.4} />
|
||||||
<span>{m.playlist_add()}</span>
|
<span>{$LL.playlist_add()}</span>
|
||||||
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiPencil} size={1.4} />
|
<Icon path={mdiPencil} size={1.4} />
|
||||||
<span>{m.edit()}</span>
|
<span>{$LL.edit()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiTrashCan} size={1.4} />
|
<Icon path={mdiTrashCan} size={1.4} />
|
||||||
<span>{m.trash()}</span>
|
<span>{$LL.trash()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiReload} size={1.4} />
|
<Icon path={mdiReload} size={1.4} />
|
||||||
<span>{m.update_now()}</span>
|
<span>{$LL.update_now()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item sep" use:melt={$shareMenuTrigger}>
|
<div class="item sep" use:melt={$shareMenuTrigger}>
|
||||||
<Icon path={mdiShare} size={1.4} />
|
<Icon path={mdiShare} size={1.4} />
|
||||||
<span>{m.share()}</span>
|
<span>{$LL.share()}</span>
|
||||||
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,11 +87,11 @@
|
||||||
<div class="ctxmenu" use:melt={$shareMenu}>
|
<div class="ctxmenu" use:melt={$shareMenu}>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiContentCopy} size={1.4} />
|
<Icon path={mdiContentCopy} size={1.4} />
|
||||||
<span>{m.copy_url()}</span>
|
<span>{$LL.copy_url()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiDownload} size={1.4} />
|
<Icon path={mdiDownload} size={1.4} />
|
||||||
<span>{m.download_audio_files()}</span>
|
<span>{$LL.download_audio_files()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiLogin, mdiCog, mdiBookOpen } from "@mdi/js";
|
import { mdiLogin, mdiCog, mdiBookOpen } from "@mdi/js";
|
||||||
import { melt } from "@tirayamusic/melt-ui";
|
import { melt } from "@tirayamusic/melt-ui";
|
||||||
|
|
||||||
|
@ -17,19 +18,19 @@
|
||||||
<div class="ctxmenu" use:melt={menu}>
|
<div class="ctxmenu" use:melt={menu}>
|
||||||
<div class="item" use:melt={item} on:m-click={() => alert("login")}>
|
<div class="item" use:melt={item} on:m-click={() => alert("login")}>
|
||||||
<Icon path={mdiLogin} size={1.4} />
|
<Icon path={mdiLogin} size={1.4} />
|
||||||
<span>Login</span>
|
<span>{$LL.login()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiCog} size={1.4} />
|
<Icon path={mdiCog} size={1.4} />
|
||||||
<span>Settings</span>
|
<span>{$LL.settings()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiBookOpen} size={1.4} />
|
<Icon path={mdiBookOpen} size={1.4} />
|
||||||
<span>Documentation</span>
|
<span>{$LL.documentation()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={iconTiraya} size={1.4} />
|
<Icon path={iconTiraya} size={1.4} />
|
||||||
<span>Tiraya 0.1.0 • FE: 0.1.0</span>
|
<span class="text-xs">{$LL.app_name()} 0.1.0<br />{$LL.frontend()} 0.1.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- Context menu for tracks -->
|
<!-- Context menu for tracks -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiPlaylistPlay,
|
mdiPlaylistPlay,
|
||||||
mdiPlaylistPlus,
|
mdiPlaylistPlus,
|
||||||
|
@ -72,44 +72,44 @@
|
||||||
<div class="ctxmenu" use:melt={menu}>
|
<div class="ctxmenu" use:melt={menu}>
|
||||||
{#if !single}
|
{#if !single}
|
||||||
<div class="item item-sm item-primary">
|
<div class="item item-sm item-primary">
|
||||||
<span>{tracks.length} tracks</span>
|
<span>{$LL.n_tracks(tracks.length)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Queue -->
|
<!-- Queue -->
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiPlaylistPlay} size={1.4} />
|
<Icon path={mdiPlaylistPlay} size={1.4} />
|
||||||
<span>{m.play_next()}</span>
|
<span>{$LL.play_next()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiPlaylistPlus} size={1.4} />
|
<Icon path={mdiPlaylistPlus} size={1.4} />
|
||||||
<span>{m.add_to_queue()}</span>
|
<span>{$LL.add_to_queue()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Track details -->
|
<!-- Track details -->
|
||||||
{#if single}
|
{#if single}
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiAccountMusic} size={1.4} />
|
<Icon path={mdiAccountMusic} size={1.4} />
|
||||||
<span>{m.view_artist()}</span>
|
<span>{$LL.view_artist()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiAlbum} size={1.4} />
|
<Icon path={mdiAlbum} size={1.4} />
|
||||||
<span>{m.view_album()}</span>
|
<span>{$LL.view_album()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiText} size={1.4} />
|
<Icon path={mdiText} size={1.4} />
|
||||||
<span>{m.view_track_info()}</span>
|
<span>{$LL.view_track_info()}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- User library -->
|
<!-- User library -->
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiHeartOutline} size={1.4} />
|
<Icon path={mdiHeartOutline} size={1.4} />
|
||||||
<span>{m.library_add()}</span>
|
<span>{$LL.library_add()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={$playlistMenuTrigger}>
|
<div class="item" use:melt={$playlistMenuTrigger}>
|
||||||
<Icon path={mdiPlaylistMusic} size={1.4} />
|
<Icon path={mdiPlaylistMusic} size={1.4} />
|
||||||
<span>{m.playlist_add()}</span>
|
<span>{$LL.playlist_add()}</span>
|
||||||
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -117,11 +117,11 @@
|
||||||
{#if playlistIndex !== undefined}
|
{#if playlistIndex !== undefined}
|
||||||
<div class="item sep" use:melt={item}>
|
<div class="item sep" use:melt={item}>
|
||||||
<Icon path={mdiTrashCan} size={1.4} />
|
<Icon path={mdiTrashCan} size={1.4} />
|
||||||
<span>{m.playlist_remove()}</span>
|
<span>{$LL.playlist_remove()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={$moveMenuTrigger}>
|
<div class="item" use:melt={$moveMenuTrigger}>
|
||||||
<Icon path={mdiSwapVertical} size={1.4} />
|
<Icon path={mdiSwapVertical} size={1.4} />
|
||||||
<span>{m.move()}</span>
|
<span>{$LL.move()}</span>
|
||||||
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
<!-- Share -->
|
<!-- Share -->
|
||||||
<div class="item sep" use:melt={$shareMenuTrigger}>
|
<div class="item sep" use:melt={$shareMenuTrigger}>
|
||||||
<Icon path={mdiShare} size={1.4} />
|
<Icon path={mdiShare} size={1.4} />
|
||||||
<span>{m.share()}</span>
|
<span>{$LL.share()}</span>
|
||||||
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -150,11 +150,11 @@
|
||||||
<div class="ctxmenu" use:melt={$shareMenu}>
|
<div class="ctxmenu" use:melt={$shareMenu}>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiContentCopy} size={1.4} />
|
<Icon path={mdiContentCopy} size={1.4} />
|
||||||
<span>{single ? m.copy_url() : m.copy_urls()}</span>
|
<span>{single ? $LL.copy_url() : $LL.copy_urls()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" use:melt={item}>
|
<div class="item" use:melt={item}>
|
||||||
<Icon path={mdiDownload} size={1.4} />
|
<Icon path={mdiDownload} size={1.4} />
|
||||||
<span>{single ? m.download_audio_file() : m.download_audio_files()}</span>
|
<span>{single ? $LL.download_audio_file() : $LL.download_audio_files()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- Fixed header that is shown when scrolling down -->
|
<!-- Fixed header that is shown when scrolling down -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { beforeNavigate } from "$app/navigation";
|
import { beforeNavigate } from "$app/navigation";
|
||||||
import { mdiBellOutline, mdiClose, mdiHistory, mdiPlay } from "@mdi/js";
|
import { mdiBellOutline, mdiClose, mdiHistory, mdiPlay } from "@mdi/js";
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
} else if (t) {
|
} else if (t) {
|
||||||
title = t;
|
title = t;
|
||||||
} else {
|
} else {
|
||||||
title = m.app_name();
|
title = $LL.app_name();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: searchCtx = $page.data.header?.searchCtx;
|
$: searchCtx = $page.data.header?.searchCtx;
|
||||||
|
@ -55,11 +55,11 @@
|
||||||
)
|
)
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
let bgcolor = "hsl(var(--b2)";
|
let bgcolor = "oklch(var(--b2)";
|
||||||
$: if ($headerColor) {
|
$: if ($headerColor) {
|
||||||
bgcolor = `rgb(${$headerColor.r}, ${$headerColor.g}, ${$headerColor.b}, ${opacity})`;
|
bgcolor = `rgb(${$headerColor.r}, ${$headerColor.g}, ${$headerColor.b}, ${opacity})`;
|
||||||
} else {
|
} else {
|
||||||
bgcolor = `hsl(var(--b2) / ${opacity})`;
|
bgcolor = `oklch(var(--b2) / ${opacity})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputElm: HTMLInputElement;
|
let inputElm: HTMLInputElement;
|
||||||
|
@ -68,8 +68,8 @@
|
||||||
|
|
||||||
let searchLabel: string;
|
let searchLabel: string;
|
||||||
$: if (searchCtx) {
|
$: if (searchCtx) {
|
||||||
if (searchCtx === SearchCtx.Global) searchLabel = m.search();
|
if (searchCtx === SearchCtx.Global) searchLabel = $LL.search();
|
||||||
else searchLabel = m.search_item({ item: title });
|
else searchLabel = $LL.search_item(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchOnBlur(e: FocusEvent) {
|
function searchOnBlur(e: FocusEvent) {
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
{#if $page.data.header?.playLink}
|
{#if $page.data.header?.playLink}
|
||||||
<IconButton
|
<IconButton
|
||||||
path={mdiPlay}
|
path={mdiPlay}
|
||||||
ariaLabel={m.play_item({ title })}
|
ariaLabel={$LL.play_item(title)}
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => alert("Playing " + title)}
|
on:click={() => alert("Playing " + title)}
|
||||||
/>
|
/>
|
||||||
|
@ -135,13 +135,13 @@
|
||||||
|
|
||||||
<div class="flex gap-1 items-center">
|
<div class="flex gap-1 items-center">
|
||||||
{#if homeScreen}
|
{#if homeScreen}
|
||||||
<IconButton path={mdiBellOutline} ariaLabel={m.notifications()} />
|
<IconButton path={mdiBellOutline} ariaLabel={$LL.notifications()} />
|
||||||
<IconButton path={mdiHistory} ariaLabel={m.playback_history()} />
|
<IconButton path={mdiHistory} ariaLabel={$LL.playback_history()} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if searchCtx}
|
{#if searchCtx}
|
||||||
<IconButton
|
<IconButton
|
||||||
path={searchShow ? mdiClose : iconSearch}
|
path={searchShow ? mdiClose : iconSearch}
|
||||||
ariaLabel={searchShow ? m.clear_search() : searchLabel}
|
ariaLabel={searchShow ? $LL.clear_search() : searchLabel}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (searchShow) {
|
if (searchShow) {
|
||||||
clearSearch();
|
clearSearch();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { melt, createDropdownMenu } from "@tirayamusic/melt-ui";
|
import { melt, createDropdownMenu } from "@tirayamusic/melt-ui";
|
||||||
|
|
||||||
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
|
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-circle btn-sm avatar"
|
class="btn btn-ghost btn-circle btn-sm avatar"
|
||||||
aria-label={m.options()}
|
aria-label={$LL.options()}
|
||||||
use:melt={$trigger}
|
use:melt={$trigger}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
languageTag,
|
|
||||||
onSetLanguageTag,
|
|
||||||
type AvailableLanguageTag,
|
|
||||||
setLanguageTag,
|
|
||||||
} from "$paraglide/runtime";
|
|
||||||
|
|
||||||
// initialize the language tag
|
|
||||||
let _languageTag: AvailableLanguageTag = languageTag();
|
|
||||||
|
|
||||||
onSetLanguageTag((newLanguageTag) => {
|
|
||||||
_languageTag = newLanguageTag;
|
|
||||||
document.documentElement.lang = newLanguageTag;
|
|
||||||
});
|
|
||||||
|
|
||||||
setLanguageTag("de");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key _languageTag}
|
|
||||||
<slot />
|
|
||||||
{/key}
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
||||||
import { mdiClockOutline } from "@mdi/js";
|
import { mdiClockOutline } from "@mdi/js";
|
||||||
import { generateId, kbd } from "@tirayamusic/melt-ui/internal/helpers";
|
import { generateId, kbd } from "@tirayamusic/melt-ui/internal/helpers";
|
||||||
|
@ -421,7 +421,7 @@
|
||||||
frame = requestAnimationFrame(poll);
|
frame = requestAnimationFrame(poll);
|
||||||
});
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
cancelAnimationFrame(frame);
|
if (frame) cancelAnimationFrame(frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
// DRAG AND DROP
|
// DRAG AND DROP
|
||||||
|
@ -691,35 +691,35 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col first" role="columnheader" aria-colindex={2}>
|
<div class="col first" role="columnheader" aria-colindex={2}>
|
||||||
<SortTitle bind:sorting col={Filters.TITLE} col2="artist">
|
<SortTitle bind:sorting col={Filters.TITLE} col2="artist">
|
||||||
<span class="ellipsis-ol">{m.title()}</span>
|
<span class="ellipsis-ol">{$LL.title()}</span>
|
||||||
<span slot="t2" class="ellipsis-ol">Artist</span>
|
<span slot="t2" class="ellipsis-ol">Artist</span>
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
{#if view > ListView.Album}
|
{#if view > ListView.Album}
|
||||||
<div class="col var1" role="columnheader" aria-colindex={3}>
|
<div class="col var1" role="columnheader" aria-colindex={3}>
|
||||||
<SortTitle bind:sorting col={Filters.ALBUM}>
|
<SortTitle bind:sorting col={Filters.ALBUM}>
|
||||||
<span class="ellipsis-ol">{m.album()}</span>
|
<span class="ellipsis-ol">{$LL.album()}</span>
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if view > ListView.Album}
|
{#if view > ListView.Album}
|
||||||
<div class="col var2" role="columnheader" aria-colindex={4}>
|
<div class="col var2" role="columnheader" aria-colindex={4}>
|
||||||
<SortTitle bind:sorting col={Filters.RELEASE_DATE}>
|
<SortTitle bind:sorting col={Filters.RELEASE_DATE}>
|
||||||
<span class="ellipsis-ol">{m.release_date()}</span>
|
<span class="ellipsis-ol">{$LL.release_date()}</span>
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if view === ListView.Playlist}
|
{#if view === ListView.Playlist}
|
||||||
<div class="col var3" role="columnheader" aria-colindex={5}>
|
<div class="col var3" role="columnheader" aria-colindex={5}>
|
||||||
<SortTitle bind:sorting col={Filters.ADDED_DATE}>
|
<SortTitle bind:sorting col={Filters.ADDED_DATE}>
|
||||||
<span class="ellipsis-ol">{m.date_added()}</span>
|
<span class="ellipsis-ol">{$LL.date_added()}</span>
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showAuthors}
|
{#if showAuthors}
|
||||||
<div class="col var4" role="columnheader" aria-colindex={6}>
|
<div class="col var4" role="columnheader" aria-colindex={6}>
|
||||||
<SortTitle bind:sorting col={Filters.ADDED_BY}>
|
<SortTitle bind:sorting col={Filters.ADDED_BY}>
|
||||||
<span class="ellipsis-ol">{m.added_by()}</span>
|
<span class="ellipsis-ol">{$LL.added_by()}</span>
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -729,7 +729,7 @@
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
aria-colindex={5}
|
aria-colindex={5}
|
||||||
>
|
>
|
||||||
<SortTitle bind:sorting col={Filters.DURATION} ariaLabel={m.duration()} end>
|
<SortTitle bind:sorting col={Filters.DURATION} ariaLabel={$LL.duration()} end>
|
||||||
<Icon path={mdiClockOutline} />
|
<Icon path={mdiClockOutline} />
|
||||||
</SortTitle>
|
</SortTitle>
|
||||||
</div>
|
</div>
|
||||||
|
@ -737,7 +737,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if filteredTracks.length === 0}
|
{#if filteredTracks.length === 0}
|
||||||
<NoItemsMsg itemType="tracks" search={Boolean(searchTerm)} />
|
<NoItemsMsg itemType="track" search={Boolean(searchTerm)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { locale } from "$i18n/i18n-svelte";
|
||||||
import { mdiPlay, mdiDotsVertical } from "@mdi/js";
|
import { mdiPlay, mdiDotsVertical } from "@mdi/js";
|
||||||
import { createContextMenu, createDropdownMenu, melt } from "@tirayamusic/melt-ui";
|
import { createContextMenu, createDropdownMenu, melt } from "@tirayamusic/melt-ui";
|
||||||
|
|
||||||
|
@ -136,7 +137,7 @@
|
||||||
<div class="col var2" role="gridcell" aria-colindex={4}>
|
<div class="col var2" role="gridcell" aria-colindex={4}>
|
||||||
{#if track.item.album.releaseDate}
|
{#if track.item.album.releaseDate}
|
||||||
<span class="ellipsis-ol">
|
<span class="ellipsis-ol">
|
||||||
{formatDateStr(track.item.album.releaseDate)}
|
{formatDateStr(track.item.album.releaseDate, $locale)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -146,7 +147,7 @@
|
||||||
<div class="col var3" role="gridcell" aria-colindex={5}>
|
<div class="col var3" role="gridcell" aria-colindex={5}>
|
||||||
{#if track.item.addedDate}
|
{#if track.item.addedDate}
|
||||||
<span class="ellipsis-ol">
|
<span class="ellipsis-ol">
|
||||||
{formatDate(track.item.addedDate)}
|
{formatDate(track.item.addedDate, $locale)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
@apply bg-base-100;
|
@apply bg-base-100;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
border-bottom: 1px solid hsl(var(--bc) / 0.4);
|
border-bottom: 1px solid oklch(var(--bc) / 0.4);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiAccountMusic,
|
mdiAccountMusic,
|
||||||
mdiAccountMusicOutline,
|
mdiAccountMusicOutline,
|
||||||
|
@ -25,31 +25,31 @@
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<ul class="menu menu-compact flex flex-col p-1">
|
<ul class="menu menu-compact flex flex-col p-1">
|
||||||
<NavbarItemLarge
|
<NavbarItemLarge
|
||||||
title={m.home()}
|
title={$LL.home()}
|
||||||
icon={mdiHomeOutline}
|
icon={mdiHomeOutline}
|
||||||
iconActive={mdiHome}
|
iconActive={mdiHome}
|
||||||
href="/"
|
href="/"
|
||||||
/>
|
/>
|
||||||
<NavbarItemLarge
|
<NavbarItemLarge
|
||||||
title={m.search()}
|
title={$LL.search()}
|
||||||
icon={iconSearch}
|
icon={iconSearch}
|
||||||
iconActive={iconSearchFilled}
|
iconActive={iconSearchFilled}
|
||||||
href="/search"
|
href="/search"
|
||||||
/>
|
/>
|
||||||
<NavbarItemLarge
|
<NavbarItemLarge
|
||||||
title={m.artists()}
|
title={$LL.artists()}
|
||||||
icon={mdiAccountMusicOutline}
|
icon={mdiAccountMusicOutline}
|
||||||
iconActive={mdiAccountMusic}
|
iconActive={mdiAccountMusic}
|
||||||
href="/artist"
|
href="/artist"
|
||||||
/>
|
/>
|
||||||
<NavbarItemLarge
|
<NavbarItemLarge
|
||||||
title={m.albums()}
|
title={$LL.albums()}
|
||||||
icon={iconAlbumOutline}
|
icon={iconAlbumOutline}
|
||||||
iconActive={mdiAlbum}
|
iconActive={mdiAlbum}
|
||||||
href="/album"
|
href="/album"
|
||||||
/>
|
/>
|
||||||
<NavbarItemLarge
|
<NavbarItemLarge
|
||||||
title={m.tracks()}
|
title={$LL.tracks()}
|
||||||
icon={mdiMusicNoteOutline}
|
icon={mdiMusicNoteOutline}
|
||||||
iconActive={mdiMusicNote}
|
iconActive={mdiMusicNote}
|
||||||
href="/tracks"
|
href="/tracks"
|
||||||
|
@ -60,9 +60,9 @@
|
||||||
<a
|
<a
|
||||||
href="/playlist"
|
href="/playlist"
|
||||||
class="flex-1 text-sm text-base-content text-opacity-40 font-semibold"
|
class="flex-1 text-sm text-base-content text-opacity-40 font-semibold"
|
||||||
>{m.playlists()}</a
|
>{$LL.playlists()}</a
|
||||||
>
|
>
|
||||||
<IconButton path={mdiPlus} size="xs" ariaLabel={m.playlist_new()} />
|
<IconButton path={mdiPlus} size="xs" ariaLabel={$LL.playlist_new()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-1 min-h-0 mb-2">
|
<div class="flex flex-1 min-h-0 mb-2">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiAccountMusic,
|
mdiAccountMusic,
|
||||||
mdiAccountMusicOutline,
|
mdiAccountMusicOutline,
|
||||||
|
@ -16,27 +16,32 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="btm-nav btm-nav-sm bg-base-300 z-50">
|
<div class="btm-nav btm-nav-sm bg-base-300 z-50">
|
||||||
<NavbarMobileItem title="Home" icon={mdiHomeOutline} iconActive={mdiHome} href="/" />
|
|
||||||
<NavbarMobileItem
|
<NavbarMobileItem
|
||||||
title={m.search()}
|
title={$LL.home()}
|
||||||
|
icon={mdiHomeOutline}
|
||||||
|
iconActive={mdiHome}
|
||||||
|
href="/"
|
||||||
|
/>
|
||||||
|
<NavbarMobileItem
|
||||||
|
title={$LL.search()}
|
||||||
icon={iconSearch}
|
icon={iconSearch}
|
||||||
iconActive={iconSearchFilled}
|
iconActive={iconSearchFilled}
|
||||||
href="/search"
|
href="/search"
|
||||||
/>
|
/>
|
||||||
<NavbarMobileItem
|
<NavbarMobileItem
|
||||||
title={m.artists()}
|
title={$LL.artists()}
|
||||||
icon={mdiAccountMusicOutline}
|
icon={mdiAccountMusicOutline}
|
||||||
iconActive={mdiAccountMusic}
|
iconActive={mdiAccountMusic}
|
||||||
href="/artist"
|
href="/artist"
|
||||||
/>
|
/>
|
||||||
<NavbarMobileItem
|
<NavbarMobileItem
|
||||||
title={m.albums()}
|
title={$LL.albums()}
|
||||||
icon={iconAlbumOutline}
|
icon={iconAlbumOutline}
|
||||||
iconActive={mdiAlbum}
|
iconActive={mdiAlbum}
|
||||||
href="/album"
|
href="/album"
|
||||||
/>
|
/>
|
||||||
<NavbarMobileItem
|
<NavbarMobileItem
|
||||||
title={m.playlists()}
|
title={$LL.playlists()}
|
||||||
icon={mdiPlaylistMusicOutline}
|
icon={mdiPlaylistMusicOutline}
|
||||||
iconActive={mdiPlaylistMusic}
|
iconActive={mdiPlaylistMusic}
|
||||||
href="/playlist"
|
href="/playlist"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
|
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
|
||||||
|
|
||||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
import IconButton from "$lib/components/ui/IconButton.svelte";
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
size={isLarge ? "sm" : "xs"}
|
size={isLarge ? "sm" : "xs"}
|
||||||
color="default"
|
color="default"
|
||||||
cls="absolute top-0 right-0 opacity-0 hover:opacity-100 transition-opacity"
|
cls="absolute top-0 right-0 opacity-0 hover:opacity-100 transition-opacity"
|
||||||
ariaLabel={isLarge ? m.cover_show_small() : m.m_cover_show_large()}
|
ariaLabel={isLarge ? $LL.cover_show_small() : $LL.m_cover_show_large()}
|
||||||
on:click={toggleCurrentCoverLarge}
|
on:click={toggleCurrentCoverLarge}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiSkipPrevious,
|
mdiSkipPrevious,
|
||||||
|
@ -48,7 +48,10 @@
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-start"
|
class="flex items-center justify-start"
|
||||||
aria-label={m.now_playing({ title: "Across the Sea", artist: "Leaves' Eyes" })}
|
aria-label={$LL.now_playing({
|
||||||
|
title: "Across the Sea",
|
||||||
|
artist: "Leaves' Eyes",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<CurrentCover />
|
<CurrentCover />
|
||||||
<TrackName {track} />
|
<TrackName {track} />
|
||||||
|
@ -61,23 +64,23 @@
|
||||||
<div class="flex w-full space-x-2 justify-center items-center">
|
<div class="flex w-full space-x-2 justify-center items-center">
|
||||||
<IconToggle
|
<IconToggle
|
||||||
iconOn={mdiShuffle}
|
iconOn={mdiShuffle}
|
||||||
ariaLabelOn={m.shuffle_enable()}
|
ariaLabelOn={$LL.shuffle_enable()}
|
||||||
ariaLabelOff={m.shuffle_disable()}
|
ariaLabelOff={$LL.shuffle_disable()}
|
||||||
/>
|
/>
|
||||||
<IconButton path={mdiSkipPrevious} size="md" ariaLabel={m.previous()} />
|
<IconButton path={mdiSkipPrevious} size="md" ariaLabel={$LL.previous()} />
|
||||||
<IconToggle
|
<IconToggle
|
||||||
iconOn={mdiPause}
|
iconOn={mdiPause}
|
||||||
iconOff={mdiPlay}
|
iconOff={mdiPlay}
|
||||||
size="md"
|
size="md"
|
||||||
color="primary"
|
color="primary"
|
||||||
iColorOn=""
|
iColorOn=""
|
||||||
ariaLabelOn={m.play()}
|
ariaLabelOn={$LL.play()}
|
||||||
ariaLabelOff={m.pause()}
|
ariaLabelOff={$LL.pause()}
|
||||||
/>
|
/>
|
||||||
<IconButton path={mdiSkipNext} size="md" ariaLabel={m.next()} />
|
<IconButton path={mdiSkipNext} size="md" ariaLabel={$LL.next()} />
|
||||||
<IconToggleMulti
|
<IconToggleMulti
|
||||||
icons={[mdiRepeat, mdiRepeat, mdiRepeatOnce]}
|
icons={[mdiRepeat, mdiRepeat, mdiRepeatOnce]}
|
||||||
ariaLabels={[m.repeat_disable(), m.repeat_queue(), m.repeat_track()]}
|
ariaLabels={[$LL.repeat_disable(), $LL.repeat_queue(), $LL.repeat_track()]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -91,7 +94,7 @@
|
||||||
max={length}
|
max={length}
|
||||||
bind:value={position}
|
bind:value={position}
|
||||||
bind:displayValue={displayPosition}
|
bind:displayValue={displayPosition}
|
||||||
ariaLabel={m.seek_bar()}
|
ariaLabel={$LL.seek_bar()}
|
||||||
/>
|
/>
|
||||||
<span class="w-12">{formatDuration(length)}</span>
|
<span class="w-12">{formatDuration(length)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,15 +106,15 @@
|
||||||
<IconToggle
|
<IconToggle
|
||||||
iconOn={mdiPlaylistPlay}
|
iconOn={mdiPlaylistPlay}
|
||||||
value={$clientState.showQueue}
|
value={$clientState.showQueue}
|
||||||
ariaLabelOn={m.queue_show()}
|
ariaLabelOn={$LL.queue_show()}
|
||||||
ariaLabelOff={m.queue_hide()}
|
ariaLabelOff={$LL.queue_hide()}
|
||||||
on:change={toggleShowQueue}
|
on:change={toggleShowQueue}
|
||||||
/>
|
/>
|
||||||
<VolumeControl />
|
<VolumeControl />
|
||||||
<IconButton path={mdiFullscreen} ariaLabel={m.fullscreen_player()} />
|
<IconButton path={mdiFullscreen} ariaLabel={$LL.fullscreen_player()} />
|
||||||
<IconButtonTrigger
|
<IconButtonTrigger
|
||||||
path={mdiDotsVertical}
|
path={mdiDotsVertical}
|
||||||
ariaLabel={m.track_options()}
|
ariaLabel={$LL.track_options()}
|
||||||
trigger={$trigger}
|
trigger={$trigger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import {
|
import {
|
||||||
mdiFullscreen,
|
mdiFullscreen,
|
||||||
mdiPause,
|
mdiPause,
|
||||||
|
@ -75,10 +75,10 @@
|
||||||
class="shadow rounded-md z-50"
|
class="shadow rounded-md z-50"
|
||||||
class:transition-colors={colorTrans}
|
class:transition-colors={colorTrans}
|
||||||
style={`background-color: ${bgColorHex}`}
|
style={`background-color: ${bgColorHex}`}
|
||||||
aria-label={m.now_playing({ title: "Across the Sea", artist: "Leaves' Eyes" })}
|
aria-label={$LL.now_playing({ title: "Across the Sea", artist: "Leaves' Eyes" })}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<button class="relative" aria-label={m.fullscreen_player()}>
|
<button class="relative" aria-label={$LL.fullscreen_player()}>
|
||||||
<div
|
<div
|
||||||
class="absolute w-full h-full flex items-center justify-center bg-base-100/50
|
class="absolute w-full h-full flex items-center justify-center bg-base-100/50
|
||||||
transition-opacity opacity-0 hover:opacity-100"
|
transition-opacity opacity-0 hover:opacity-100"
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
<IconButton
|
<IconButton
|
||||||
path={mdiSkipPrevious}
|
path={mdiSkipPrevious}
|
||||||
size="md"
|
size="md"
|
||||||
ariaLabel={m.previous()}
|
ariaLabel={$LL.previous()}
|
||||||
on:click={prevTrack}
|
on:click={prevTrack}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -122,14 +122,14 @@
|
||||||
iconOff={mdiPlay}
|
iconOff={mdiPlay}
|
||||||
size="md"
|
size="md"
|
||||||
iColorOn=""
|
iColorOn=""
|
||||||
ariaLabelOff={m.pause()}
|
ariaLabelOff={$LL.pause()}
|
||||||
ariaLabelOn={m.play()}
|
ariaLabelOn={$LL.play()}
|
||||||
/>
|
/>
|
||||||
{#if !$mainPhone}
|
{#if !$mainPhone}
|
||||||
<IconButton
|
<IconButton
|
||||||
path={mdiSkipNext}
|
path={mdiSkipNext}
|
||||||
size="md"
|
size="md"
|
||||||
ariaLabel={m.next()}
|
ariaLabel={$LL.next()}
|
||||||
on:click={nextTrack}
|
on:click={nextTrack}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="seekbar" bind:this={seekbarElm}>
|
<div class="seekbar" bind:this={seekbarElm}>
|
||||||
<Slider ariaLabel={m.seek_bar()} />
|
<Slider ariaLabel={$LL.seek_bar()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiVolumeHigh, mdiVolumeMedium, mdiVolumeLow, mdiVolumeMute } from "@mdi/js";
|
import { mdiVolumeHigh, mdiVolumeMedium, mdiVolumeLow, mdiVolumeMute } from "@mdi/js";
|
||||||
|
|
||||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
import IconButton from "$lib/components/ui/IconButton.svelte";
|
||||||
|
@ -40,14 +40,14 @@
|
||||||
<div class="flex items-center" on:wheel|passive={onScroll}>
|
<div class="flex items-center" on:wheel|passive={onScroll}>
|
||||||
<IconButton
|
<IconButton
|
||||||
path={icon}
|
path={icon}
|
||||||
ariaLabel={mute ? m.unmute() : m.mute()}
|
ariaLabel={mute ? $LL.unmute() : $LL.mute()}
|
||||||
on:click={toggleMute}
|
on:click={toggleMute}
|
||||||
/>
|
/>
|
||||||
<div class="inline-block w-20">
|
<div class="inline-block w-20">
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
ariaLabel={m.volume_control()}
|
ariaLabel={$LL.volume_control()}
|
||||||
value={mute ? 0 : volume}
|
value={mute ? 0 : volume}
|
||||||
on:change={updateVolume}
|
on:change={updateVolume}
|
||||||
liveUpdate
|
liveUpdate
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiCalendar, mdiOrderAlphabeticalAscending } from "@mdi/js";
|
import { mdiCalendar, mdiOrderAlphabeticalAscending } from "@mdi/js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -94,21 +94,21 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap gap-2 justify-between sticky-header py-2 z-20">
|
<div class="flex flex-wrap gap-2 justify-between sticky-header py-2 z-20">
|
||||||
<FilterButtons
|
<FilterButtons
|
||||||
items={[`${m.albums()} (${nAlbums})`, `${m.singles()} (${nSingles})`]}
|
items={[`${$LL.albums()} (${nAlbums})`, `${$LL.singles()} (${nSingles})`]}
|
||||||
mask={[nAlbums === 0, nSingles === 0]}
|
mask={[nAlbums === 0, nSingles === 0]}
|
||||||
toggle
|
toggle
|
||||||
bind:value={albumFilter}
|
bind:value={albumFilter}
|
||||||
/>
|
/>
|
||||||
<FilterButtons
|
<FilterButtons
|
||||||
items={[mdiCalendar, mdiOrderAlphabeticalAscending]}
|
items={[mdiCalendar, mdiOrderAlphabeticalAscending]}
|
||||||
ariaLabels={[m.sort_date(), m.sort_name()]}
|
ariaLabels={[$LL.sort_date(), $LL.sort_name()]}
|
||||||
bind:value={albumSort}
|
bind:value={albumSort}
|
||||||
icons
|
icons
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if searchTerm && filteredAlbums.length === 0}
|
{#if searchTerm && filteredAlbums.length === 0}
|
||||||
<EmptySearchNote itemType="albums" search={Boolean(searchTerm)} />
|
<EmptySearchNote itemType="album" search={Boolean(searchTerm)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Grid totalItems={filteredAlbums.length} bind:startIndex bind:endIndex>
|
<Grid totalItems={filteredAlbums.length} bind:startIndex bind:endIndex>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
frame = requestAnimationFrame(poll);
|
frame = requestAnimationFrame(poll);
|
||||||
});
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
cancelAnimationFrame(frame);
|
if (frame) cancelAnimationFrame(frame);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- Tile with a large image used to feature albums or playlists -->
|
<!-- Tile with a large image used to feature albums or playlists -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { mdiPlay } from "@mdi/js";
|
import { mdiPlay } from "@mdi/js";
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<button
|
<button
|
||||||
class="btn btn-circle btn-primary absolute bottom-2 right-2 z-10"
|
class="btn btn-circle btn-primary absolute bottom-2 right-2 z-10"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-label={m.play_item({ title })}
|
aria-label={$LL.play_item(title)}
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
dispatch("play");
|
dispatch("play");
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { mdiHeart, mdiHeartOutline } from "@mdi/js";
|
import { mdiHeart, mdiHeartOutline } from "@mdi/js";
|
||||||
|
|
||||||
import IconToggle from "./IconToggle.svelte";
|
import IconToggle from "./IconToggle.svelte";
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
iconOn={mdiHeart}
|
iconOn={mdiHeart}
|
||||||
iconOff={mdiHeartOutline}
|
iconOff={mdiHeartOutline}
|
||||||
{size}
|
{size}
|
||||||
ariaLabelOn={m.library_add()}
|
ariaLabelOn={$LL.library_add()}
|
||||||
ariaLabelOff={m.library_remove()}
|
ariaLabelOff={$LL.library_remove()}
|
||||||
bind:value
|
bind:value
|
||||||
/>
|
/>
|
||||||
|
|
105
src/lib/components/ui/LoadingAnimation.svelte
Normal file
105
src/lib/components/ui/LoadingAnimation.svelte
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#a"
|
||||||
|
id="b"
|
||||||
|
x1="75.381"
|
||||||
|
x2="56.756"
|
||||||
|
y1="72.556"
|
||||||
|
y2="62.604"
|
||||||
|
gradientTransform="rotate(120 159.185 84.508)scale(2.90865)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
/>
|
||||||
|
<linearGradient id="a">
|
||||||
|
<stop offset="0" style="stop-color:#16b5ab;stop-opacity:1" />
|
||||||
|
<stop offset="1" style="stop-color:#00eaff;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#a"
|
||||||
|
id="c"
|
||||||
|
x1="75.381"
|
||||||
|
x2="56.756"
|
||||||
|
y1="72.556"
|
||||||
|
y2="62.604"
|
||||||
|
gradientTransform="rotate(-120 74.447 162.819)scale(2.90865)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#a"
|
||||||
|
id="d"
|
||||||
|
x1="75.381"
|
||||||
|
x2="56.756"
|
||||||
|
y1="72.556"
|
||||||
|
y2="62.604"
|
||||||
|
gradientTransform="matrix(2.90865 0 0 2.90865 -135.58 -146.795)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
d="m 25.988281,17.615234 a 6.3502348,6.3502348 0 0 0 -9.52539,5.5 V 84.75 A 6.3495998,6.3495998 0 0 0 22.8125,91.099609 6.3495998,6.3495998 0 0 0 29.162109,84.75 V 34.111328 l 21.611328,12.478516 a 6.3495998,6.3495998 0 0 0 8.673829,-2.324219 6.3495998,6.3495998 0 0 0 -2.324219,-8.673828 z"
|
||||||
|
class="border"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 30.591797,4.234375 a 6.3495998,6.3495998 0 0 0 -3.855469,2.9589844 6.3495998,6.3495998 0 0 0 2.324219,8.6718746 L 72.914062,41.185547 51.302734,53.662109 a 6.3495998,6.3495998 0 0 0 -2.324218,8.673828 6.3495998,6.3495998 0 0 0 8.673828,2.324219 L 88.787109,46.683594 a 6.3502348,6.3502348 0 0 0 0,-10.998047 L 35.410156,4.8691406 A 6.3495998,6.3495998 0 0 0 30.591797,4.234375 Z"
|
||||||
|
class="border"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 38.623047,44.208984 a 6.3495998,6.3495998 0 0 0 -6.34961,6.34961 v 35.951172 a 6.3502348,6.3502348 0 0 0 9.523438,5.5 l 53.378906,-30.81836 a 6.3495998,6.3495998 0 0 0 2.322266,-8.673828 6.3495998,6.3495998 0 0 0 -8.671875,-2.324219 L 44.972656,75.511719 V 50.558594 a 6.3495998,6.3495998 0 0 0 -6.349609,-6.34961 z"
|
||||||
|
class="border"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:url(#b);stroke-width:12.6992;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 22.813222,84.749657 5e-6,-61.634538 31.13496,17.975773"
|
||||||
|
class="anim"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:url(#c);stroke-width:12.6992;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="M 32.235584,10.367238 85.612628,41.184521 54.477692,59.160292"
|
||||||
|
class="anim"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:url(#d);stroke-width:12.6992;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="M 92.000037,55.693052 38.622986,86.510304 v -35.95157"
|
||||||
|
class="anim"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
/** Animation code from https://svgartista.net/ */
|
||||||
|
@keyframes logo-loading {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 99.58604431152344px;
|
||||||
|
stroke-dasharray: 99.58604431152344px;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-dasharray: 99.58604431152344px;
|
||||||
|
}
|
||||||
|
|
||||||
|
50.001% {
|
||||||
|
stroke-dashoffset: 199.17218017578125px;
|
||||||
|
stroke-dasharray: 99.58604431152344px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 99.58604431152344px;
|
||||||
|
stroke-dasharray: 99.58604431152344px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
stroke-width: 1;
|
||||||
|
@apply stroke-base-content/30 fill-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anim {
|
||||||
|
animation: logo-loading 2s ease 0s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -1,6 +1,6 @@
|
||||||
<!-- Message shown to the user if a list or grid contains no items -->
|
<!-- Message shown to the user if a list or grid contains no items -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { clearSearch } from "$lib/util/functions";
|
import { clearSearch } from "$lib/util/functions";
|
||||||
import { randomKaomoji } from "$lib/util/kaomoji";
|
import { randomKaomoji } from "$lib/util/kaomoji";
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@
|
||||||
<p class="text-3xl text-base-content text-opacity-60 mb-2" aria-hidden="true">
|
<p class="text-3xl text-base-content text-opacity-60 mb-2" aria-hidden="true">
|
||||||
{randomKaomoji(404)}
|
{randomKaomoji(404)}
|
||||||
</p>
|
</p>
|
||||||
<p>{m.search_no_result({ itemType })}</p>
|
<p>{$LL.search_no_result(itemType)}</p>
|
||||||
<button class="btn btn-outline btn-sm mt-2" on:click={clearSearch}
|
<button class="btn btn-outline btn-sm mt-2" on:click={clearSearch}
|
||||||
>{m.clear_search()}</button
|
>{$LL.clear_search()}</button
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<p>{m.empty_list({ itemType })}</p>
|
<p>{$LL.empty_list(itemType)}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { parseHslString } from "$lib/util/colors";
|
import { parseOklchString } from "$lib/util/colors";
|
||||||
import { WHITE } from "$lib/util/constants";
|
import { WHITE } from "$lib/util/constants";
|
||||||
import { getCssVariable } from "$lib/util/functions";
|
import { getCssVariable } from "$lib/util/functions";
|
||||||
import type { Color } from "$lib/util/types";
|
import type { Color } from "$lib/util/types";
|
||||||
|
@ -6,7 +6,7 @@ import { writable } from "svelte/store";
|
||||||
|
|
||||||
function getTextColor(): Color {
|
function getTextColor(): Color {
|
||||||
const colorStr = getCssVariable("--bc");
|
const colorStr = getCssVariable("--bc");
|
||||||
const parsed = parseHslString(colorStr);
|
const parsed = parseOklchString(colorStr);
|
||||||
return parsed || WHITE;
|
return parsed || WHITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { correctBgColor } from "./colors";
|
import { correctBgColor, oklchToRgb, colorToHex } from "./colors";
|
||||||
import { WHITE } from "./constants";
|
import { WHITE } from "./constants";
|
||||||
import type { Color } from "./types";
|
import type { Color } from "./types";
|
||||||
|
|
||||||
|
@ -10,3 +10,10 @@ describe("correctBgColor", () => {
|
||||||
expect(correctBgColor(WHITE, YELLOW, 5)).toEqual({ r: 114, g: 114, b: 57 });
|
expect(correctBgColor(WHITE, YELLOW, 5)).toEqual({ r: 114, g: 114, b: 57 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ l: 0.7, c: 0.1, h: 141, rgb: "#7bae72" },
|
||||||
|
{ l: 0.5294, c: 0.039, h: 230, rgb: "#54707e" },
|
||||||
|
])("oklchToRgb", ({ l, c, h, rgb }) => {
|
||||||
|
it("rgb", () => expect(colorToHex(oklchToRgb(l, c, h))).toBe(rgb));
|
||||||
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function colorToHex(c: Color): string {
|
||||||
* @param l - Lightness percentage (0-100)
|
* @param l - Lightness percentage (0-100)
|
||||||
* @returns color in RGB format
|
* @returns color in RGB format
|
||||||
*/
|
*/
|
||||||
function hslToRgb(h: number, s: number, l: number): Color {
|
export function hslToRgb(h: number, s: number, l: number): Color {
|
||||||
s /= 100;
|
s /= 100;
|
||||||
l /= 100;
|
l /= 100;
|
||||||
const k = (n: number) => (n + h / 30) % 12;
|
const k = (n: number) => (n + h / 30) % 12;
|
||||||
|
@ -29,6 +29,78 @@ function hslToRgb(h: number, s: number, l: number): Color {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a color from OKLCH to RGB
|
||||||
|
*
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch
|
||||||
|
*
|
||||||
|
* Conversion algorithm taken from https://github.com/Evercoder/culori
|
||||||
|
* @author Dan Burzo
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* @param l - Lightness (0-1)
|
||||||
|
* @param c - Chroma (0-1)
|
||||||
|
* @param h - Hue angle (0-1)
|
||||||
|
* @returns color in RGB format
|
||||||
|
*/
|
||||||
|
export function oklchToRgb(l: number, c: number, h: number): Color {
|
||||||
|
/* eslint-disable @typescript-eslint/no-loss-of-precision */
|
||||||
|
|
||||||
|
// Oklab
|
||||||
|
const lch = {
|
||||||
|
l,
|
||||||
|
a: c ? c * Math.cos((h / 180) * Math.PI) : 0,
|
||||||
|
b: c ? c * Math.sin((h / 180) * Math.PI) : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const L = Math.pow(
|
||||||
|
l * 0.99999999845051981432 +
|
||||||
|
0.39633779217376785678 * lch.a +
|
||||||
|
0.21580375806075880339 * lch.b,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const M = Math.pow(
|
||||||
|
l * 1.0000000088817607767 -
|
||||||
|
0.1055613423236563494 * lch.a -
|
||||||
|
0.063854174771705903402 * lch.b,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const S = Math.pow(
|
||||||
|
l * 1.0000000546724109177 -
|
||||||
|
0.089484182094965759684 * lch.a -
|
||||||
|
1.2914855378640917399 * lch.b,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
const lrgb = {
|
||||||
|
r: +4.076741661347994 * L - 3.307711590408193 * M + 0.230969928729428 * S,
|
||||||
|
g: -1.2684380040921763 * L + 2.6097574006633715 * M - 0.3413193963102197 * S,
|
||||||
|
b: -0.004196086541837188 * L - 0.7034186144594493 * M + 1.7076147009309444 * S,
|
||||||
|
};
|
||||||
|
|
||||||
|
// RGB
|
||||||
|
const rgbfn = (c: number) => {
|
||||||
|
const abs = Math.abs(c);
|
||||||
|
if (abs > 0.0031308) {
|
||||||
|
return (Math.sign(c) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055) * 255;
|
||||||
|
}
|
||||||
|
return c * 12.92 * 255;
|
||||||
|
};
|
||||||
|
/* eslint-enable @typescript-eslint/no-loss-of-precision */
|
||||||
|
return {
|
||||||
|
r: rgbfn(lrgb.r),
|
||||||
|
g: rgbfn(lrgb.g),
|
||||||
|
b: rgbfn(lrgb.b),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePercentageString(s: string): number {
|
||||||
|
if (s.endsWith("%")) {
|
||||||
|
return parseFloat(s.substring(0, s.length - 1)) / 100;
|
||||||
|
} else {
|
||||||
|
return parseFloat(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a string in HSL format (from CSS) and return its color.
|
* Parse a string in HSL format (from CSS) and return its color.
|
||||||
*
|
*
|
||||||
|
@ -39,12 +111,24 @@ export function parseHslString(hsl: string): Color | null {
|
||||||
if (parts.length !== 3) return null;
|
if (parts.length !== 3) return null;
|
||||||
|
|
||||||
const h = parseInt(parts[0]);
|
const h = parseInt(parts[0]);
|
||||||
const s = parseInt(parts[1]);
|
const s = parsePercentageString(parts[1]);
|
||||||
const l = parseInt(parts[2]);
|
const l = parsePercentageString(parts[2]);
|
||||||
if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
|
if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
|
||||||
return hslToRgb(h, s, l);
|
return hslToRgb(h, s, l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseOklchString(oklch: string): Color | null {
|
||||||
|
const parts = oklch.split(" ", 3);
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
|
||||||
|
const l = parsePercentageString(parts[0]);
|
||||||
|
const c = parseFloat(parts[1]);
|
||||||
|
const h = parseFloat(parts[2]);
|
||||||
|
|
||||||
|
if (isNaN(l) || isNaN(c) || isNaN(h)) return null;
|
||||||
|
return oklchToRgb(l, c, h);
|
||||||
|
}
|
||||||
|
|
||||||
export function fastAverageColor(
|
export function fastAverageColor(
|
||||||
src: FastAverageColorResource | null,
|
src: FastAverageColorResource | null,
|
||||||
callback: (res: Color | null) => void
|
callback: (res: Color | null) => void
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { Color } from "./types";
|
||||||
// Colors
|
// Colors
|
||||||
export const WHITE: Color = { r: 255, g: 255, b: 255 };
|
export const WHITE: Color = { r: 255, g: 255, b: 255 };
|
||||||
export const MIN_CONTRAST_TITLE = 4.5;
|
export const MIN_CONTRAST_TITLE = 4.5;
|
||||||
export const COLOR_B3 = "hsl(var(--b3))";
|
export const COLOR_B3 = "oklch(var(--b3))";
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
export const MIN_CONTRAST_BODY = 7;
|
export const MIN_CONTRAST_BODY = 7;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { Page } from "@sveltejs/kit";
|
import type { Page } from "@sveltejs/kit";
|
||||||
import { DATE_LANG } from "./constants";
|
|
||||||
import { CLEAR_SEARCH, hub } from "./events";
|
import { CLEAR_SEARCH, hub } from "./events";
|
||||||
import {
|
import {
|
||||||
LinkType,
|
LinkType,
|
||||||
|
@ -57,24 +56,24 @@ export function dateToNumber(date: string | null | undefined): number | null {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDateStr(date: string): string | null {
|
export function formatDateStr(date: string, locale: string): string | null {
|
||||||
const parsed = parseDate(date);
|
const parsed = parseDate(date);
|
||||||
if (parsed === null) return null;
|
if (parsed === null) return null;
|
||||||
if (parsed.month) {
|
if (parsed.month) {
|
||||||
const dt = new Date(parsed.year, parsed.month, parsed.day || 1);
|
const dt = new Date(parsed.year, parsed.month, parsed.day || 1);
|
||||||
return dt.toLocaleDateString(DATE_LANG, {
|
return dt.toLocaleDateString(locale, {
|
||||||
day: parsed.day ? "numeric" : undefined,
|
day: parsed.day ? "2-digit" : undefined,
|
||||||
month: "short",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return parsed.year.toString();
|
return parsed.year.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date: Date): string {
|
export function formatDate(date: Date, locale: string): string {
|
||||||
return date.toLocaleDateString(DATE_LANG, {
|
return date.toLocaleDateString(locale, {
|
||||||
day: "numeric",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -249,3 +248,9 @@ export function arrMoveMulti<T>(arr: T[], positions: number[], target: number) {
|
||||||
toInsert.reverse();
|
toInsert.reverse();
|
||||||
arr.splice(target + offset, 0, ...toInsert);
|
arr.splice(target + offset, 0, ...toInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCookie(cName: string, cValue: string, expDays: number) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + expDays * 24 * 60 * 60 * 1000);
|
||||||
|
document.cookie = `${cName}=${cValue}; expires=${date.toUTCString()}; SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const iconTiraya =
|
export const iconTiraya =
|
||||||
"M7.966 1.982a1.317 1.33 0 0 0-.799.62 1.317 1.33 0 0 0 .483 1.816l9.623 5.612-4.79 2.794A1.317 1.33 0 0 0 12 14.639a1.317 1.33 0 0 0 1.8.488l6.764-3.946a1.317 1.33 0 0 0 0-2.303l-11.6-6.763a1.317 1.33 0 0 0-.998-.133Zm-2.364 2.93a1.317 1.33 0 0 0-.659 1.151V19.59a1.317 1.33 0 0 0 1.317 1.33 1.317 1.33 0 0 0 1.317-1.33V8.366l4.791 2.794a1.317 1.33 0 0 0 1.798-.486 1.317 1.33 0 0 0-.48-1.818L6.918 4.913a1.317 1.33 0 0 0-1.317 0zm4.094 5.845a1.317 1.33 0 0 0-1.317 1.33v7.89a1.317 1.33 0 0 0 1.976 1.15l11.599-6.762a1.317 1.33 0 0 0 .48-1.816 1.317 1.33 0 0 0-1.797-.487l-9.624 5.61v-5.584a1.317 1.33 0 0 0-1.317-1.33Z";
|
"M9.27 10.61a1.524 1.524 0 0 0-1.525 1.524v8.628a1.524 1.524 0 0 0 2.286 1.32l12.81-7.396a1.524 1.524 0 0 0 .558-2.082 1.524 1.524 0 0 0-2.082-.557l-10.524 6.076v-5.989A1.524 1.524 0 0 0 9.27 10.61 M7.342 1.016a1.52 1.52 0 0 0-.926.71 1.524 1.524 0 0 0 .558 2.082l10.525 6.076-5.187 2.995a1.524 1.524 0 0 0-.558 2.081 1.524 1.524 0 0 0 2.082.558l7.472-4.314a1.524 1.524 0 0 0 0-2.64L8.498 1.169a1.52 1.52 0 0 0-1.156-.152 M4.713 4.228a1.52 1.52 0 0 0-.762 1.32v14.791a1.524 1.524 0 0 0 1.524 1.524A1.524 1.524 0 0 0 7 20.34V8.187l5.186 2.994a1.524 1.524 0 0 0 2.082-.558 1.524 1.524 0 0 0-.558-2.081L6.237 4.228a1.52 1.52 0 0 0-1.524 0";
|
||||||
export const iconAlbumOutline =
|
export const iconAlbumOutline =
|
||||||
"m12 16.1q1.75 0 3-1.19t1.25-2.91q0-1.78-1.24-3.01t-3.01-1.24q-1.72 0-2.91 1.25t-1.19 3q0 1.72 1.19 2.91t2.91 1.19zm0-3.1q-0.425 0-0.712-0.288t-0.288-0.712 0.288-0.712 0.712-0.288 0.712 0.288 0.288 0.712-0.288 0.712-0.712 0.288zm0 9q-2.05 0-3.88-0.788t-3.19-2.15-2.15-3.19-0.788-3.88q0-2.08 0.788-3.9t2.15-3.18 3.19-2.14 3.88-0.788q2.08 0 3.9 0.788t3.18 2.14 2.14 3.18 0.788 3.9q0 2.05-0.788 3.88t-2.14 3.19-3.18 2.15-3.9 0.788zm0-1.5q3.55 0 6.02-2.49t2.48-6.01q0-3.55-2.48-6.02t-6.02-2.48q-3.52 0-6.01 2.48t-2.49 6.02q0 3.52 2.49 6.01t6.01 2.49z";
|
"m12 16.1q1.75 0 3-1.19t1.25-2.91q0-1.78-1.24-3.01t-3.01-1.24q-1.72 0-2.91 1.25t-1.19 3q0 1.72 1.19 2.91t2.91 1.19zm0-3.1q-0.425 0-0.712-0.288t-0.288-0.712 0.288-0.712 0.712-0.288 0.712 0.288 0.288 0.712-0.288 0.712-0.712 0.288zm0 9q-2.05 0-3.88-0.788t-3.19-2.15-2.15-3.19-0.788-3.88q0-2.08 0.788-3.9t2.15-3.18 3.19-2.14 3.88-0.788q2.08 0 3.9 0.788t3.18 2.14 2.14 3.18 0.788 3.9q0 2.05-0.788 3.88t-2.14 3.19-3.18 2.15-3.9 0.788zm0-1.5q3.55 0 6.02-2.49t2.48-6.01q0-3.55-2.48-6.02t-6.02-2.48q-3.52 0-6.01 2.48t-2.49 6.02q0 3.52 2.49 6.01t6.01 2.49z";
|
||||||
export const iconSearch =
|
export const iconSearch =
|
||||||
|
|
6
src/routes/+layout.server.ts
Normal file
6
src/routes/+layout.server.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { LayoutServerLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = ({ locals: { locale } }) => {
|
||||||
|
// pass locale information from "server-context" to "shared server + client context"
|
||||||
|
return { locale };
|
||||||
|
};
|
|
@ -5,6 +5,8 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { afterNavigate } from "$app/navigation";
|
import { afterNavigate } from "$app/navigation";
|
||||||
|
import { setLocale } from "$i18n/i18n-svelte";
|
||||||
|
import type { LayoutData } from "./$types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clientState,
|
clientState,
|
||||||
|
@ -31,7 +33,10 @@
|
||||||
import NavbarMobile from "$lib/components/nav/NavbarMobile.svelte";
|
import NavbarMobile from "$lib/components/nav/NavbarMobile.svelte";
|
||||||
import PlayerbarMobile from "$lib/components/player/PlayerbarMobile.svelte";
|
import PlayerbarMobile from "$lib/components/player/PlayerbarMobile.svelte";
|
||||||
import FixedHeader from "$lib/components/header/FixedHeader.svelte";
|
import FixedHeader from "$lib/components/header/FixedHeader.svelte";
|
||||||
import ParaglideJsProvider from "$lib/components/layout/ParaglideJsProvider.svelte";
|
|
||||||
|
export let data: LayoutData;
|
||||||
|
// at the very top, set the locale before you access the store and before the actual rendering takes place
|
||||||
|
setLocale(data.locale);
|
||||||
|
|
||||||
// Layout boundaries
|
// Layout boundaries
|
||||||
const NAVBAR_MIN = 150;
|
const NAVBAR_MIN = 150;
|
||||||
|
@ -160,70 +165,68 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ParaglideJsProvider>
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<div
|
||||||
|
id="app"
|
||||||
|
class:mobile={!showNavbar}
|
||||||
|
bind:clientWidth={$screenWidth}
|
||||||
|
bind:clientHeight={$screenHeight}
|
||||||
|
on:dragend={onDragEnd}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="main-content"
|
||||||
class:mobile={!showNavbar}
|
style={`--navbar-width: ${navbarWidthSty}px; --queuebar-width: ${queuebarWidthSty}px;`}
|
||||||
bind:clientWidth={$screenWidth}
|
|
||||||
bind:clientHeight={$screenHeight}
|
|
||||||
on:dragend={onDragEnd}
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
id="main-content"
|
|
||||||
style={`--navbar-width: ${navbarWidthSty}px; --queuebar-width: ${queuebarWidthSty}px;`}
|
|
||||||
>
|
|
||||||
{#if showNavbar}
|
|
||||||
<nav class="sidebar" bind:this={navbarElm}>
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<div
|
|
||||||
class="handle right-0"
|
|
||||||
on:mousedown={initResizeNavbar}
|
|
||||||
on:touchstart|passive={initResizeNavbarTouch}
|
|
||||||
class:active={navbarResizing}
|
|
||||||
/>
|
|
||||||
<Menubar />
|
|
||||||
</nav>
|
|
||||||
{/if}
|
|
||||||
{#if showQueue}
|
|
||||||
<aside class="sidebar" bind:this={queuebarElm}>
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<div
|
|
||||||
class="handle left-0"
|
|
||||||
on:mousedown={initResizeQueuebar}
|
|
||||||
on:touchstart|passive={initResizeQueuebarTouch}
|
|
||||||
class:active={queuebarResizing}
|
|
||||||
/>
|
|
||||||
<DummyText />
|
|
||||||
</aside>
|
|
||||||
{/if}
|
|
||||||
<header>
|
|
||||||
<FixedHeader {scrollPos} />
|
|
||||||
</header>
|
|
||||||
<main
|
|
||||||
bind:this={$mainElm}
|
|
||||||
class:mainSmall={$mainSmall}
|
|
||||||
class:mainPhone={$mainPhone}
|
|
||||||
on:scroll={onScroll}
|
|
||||||
>
|
|
||||||
{#if !$page.data.header?.fading}
|
|
||||||
<div class="h-12" />
|
|
||||||
{/if}
|
|
||||||
<slot />
|
|
||||||
{#if !showNavbar}
|
|
||||||
<!-- Spacer to ensure enough bottom space to scroll beyond floating playerbara -->
|
|
||||||
<div class="h-24" />
|
|
||||||
{/if}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
{#if showNavbar}
|
{#if showNavbar}
|
||||||
<Playerbar />
|
<nav class="sidebar" bind:this={navbarElm}>
|
||||||
{:else}
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<PlayerbarMobile />
|
<div
|
||||||
<NavbarMobile />
|
class="handle right-0"
|
||||||
|
on:mousedown={initResizeNavbar}
|
||||||
|
on:touchstart|passive={initResizeNavbarTouch}
|
||||||
|
class:active={navbarResizing}
|
||||||
|
/>
|
||||||
|
<Menubar />
|
||||||
|
</nav>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showQueue}
|
||||||
|
<aside class="sidebar" bind:this={queuebarElm}>
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class="handle left-0"
|
||||||
|
on:mousedown={initResizeQueuebar}
|
||||||
|
on:touchstart|passive={initResizeQueuebarTouch}
|
||||||
|
class:active={queuebarResizing}
|
||||||
|
/>
|
||||||
|
<DummyText />
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
<header>
|
||||||
|
<FixedHeader {scrollPos} />
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
bind:this={$mainElm}
|
||||||
|
class:mainSmall={$mainSmall}
|
||||||
|
class:mainPhone={$mainPhone}
|
||||||
|
on:scroll={onScroll}
|
||||||
|
>
|
||||||
|
{#if !$page.data.header?.fading}
|
||||||
|
<div class="h-12" />
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
{#if !showNavbar}
|
||||||
|
<!-- Spacer to ensure enough bottom space to scroll beyond floating playerbara -->
|
||||||
|
<div class="h-24" />
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</ParaglideJsProvider>
|
{#if showNavbar}
|
||||||
|
<Playerbar />
|
||||||
|
{:else}
|
||||||
|
<PlayerbarMobile />
|
||||||
|
<NavbarMobile />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
#app {
|
#app {
|
||||||
|
|
|
@ -1 +1,11 @@
|
||||||
export const ssr = false;
|
import type { LayoutLoad } from "./$types";
|
||||||
|
import type { Locales } from "$i18n/i18n-types";
|
||||||
|
import { loadLocaleAsync } from "$i18n/i18n-util.async";
|
||||||
|
|
||||||
|
export const load: LayoutLoad<{ locale: Locales }> = async ({ data: { locale } }) => {
|
||||||
|
await loadLocaleAsync(locale);
|
||||||
|
1;
|
||||||
|
|
||||||
|
// pass locale to the "rendering context"
|
||||||
|
return { locale };
|
||||||
|
};
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL, { setLocale, locale } from "$i18n/i18n-svelte";
|
||||||
import {
|
import { locales } from "$i18n/i18n-util";
|
||||||
availableLanguageTags,
|
|
||||||
languageTag,
|
|
||||||
setLanguageTag,
|
|
||||||
} from "$paraglide/runtime";
|
|
||||||
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
||||||
import DummyText from "$lib/components/ui/DummyText.svelte";
|
import DummyText from "$lib/components/ui/DummyText.svelte";
|
||||||
import type { ChangeEventHandler } from "svelte/elements";
|
import type { ChangeEventHandler } from "svelte/elements";
|
||||||
|
import { loadLocaleAsync } from "$i18n/i18n-util.async";
|
||||||
|
import type { Locales } from "$i18n/i18n-types";
|
||||||
|
import { setCookie } from "$lib/util/functions";
|
||||||
|
|
||||||
const lf = (lang: string) =>
|
const lf = (lang: string) =>
|
||||||
`[${lang}] ${new Intl.DisplayNames(lang, { type: "language" }).of(lang)}`;
|
`[${lang}] ${new Intl.DisplayNames(lang, { type: "language" }).of(lang)}`;
|
||||||
|
|
||||||
const langChange: ChangeEventHandler<HTMLSelectElement> = (e) => {
|
const langChange: ChangeEventHandler<HTMLSelectElement> = (e) => {
|
||||||
setLanguageTag(availableLanguageTags[e.currentTarget.selectedIndex]);
|
switchLocale(locales[e.currentTarget.selectedIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchLocale = async (newLocale: Locales) => {
|
||||||
|
await loadLocaleAsync(newLocale);
|
||||||
|
setLocale(newLocale);
|
||||||
|
setCookie("lang", newLocale, 365);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SimpleWrapper>
|
<SimpleWrapper>
|
||||||
<h1 class="text-4xl">{m.hello_world()}</h1>
|
<h1 class="text-4xl">{$LL.hello_world()}</h1>
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,11 +39,11 @@
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<label class="form-control">
|
<label class="form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">{m.language()}</span>
|
<span class="label-text">{$LL.language()}</span>
|
||||||
</div>
|
</div>
|
||||||
<select class="select select-bordered w-full max-w-xs" on:change={langChange}>
|
<select class="select select-bordered w-full max-w-xs" on:change={langChange}>
|
||||||
{#each availableLanguageTags as lang}
|
{#each locales as lang}
|
||||||
<option selected={lang === languageTag()}>{lf(lang)}</option>
|
<option selected={lang === $locale}>{lf(lang)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import type { HeaderData } from "$lib/util/types";
|
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import type { HeaderData } from "$lib/util/types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util.js";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: { title: m.app_name } satisfies HeaderData,
|
header: { title: LL.app_name() } satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import { SearchCtx, type HeaderData } from "$lib/util/types";
|
import { SearchCtx, type HeaderData } from "$lib/util/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: m.albums,
|
title: LL.albums(),
|
||||||
searchCtx: SearchCtx.Content,
|
searchCtx: SearchCtx.Content,
|
||||||
} satisfies HeaderData,
|
} satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import LL, { locale } from "$i18n/i18n-svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import { searchTerm } from "$lib/stores/layout";
|
import { searchTerm } from "$lib/stores/layout";
|
||||||
|
@ -23,8 +24,8 @@
|
||||||
{#each data.album.artists as artist}
|
{#each data.album.artists as artist}
|
||||||
<AvatarBadge link={artistLink(artist)} imageUrl={artist.imageUrl} bold />
|
<AvatarBadge link={artistLink(artist)} imageUrl={artist.imageUrl} bold />
|
||||||
{/each}
|
{/each}
|
||||||
<span>{formatDateStr(data.album.releaseDate)}</span>
|
<span>{formatDateStr(data.album.releaseDate, $locale)}</span>
|
||||||
<span>11 tracks, 31:23</span>
|
<span>{$LL.n_tracks(11)}, 31:23</span>
|
||||||
</div>
|
</div>
|
||||||
</ContentHeader>
|
</ContentHeader>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import type { HeaderData } from "$lib/util/types";
|
import type { HeaderData } from "$lib/util/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: m.artists,
|
title: LL.artists(),
|
||||||
} satisfies HeaderData,
|
} satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import { searchTerm } from "$lib/stores/layout";
|
import { searchTerm } from "$lib/stores/layout";
|
||||||
|
@ -20,10 +20,10 @@
|
||||||
{#if !$searchTerm}
|
{#if !$searchTerm}
|
||||||
<TrackList
|
<TrackList
|
||||||
{tracks}
|
{tracks}
|
||||||
name={m.top_tracks_of({ name: data.artist.name })}
|
name={$LL.top_tracks_of(data.artist.name)}
|
||||||
view={ListView.Catalog}
|
view={ListView.Catalog}
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-sm btn-outline mt-2 mb-4">{m.all_tracks()}</button>
|
<button class="btn btn-sm btn-outline mt-2 mb-4">{$LL.all_tracks()}</button>
|
||||||
{/if}
|
{/if}
|
||||||
<AlbumGrid {albums} searchTerm={$searchTerm} />
|
<AlbumGrid {albums} searchTerm={$searchTerm} />
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import type { HeaderData } from "$lib/util/types";
|
import type { HeaderData } from "$lib/util/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: m.playlists,
|
title: LL.title(),
|
||||||
} satisfies HeaderData,
|
} satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import LL from "$i18n/i18n-svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import { searchTerm } from "$lib/stores/layout";
|
import { searchTerm } from "$lib/stores/layout";
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<AvatarBadge link={userLink("Spotify", false)} bold />
|
<AvatarBadge link={userLink("Spotify", false)} bold />
|
||||||
<span>2023</span>
|
<span>2023</span>
|
||||||
<span>11 tracks, 31:23</span>
|
<span>{$LL.n_tracks(11)}, 31:23</span>
|
||||||
</div>
|
</div>
|
||||||
</ContentHeader>
|
</ContentHeader>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import { SearchCtx, type HeaderData } from "$lib/util/types";
|
import { SearchCtx, type HeaderData } from "$lib/util/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: m.search,
|
title: LL.search(),
|
||||||
searchCtx: SearchCtx.Global,
|
searchCtx: SearchCtx.Global,
|
||||||
} satisfies HeaderData,
|
} satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from "$paraglide/messages";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import ContentHeader from "$lib/components/header/ContentHeader.svelte";
|
import ContentHeader from "$lib/components/header/ContentHeader.svelte";
|
||||||
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
||||||
import TrackList from "$lib/components/list/TrackList.svelte";
|
import TrackList from "$lib/components/list/TrackList.svelte";
|
||||||
|
@ -14,16 +14,16 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContentHeader title={m.tracks()} imageUrl="">
|
<ContentHeader title={$LL.tracks()} imageUrl="">
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<span>11 tracks, 31:23</span>
|
<span>{$LL.n_tracks(11)}, 31:23</span>
|
||||||
</div>
|
</div>
|
||||||
</ContentHeader>
|
</ContentHeader>
|
||||||
|
|
||||||
<SimpleWrapper>
|
<SimpleWrapper>
|
||||||
<TrackList
|
<TrackList
|
||||||
{tracks}
|
{tracks}
|
||||||
name={m.tracks()}
|
name={$LL.tracks()}
|
||||||
searchTerm={$searchTerm}
|
searchTerm={$searchTerm}
|
||||||
view={ListView.Playlist}
|
view={ListView.Playlist}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import * as m from "$paraglide/messages";
|
|
||||||
import { LinkType, type HeaderData, SearchCtx } from "$lib/util/types";
|
import { LinkType, type HeaderData, SearchCtx } from "$lib/util/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { i18nObject } from "$i18n/i18n-util";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ parent }) => {
|
||||||
|
const { locale } = await parent();
|
||||||
|
const LL = i18nObject(locale);
|
||||||
|
|
||||||
export const load = (({ params }) => {
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: m.tracks,
|
title: LL.tracks(),
|
||||||
playLink: {
|
playLink: {
|
||||||
title: "Tracks",
|
title: LL.tracks(),
|
||||||
linkType: LinkType.Playlist,
|
linkType: LinkType.Playlist,
|
||||||
id: "",
|
id: "",
|
||||||
},
|
},
|
||||||
|
@ -15,4 +18,4 @@ export const load = (({ params }) => {
|
||||||
searchCtx: SearchCtx.Content,
|
searchCtx: SearchCtx.Content,
|
||||||
} satisfies HeaderData,
|
} satisfies HeaderData,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-color: hsl(var(--bc) / 0.4) transparent;
|
scrollbar-color: oklch(var(--bc) / 0.4) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
@ -17,11 +17,11 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: hsl(var(--bc) / 0.4);
|
background-color: oklch(var(--bc) / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid hsl(var(--bc));
|
outline: 2px solid oklch(var(--bc));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-cover {
|
.bg-cover {
|
||||||
|
@ -60,8 +60,8 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
background-color: hsl(var(--p));
|
background-color: oklch(var(--p));
|
||||||
color: hsl(var(--pc));
|
color: oklch(var(--pc));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><defs><linearGradient xlink:href="#a" id="b" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="matrix(2.63348 0 0 2.63348 -117.324 -128.325)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" style="stop-color:#0035ff;stop-opacity:1"/><stop offset="1" style="stop-color:#00eaff;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="scale(-2.63348) rotate(-60 -52.86 95.708)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="scale(-2.63348) rotate(60 97.432 -46.988)" gradientUnits="userSpaceOnUse"/></defs><path d="M88.727 55.007 40.399 82.91V50.36" style="fill:none;stroke:url(#b);stroke-width:14.4841;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="M26.085 81.313V25.509l28.19 16.275" style="fill:none;stroke:url(#c);stroke-width:14.4841;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="m34.615 13.97 48.328 27.902-28.19 16.275" style="fill:none;stroke:url(#d);stroke-width:14.4841;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><defs><linearGradient xlink:href="#a" id="b" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="matrix(2.63348 0 0 2.63348 -117.324 -128.325)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" style="stop-color:#16b5ab;stop-opacity:1"/><stop offset="1" style="stop-color:#00eaff;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="scale(-2.63348) rotate(-60 -52.86 95.708)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="scale(-2.63348) rotate(60 97.432 -46.988)" gradientUnits="userSpaceOnUse"/></defs><path d="M88.727 55.007 40.399 82.91V50.36" style="fill:none;stroke:url(#b);stroke-width:11.49784008;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" transform="matrix(1.10449 0 0 1.10449 -5.998 -5.061)"/><path d="M26.085 81.313V25.509l28.19 16.275" style="fill:none;stroke:url(#c);stroke-width:11.49784008;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" transform="matrix(1.10449 0 0 1.10449 -5.998 -5.061)"/><path d="m34.615 13.97 48.328 27.902-28.19 16.275" style="fill:none;stroke:url(#d);stroke-width:11.49784008;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" transform="matrix(1.10449 0 0 1.10449 -5.998 -5.061)"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><defs><linearGradient xlink:href="#a" id="b" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="translate(-89.437 -98.604) scale(2.19457)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" style="stop-color:#0035ff;stop-opacity:1"/><stop offset="1" style="stop-color:#00eaff;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="rotate(120 123.026 73.548) scale(2.19457)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="rotate(-120 66.107 125.211) scale(2.19457)" gradientUnits="userSpaceOnUse"/></defs><circle cx="50" cy="50" r="50" style="fill:#1a1e25;stroke-width:2.6382"/><path d="M82.272 54.173 42 77.424V50.3" style="fill:none;stroke:url(#b);stroke-width:12.0701;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="M30.071 76.094V29.591l23.491 13.563" style="fill:none;stroke:url(#c);stroke-width:12.0701;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="m37.18 19.975 40.272 23.251L53.961 56.79" style="fill:none;stroke:url(#d);stroke-width:12.0701;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><defs><linearGradient xlink:href="#a" id="b" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="translate(-89.437 -98.604) scale(2.19457)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" style="stop-color:#16b5ab;stop-opacity:1"/><stop offset="1" style="stop-color:#00eaff;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="rotate(120 123.026 73.548) scale(2.19457)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="75.381" x2="56.756" y1="72.556" y2="62.604" gradientTransform="rotate(-120 66.107 125.211) scale(2.19457)" gradientUnits="userSpaceOnUse"/></defs><circle cx="50" cy="50" r="50" style="fill:#1a1e25;stroke-width:2.6382"/><path d="M82.272 54.173 42 77.424V50.3" style="fill:none;stroke:url(#b);stroke-width:9.58154663;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="M30.071 76.094V29.591l23.491 13.563" style="fill:none;stroke:url(#c);stroke-width:9.58154663;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/><path d="m37.18 19.975 40.272 23.251L53.961 56.79" style="fill:none;stroke:url(#d);stroke-width:9.58154663;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,5 +1,5 @@
|
||||||
import adapter from "@sveltejs/adapter-static";
|
import adapter from "@sveltejs/adapter-node";
|
||||||
import { vitePreprocess } from "@sveltejs/kit/vite";
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
import { preprocessMeltUI } from "@melt-ui/pp";
|
import { preprocessMeltUI } from "@melt-ui/pp";
|
||||||
import sequence from "svelte-sequential-preprocessor";
|
import sequence from "svelte-sequential-preprocessor";
|
||||||
|
|
||||||
|
@ -12,12 +12,11 @@ const config = {
|
||||||
kit: {
|
kit: {
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
fallback: "index.html",
|
precompress: true,
|
||||||
// precompress: true,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
alias: {
|
alias: {
|
||||||
$paraglide: "./src/paraglide",
|
$i18n: "./src/i18n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@ module.exports = {
|
||||||
"--rounded-btn": "1.9rem",
|
"--rounded-btn": "1.9rem",
|
||||||
},
|
},
|
||||||
"tiraya-light": {
|
"tiraya-light": {
|
||||||
primary: "#0065ff",
|
primary: "#11c2bf",
|
||||||
secondary: "#1e3ca1",
|
secondary: "#1e3ca1",
|
||||||
accent: "#d99330",
|
accent: "#d99330",
|
||||||
neutral: "#181a2a",
|
neutral: "#181a2a",
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true
|
||||||
"moduleResolution": "Bundler"
|
|
||||||
}
|
}
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import { paraglide } from "@inlang/paraglide-js-adapter-vite";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [sveltekit()],
|
||||||
sveltekit(),
|
|
||||||
paraglide({ project: "./project.inlang", outdir: "./src/paraglide" }),
|
|
||||||
],
|
|
||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
include: ["src/**/*.{test,spec}.{js,ts}"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue