Compare commits

...

10 commits

64 changed files with 1449 additions and 884 deletions

View file

@ -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

View file

@ -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)$

View file

@ -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
View file

@ -0,0 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
"adapter": "svelte",
"baseLocale": "en"
}

View file

@ -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.

View file

@ -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}}"
} }

View file

@ -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}}"
} }

View file

@ -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"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View 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
View file

@ -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 {}
} }

View file

@ -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
View 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
View 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;
};

View file

@ -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>

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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();

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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>

View file

@ -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;

View file

@ -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">

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -44,7 +44,7 @@
frame = requestAnimationFrame(poll); frame = requestAnimationFrame(poll);
}); });
onDestroy(() => { onDestroy(() => {
cancelAnimationFrame(frame); if (frame) cancelAnimationFrame(frame);
}); });
</script> </script>

View file

@ -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();

View file

@ -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
/> />

View 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

View file

@ -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>

View file

@ -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;
} }

View file

@ -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));
});

View file

@ -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

View file

@ -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;

View file

@ -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`;
}

View file

@ -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 =

View 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 };
};

View file

@ -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 {

View file

@ -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 };
};

View file

@ -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>

View file

@ -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; };

View file

@ -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; };

View file

@ -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>

View file

@ -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; };

View file

@ -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} />

View file

@ -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; };

View file

@ -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>

View file

@ -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; };

View file

@ -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}
/> />

View file

@ -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; };

View file

@ -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;
} }

View file

@ -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

View file

@ -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

View file

@ -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",
}, },
}, },
}; };

View file

@ -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",

View file

@ -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
// //

View file

@ -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}"],
}, },