Compare commits

...

10 commits

64 changed files with 1449 additions and 884 deletions

View file

@ -6,6 +6,7 @@ node_modules
.env
.env.*
!.env.example
/scripts
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml

View file

@ -1,8 +1,19 @@
repos:
- repo: local
hooks:
- id: svelte-lint
name: svelte-lint
- id: svelte-check
name: svelte-check
language: system
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.example
/src/paraglide
/src/i18n
# Ignore files for PNPM, NPM and YARN
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.

View file

@ -1,5 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hallo Welt",
"search": "Suche",
"home": "Startseite",
@ -29,11 +28,11 @@
"unmute": "Stummschaltung aufheben",
"sort_date": "Nach Datum sortieren",
"sort_name": "Alphabetisch sortieren",
"play_item": "{title} abspielen",
"play_item": "{0} abspielen",
"library_add": "Zur Sammlung hinzufügen",
"library_remove": "Von der Sammlung entfernen",
"search_no_result": "Es gibt keine {itemType}, die deiner Suche entsprechen",
"empty_list": "Diese Liste enthält keine {itemType}",
"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 {0|{artist: Künstler, album: Alben, track: Titel, playlist: Playlists, *: Elemente}}",
"clear_search": "Suche löschen",
"title": "Titel",
"album": "Album",
@ -42,8 +41,8 @@
"added_by": "Hinzugefügt von",
"duration": "Dauer",
"singles": "Singles",
"search_item": "{item} durchsuchen",
"top_tracks_of": "Beliebte Titel von {name}",
"search_item": "{0} durchsuchen",
"top_tracks_of": "Beliebte Titel von {0}",
"all_tracks": "Alle Titel",
"notifications": "Benachrichtigungen",
"playback_history": "Wiedergabeverlauf",
@ -60,7 +59,7 @@
"copy_url": "URL kopieren",
"download_audio_files": "Audiodateien herunterladen",
"search_create_playlist": "Playlist suchen/erstellen",
"playlist_new_name": "Playlist \"{name}\" erstellen",
"playlist_new_name": "Playlist \"{0}\" erstellen",
"view_artist": "Zeige Künstler",
"view_album": "Zeige Album",
"view_track_info": "Zeige Titelinfo",
@ -68,5 +67,13 @@
"playlist_remove": "Von Playlist entfernen",
"download_audio_file": "Audiodatei herunterladen",
"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",
"search": "Search",
"home": "Home",
@ -29,11 +28,11 @@
"unmute": "Unmute",
"sort_date": "Sort by date",
"sort_name": "Sort by name",
"play_item": "Play {title}",
"play_item": "Play {0}",
"library_add": "Add to library",
"library_remove": "Remove from library",
"search_no_result": "There are no {itemType} matching your search",
"empty_list": "There are no {itemType} in this list",
"search_no_result": "There are no {0|{artist: artists, album: albums, track: tracks, playlist: playlists, *: items}} matching your search",
"empty_list": "There are no {0|{artist: artists, album: albums, track: tracks, playlist: playlists, *: items}} in this list",
"clear_search": "Clear search",
"title": "Title",
"album": "Album",
@ -42,8 +41,8 @@
"added_by": "Added by",
"duration": "Duration",
"singles": "Singles",
"search_item": "Search {item}",
"top_tracks_of": "Top tracks of {name}",
"search_item": "Search {0}",
"top_tracks_of": "Top tracks of {0}",
"all_tracks": "All tracks",
"notifications": "Notifications",
"playback_history": "Playback history",
@ -60,7 +59,7 @@
"copy_url": "Copy URL",
"download_audio_files": "Download audio files",
"search_create_playlist": "Search/create playlist",
"playlist_new_name": "Create playlist \"{name}\"",
"playlist_new_name": "Create playlist \"{0}\"",
"view_artist": "View artist",
"view_album": "View album",
"view_track_info": "View track info",
@ -68,5 +67,13 @@
"playlist_remove": "Remove from playlist",
"download_audio_file": "Download audio file",
"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",
"scripts": {
"dev": "vite dev",
"build": "paraglide-js compile --project ./project.inlang && vite build",
"build": "npm run typesafe-i18n && vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --plugin prettier-plugin-svelte --check . && eslint . && npm run check",
"format": "prettier --plugin prettier-plugin-svelte --write .",
"postinstall": "paraglide-js compile --project ./project.inlang"
"typesafe-i18n": "tsx scripts/import_translations.ts"
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.16",
"@mdi/js": "^7.3.67",
"@resreq/event-hub": "^1.6.0",
"@tirayamusic/melt-ui": "^0.37.5",
"daisyui": "^3.9.4",
"daisyui": "^4.4.20",
"fast-average-color": "^9.4.0",
"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": {
"@inlang/paraglide-js": "1.0.0-prerelease.19",
"@inlang/paraglide-js-adapter-vite": "^1.0.2",
"@melt-ui/pp": "^0.1.4",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.27.7",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.5",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "^10.4.16",
@ -47,9 +48,10 @@
"svelte-sequential-preprocessor": "^2.0.1",
"tailwindcss": "^3.3.6",
"tslib": "^2.6.2",
"tsx": "^4.6.2",
"typescript": "^5.3.3",
"vite": "^4.5.1",
"vite-bundle-visualizer": "^0.10.1",
"vitest": "^0.33.0"
"vite": "^5.0.0",
"vite-bundle-visualizer": "^1.0.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 { MeltEventHandler } from "@tirayamusic/melt-ui/internal/types";
import type { Locales, TranslationFunctions } from "$i18n/i18n-types";
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
locale: Locales;
LL: TranslationFunctions;
}
// interface PageData {}
// interface Platform {}
}

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-theme="tiraya">
<html lang="%lang%" data-theme="tiraya">
<head>
<meta charset="utf-8" />
<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">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { mdiPlus } from "@mdi/js";
import { melt } from "@tirayamusic/melt-ui";
import type { MeltEvent } from "@tirayamusic/melt-ui/internal/types";
@ -60,7 +60,7 @@
<div class="item noicon">
<input
type="text"
placeholder={m.search_create_playlist()}
placeholder={$LL.search_create_playlist()}
on:keydown={onPlaylistSearchKeydown}
bind:this={playlistSearchElm}
bind:value={playlistSearch}
@ -77,8 +77,8 @@
<Icon path={mdiPlus} size={1.4} />
<span
>{playlistSearch
? m.playlist_new_name({ name: playlistSearch })
: m.playlist_new()}</span
? $LL.playlist_new_name(playlistSearch)
: $LL.playlist_new()}</span
>
</div>
</div>

View file

@ -1,6 +1,6 @@
<!-- Context menu for artists, albums and playlists -->
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiContentCopy,
mdiDownload,
@ -44,40 +44,40 @@
<div class="ctxmenu" use:melt={menu}>
<div class="item" use:melt={item}>
<Icon path={mdiPlaylistPlay} size={1.4} />
<span>{m.play_next()}</span>
<span>{$LL.play_next()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiPlaylistPlus} size={1.4} />
<span>{m.add_to_queue()}</span>
<span>{$LL.add_to_queue()}</span>
</div>
<div class="item sep" use:melt={item}>
<Icon path={mdiHeartOutline} size={1.4} />
<span>{m.library_add()}</span>
<span>{$LL.library_add()}</span>
</div>
<div class="item" use:melt={$playlistMenuTrigger}>
<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>
<div class="item sep" use:melt={item}>
<Icon path={mdiPencil} size={1.4} />
<span>{m.edit()}</span>
<span>{$LL.edit()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiTrashCan} size={1.4} />
<span>{m.trash()}</span>
<span>{$LL.trash()}</span>
</div>
<div class="item sep" use:melt={item}>
<Icon path={mdiReload} size={1.4} />
<span>{m.update_now()}</span>
<span>{$LL.update_now()}</span>
</div>
<div class="item sep" use:melt={$shareMenuTrigger}>
<Icon path={mdiShare} size={1.4} />
<span>{m.share()}</span>
<span>{$LL.share()}</span>
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
</div>
</div>
@ -87,11 +87,11 @@
<div class="ctxmenu" use:melt={$shareMenu}>
<div class="item" use:melt={item}>
<Icon path={mdiContentCopy} size={1.4} />
<span>{m.copy_url()}</span>
<span>{$LL.copy_url()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiDownload} size={1.4} />
<span>{m.download_audio_files()}</span>
<span>{$LL.download_audio_files()}</span>
</div>
</div>
{/if}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LL from "$i18n/i18n-svelte";
import { mdiLogin, mdiCog, mdiBookOpen } from "@mdi/js";
import { melt } from "@tirayamusic/melt-ui";
@ -17,19 +18,19 @@
<div class="ctxmenu" use:melt={menu}>
<div class="item" use:melt={item} on:m-click={() => alert("login")}>
<Icon path={mdiLogin} size={1.4} />
<span>Login</span>
<span>{$LL.login()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiCog} size={1.4} />
<span>Settings</span>
<span>{$LL.settings()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiBookOpen} size={1.4} />
<span>Documentation</span>
<span>{$LL.documentation()}</span>
</div>
<div class="item" use:melt={item}>
<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>
{/if}

View file

@ -1,6 +1,6 @@
<!-- Context menu for tracks -->
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiPlaylistPlay,
mdiPlaylistPlus,
@ -72,44 +72,44 @@
<div class="ctxmenu" use:melt={menu}>
{#if !single}
<div class="item item-sm item-primary">
<span>{tracks.length} tracks</span>
<span>{$LL.n_tracks(tracks.length)}</span>
</div>
{/if}
<!-- Queue -->
<div class="item" use:melt={item}>
<Icon path={mdiPlaylistPlay} size={1.4} />
<span>{m.play_next()}</span>
<span>{$LL.play_next()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiPlaylistPlus} size={1.4} />
<span>{m.add_to_queue()}</span>
<span>{$LL.add_to_queue()}</span>
</div>
<!-- Track details -->
{#if single}
<div class="item sep" use:melt={item}>
<Icon path={mdiAccountMusic} size={1.4} />
<span>{m.view_artist()}</span>
<span>{$LL.view_artist()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiAlbum} size={1.4} />
<span>{m.view_album()}</span>
<span>{$LL.view_album()}</span>
</div>
<div class="item" use:melt={item}>
<Icon path={mdiText} size={1.4} />
<span>{m.view_track_info()}</span>
<span>{$LL.view_track_info()}</span>
</div>
{/if}
<!-- User library -->
<div class="item sep" use:melt={item}>
<Icon path={mdiHeartOutline} size={1.4} />
<span>{m.library_add()}</span>
<span>{$LL.library_add()}</span>
</div>
<div class="item" use:melt={$playlistMenuTrigger}>
<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>
@ -117,11 +117,11 @@
{#if playlistIndex !== undefined}
<div class="item sep" use:melt={item}>
<Icon path={mdiTrashCan} size={1.4} />
<span>{m.playlist_remove()}</span>
<span>{$LL.playlist_remove()}</span>
</div>
<div class="item" use:melt={$moveMenuTrigger}>
<Icon path={mdiSwapVertical} size={1.4} />
<span>{m.move()}</span>
<span>{$LL.move()}</span>
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
</div>
{/if}
@ -129,7 +129,7 @@
<!-- Share -->
<div class="item sep" use:melt={$shareMenuTrigger}>
<Icon path={mdiShare} size={1.4} />
<span>{m.share()}</span>
<span>{$LL.share()}</span>
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
</div>
</div>
@ -150,11 +150,11 @@
<div class="ctxmenu" use:melt={$shareMenu}>
<div class="item" use:melt={item}>
<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 class="item" use:melt={item}>
<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>
{/if}

View file

@ -1,6 +1,6 @@
<!-- Fixed header that is shown when scrolling down -->
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { page } from "$app/stores";
import { beforeNavigate } from "$app/navigation";
import { mdiBellOutline, mdiClose, mdiHistory, mdiPlay } from "@mdi/js";
@ -29,7 +29,7 @@
} else if (t) {
title = t;
} else {
title = m.app_name();
title = $LL.app_name();
}
}
$: searchCtx = $page.data.header?.searchCtx;
@ -55,11 +55,11 @@
)
: 1;
let bgcolor = "hsl(var(--b2)";
let bgcolor = "oklch(var(--b2)";
$: if ($headerColor) {
bgcolor = `rgb(${$headerColor.r}, ${$headerColor.g}, ${$headerColor.b}, ${opacity})`;
} else {
bgcolor = `hsl(var(--b2) / ${opacity})`;
bgcolor = `oklch(var(--b2) / ${opacity})`;
}
let inputElm: HTMLInputElement;
@ -68,8 +68,8 @@
let searchLabel: string;
$: if (searchCtx) {
if (searchCtx === SearchCtx.Global) searchLabel = m.search();
else searchLabel = m.search_item({ item: title });
if (searchCtx === SearchCtx.Global) searchLabel = $LL.search();
else searchLabel = $LL.search_item(title);
}
function searchOnBlur(e: FocusEvent) {
@ -119,7 +119,7 @@
{#if $page.data.header?.playLink}
<IconButton
path={mdiPlay}
ariaLabel={m.play_item({ title })}
ariaLabel={$LL.play_item(title)}
color="primary"
on:click={() => alert("Playing " + title)}
/>
@ -135,13 +135,13 @@
<div class="flex gap-1 items-center">
{#if homeScreen}
<IconButton path={mdiBellOutline} ariaLabel={m.notifications()} />
<IconButton path={mdiHistory} ariaLabel={m.playback_history()} />
<IconButton path={mdiBellOutline} ariaLabel={$LL.notifications()} />
<IconButton path={mdiHistory} ariaLabel={$LL.playback_history()} />
{/if}
{#if searchCtx}
<IconButton
path={searchShow ? mdiClose : iconSearch}
ariaLabel={searchShow ? m.clear_search() : searchLabel}
ariaLabel={searchShow ? $LL.clear_search() : searchLabel}
on:click={() => {
if (searchShow) {
clearSearch();

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { melt, createDropdownMenu } from "@tirayamusic/melt-ui";
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
@ -14,7 +14,7 @@
<button
class="btn btn-ghost btn-circle btn-sm avatar"
aria-label={m.options()}
aria-label={$LL.options()}
use:melt={$trigger}
>
<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">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { createEventDispatcher, onDestroy, onMount } from "svelte";
import { mdiClockOutline } from "@mdi/js";
import { generateId, kbd } from "@tirayamusic/melt-ui/internal/helpers";
@ -421,7 +421,7 @@
frame = requestAnimationFrame(poll);
});
onDestroy(() => {
cancelAnimationFrame(frame);
if (frame) cancelAnimationFrame(frame);
});
// DRAG AND DROP
@ -691,35 +691,35 @@
</div>
<div class="col first" role="columnheader" aria-colindex={2}>
<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>
</SortTitle>
</div>
{#if view > ListView.Album}
<div class="col var1" role="columnheader" aria-colindex={3}>
<SortTitle bind:sorting col={Filters.ALBUM}>
<span class="ellipsis-ol">{m.album()}</span>
<span class="ellipsis-ol">{$LL.album()}</span>
</SortTitle>
</div>
{/if}
{#if view > ListView.Album}
<div class="col var2" role="columnheader" aria-colindex={4}>
<SortTitle bind:sorting col={Filters.RELEASE_DATE}>
<span class="ellipsis-ol">{m.release_date()}</span>
<span class="ellipsis-ol">{$LL.release_date()}</span>
</SortTitle>
</div>
{/if}
{#if view === ListView.Playlist}
<div class="col var3" role="columnheader" aria-colindex={5}>
<SortTitle bind:sorting col={Filters.ADDED_DATE}>
<span class="ellipsis-ol">{m.date_added()}</span>
<span class="ellipsis-ol">{$LL.date_added()}</span>
</SortTitle>
</div>
{/if}
{#if showAuthors}
<div class="col var4" role="columnheader" aria-colindex={6}>
<SortTitle bind:sorting col={Filters.ADDED_BY}>
<span class="ellipsis-ol">{m.added_by()}</span>
<span class="ellipsis-ol">{$LL.added_by()}</span>
</SortTitle>
</div>
{/if}
@ -729,7 +729,7 @@
role="columnheader"
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} />
</SortTitle>
</div>
@ -737,7 +737,7 @@
</div>
{#if filteredTracks.length === 0}
<NoItemsMsg itemType="tracks" search={Boolean(searchTerm)} />
<NoItemsMsg itemType="track" search={Boolean(searchTerm)} />
{/if}
<div

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { locale } from "$i18n/i18n-svelte";
import { mdiPlay, mdiDotsVertical } from "@mdi/js";
import { createContextMenu, createDropdownMenu, melt } from "@tirayamusic/melt-ui";
@ -136,7 +137,7 @@
<div class="col var2" role="gridcell" aria-colindex={4}>
{#if track.item.album.releaseDate}
<span class="ellipsis-ol">
{formatDateStr(track.item.album.releaseDate)}
{formatDateStr(track.item.album.releaseDate, $locale)}
</span>
{/if}
</div>
@ -146,7 +147,7 @@
<div class="col var3" role="gridcell" aria-colindex={5}>
{#if track.item.addedDate}
<span class="ellipsis-ol">
{formatDate(track.item.addedDate)}
{formatDate(track.item.addedDate, $locale)}
</span>
{/if}
</div>

View file

@ -21,7 +21,7 @@
@apply bg-base-100;
.row {
border-bottom: 1px solid hsl(var(--bc) / 0.4);
border-bottom: 1px solid oklch(var(--bc) / 0.4);
height: 100%;
@apply font-semibold;

View file

@ -1,6 +1,6 @@
<script>
import { page } from "$app/stores";
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiAccountMusic,
mdiAccountMusicOutline,
@ -25,31 +25,31 @@
<div class="flex flex-col h-full">
<ul class="menu menu-compact flex flex-col p-1">
<NavbarItemLarge
title={m.home()}
title={$LL.home()}
icon={mdiHomeOutline}
iconActive={mdiHome}
href="/"
/>
<NavbarItemLarge
title={m.search()}
title={$LL.search()}
icon={iconSearch}
iconActive={iconSearchFilled}
href="/search"
/>
<NavbarItemLarge
title={m.artists()}
title={$LL.artists()}
icon={mdiAccountMusicOutline}
iconActive={mdiAccountMusic}
href="/artist"
/>
<NavbarItemLarge
title={m.albums()}
title={$LL.albums()}
icon={iconAlbumOutline}
iconActive={mdiAlbum}
href="/album"
/>
<NavbarItemLarge
title={m.tracks()}
title={$LL.tracks()}
icon={mdiMusicNoteOutline}
iconActive={mdiMusicNote}
href="/tracks"
@ -60,9 +60,9 @@
<a
href="/playlist"
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 class="flex flex-1 min-h-0 mb-2">

View file

@ -1,5 +1,5 @@
<script>
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiAccountMusic,
mdiAccountMusicOutline,
@ -16,27 +16,32 @@
</script>
<div class="btm-nav btm-nav-sm bg-base-300 z-50">
<NavbarMobileItem title="Home" icon={mdiHomeOutline} iconActive={mdiHome} href="/" />
<NavbarMobileItem
title={m.search()}
title={$LL.home()}
icon={mdiHomeOutline}
iconActive={mdiHome}
href="/"
/>
<NavbarMobileItem
title={$LL.search()}
icon={iconSearch}
iconActive={iconSearchFilled}
href="/search"
/>
<NavbarMobileItem
title={m.artists()}
title={$LL.artists()}
icon={mdiAccountMusicOutline}
iconActive={mdiAccountMusic}
href="/artist"
/>
<NavbarMobileItem
title={m.albums()}
title={$LL.albums()}
icon={iconAlbumOutline}
iconActive={mdiAlbum}
href="/album"
/>
<NavbarMobileItem
title={m.playlists()}
title={$LL.playlists()}
icon={mdiPlaylistMusicOutline}
iconActive={mdiPlaylistMusic}
href="/playlist"

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
import IconButton from "$lib/components/ui/IconButton.svelte";
@ -33,7 +33,7 @@
size={isLarge ? "sm" : "xs"}
color="default"
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}
/>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiPlay,
mdiSkipPrevious,
@ -48,7 +48,10 @@
<div class="track">
<div
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 />
<TrackName {track} />
@ -61,23 +64,23 @@
<div class="flex w-full space-x-2 justify-center items-center">
<IconToggle
iconOn={mdiShuffle}
ariaLabelOn={m.shuffle_enable()}
ariaLabelOff={m.shuffle_disable()}
ariaLabelOn={$LL.shuffle_enable()}
ariaLabelOff={$LL.shuffle_disable()}
/>
<IconButton path={mdiSkipPrevious} size="md" ariaLabel={m.previous()} />
<IconButton path={mdiSkipPrevious} size="md" ariaLabel={$LL.previous()} />
<IconToggle
iconOn={mdiPause}
iconOff={mdiPlay}
size="md"
color="primary"
iColorOn=""
ariaLabelOn={m.play()}
ariaLabelOff={m.pause()}
ariaLabelOn={$LL.play()}
ariaLabelOff={$LL.pause()}
/>
<IconButton path={mdiSkipNext} size="md" ariaLabel={m.next()} />
<IconButton path={mdiSkipNext} size="md" ariaLabel={$LL.next()} />
<IconToggleMulti
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
@ -91,7 +94,7 @@
max={length}
bind:value={position}
bind:displayValue={displayPosition}
ariaLabel={m.seek_bar()}
ariaLabel={$LL.seek_bar()}
/>
<span class="w-12">{formatDuration(length)}</span>
</div>
@ -103,15 +106,15 @@
<IconToggle
iconOn={mdiPlaylistPlay}
value={$clientState.showQueue}
ariaLabelOn={m.queue_show()}
ariaLabelOff={m.queue_hide()}
ariaLabelOn={$LL.queue_show()}
ariaLabelOff={$LL.queue_hide()}
on:change={toggleShowQueue}
/>
<VolumeControl />
<IconButton path={mdiFullscreen} ariaLabel={m.fullscreen_player()} />
<IconButton path={mdiFullscreen} ariaLabel={$LL.fullscreen_player()} />
<IconButtonTrigger
path={mdiDotsVertical}
ariaLabel={m.track_options()}
ariaLabel={$LL.track_options()}
trigger={$trigger}
/>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import {
mdiFullscreen,
mdiPause,
@ -75,10 +75,10 @@
class="shadow rounded-md z-50"
class:transition-colors={colorTrans}
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">
<button class="relative" aria-label={m.fullscreen_player()}>
<button class="relative" aria-label={$LL.fullscreen_player()}>
<div
class="absolute w-full h-full flex items-center justify-center bg-base-100/50
transition-opacity opacity-0 hover:opacity-100"
@ -113,7 +113,7 @@
<IconButton
path={mdiSkipPrevious}
size="md"
ariaLabel={m.previous()}
ariaLabel={$LL.previous()}
on:click={prevTrack}
/>
{/if}
@ -122,14 +122,14 @@
iconOff={mdiPlay}
size="md"
iColorOn=""
ariaLabelOff={m.pause()}
ariaLabelOn={m.play()}
ariaLabelOff={$LL.pause()}
ariaLabelOn={$LL.play()}
/>
{#if !$mainPhone}
<IconButton
path={mdiSkipNext}
size="md"
ariaLabel={m.next()}
ariaLabel={$LL.next()}
on:click={nextTrack}
/>
{/if}
@ -137,7 +137,7 @@
</div>
<div class="seekbar" bind:this={seekbarElm}>
<Slider ariaLabel={m.seek_bar()} />
<Slider ariaLabel={$LL.seek_bar()} />
</div>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { mdiVolumeHigh, mdiVolumeMedium, mdiVolumeLow, mdiVolumeMute } from "@mdi/js";
import IconButton from "$lib/components/ui/IconButton.svelte";
@ -40,14 +40,14 @@
<div class="flex items-center" on:wheel|passive={onScroll}>
<IconButton
path={icon}
ariaLabel={mute ? m.unmute() : m.mute()}
ariaLabel={mute ? $LL.unmute() : $LL.mute()}
on:click={toggleMute}
/>
<div class="inline-block w-20">
<Slider
min={0}
max={100}
ariaLabel={m.volume_control()}
ariaLabel={$LL.volume_control()}
value={mute ? 0 : volume}
on:change={updateVolume}
liveUpdate

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { mdiCalendar, mdiOrderAlphabeticalAscending } from "@mdi/js";
import {
@ -94,21 +94,21 @@
<div>
<div class="flex flex-wrap gap-2 justify-between sticky-header py-2 z-20">
<FilterButtons
items={[`${m.albums()} (${nAlbums})`, `${m.singles()} (${nSingles})`]}
items={[`${$LL.albums()} (${nAlbums})`, `${$LL.singles()} (${nSingles})`]}
mask={[nAlbums === 0, nSingles === 0]}
toggle
bind:value={albumFilter}
/>
<FilterButtons
items={[mdiCalendar, mdiOrderAlphabeticalAscending]}
ariaLabels={[m.sort_date(), m.sort_name()]}
ariaLabels={[$LL.sort_date(), $LL.sort_name()]}
bind:value={albumSort}
icons
/>
</div>
{#if searchTerm && filteredAlbums.length === 0}
<EmptySearchNote itemType="albums" search={Boolean(searchTerm)} />
<EmptySearchNote itemType="album" search={Boolean(searchTerm)} />
{/if}
<Grid totalItems={filteredAlbums.length} bind:startIndex bind:endIndex>

View file

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

View file

@ -1,6 +1,6 @@
<!-- Tile with a large image used to feature albums or playlists -->
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { createEventDispatcher } from "svelte";
import { mdiPlay } from "@mdi/js";
@ -20,7 +20,7 @@
<button
class="btn btn-circle btn-primary absolute bottom-2 right-2 z-10"
tabindex="-1"
aria-label={m.play_item({ title })}
aria-label={$LL.play_item(title)}
on:click={(e) => {
dispatch("play");
e.preventDefault();

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { mdiHeart, mdiHeartOutline } from "@mdi/js";
import IconToggle from "./IconToggle.svelte";
@ -13,7 +13,7 @@
iconOn={mdiHeart}
iconOff={mdiHeartOutline}
{size}
ariaLabelOn={m.library_add()}
ariaLabelOff={m.library_remove()}
ariaLabelOn={$LL.library_add()}
ariaLabelOff={$LL.library_remove()}
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 -->
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import { clearSearch } from "$lib/util/functions";
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">
{randomKaomoji(404)}
</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}
>{m.clear_search()}</button
>{$LL.clear_search()}</button
>
{:else}
<p>{m.empty_list({ itemType })}</p>
<p>{$LL.empty_list(itemType)}</p>
{/if}
</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 { getCssVariable } from "$lib/util/functions";
import type { Color } from "$lib/util/types";
@ -6,7 +6,7 @@ import { writable } from "svelte/store";
function getTextColor(): Color {
const colorStr = getCssVariable("--bc");
const parsed = parseHslString(colorStr);
const parsed = parseOklchString(colorStr);
return parsed || WHITE;
}

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { correctBgColor } from "./colors";
import { correctBgColor, oklchToRgb, colorToHex } from "./colors";
import { WHITE } from "./constants";
import type { Color } from "./types";
@ -10,3 +10,10 @@ describe("correctBgColor", () => {
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)
* @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;
l /= 100;
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.
*
@ -39,12 +111,24 @@ export function parseHslString(hsl: string): Color | null {
if (parts.length !== 3) return null;
const h = parseInt(parts[0]);
const s = parseInt(parts[1]);
const l = parseInt(parts[2]);
const s = parsePercentageString(parts[1]);
const l = parsePercentageString(parts[2]);
if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
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(
src: FastAverageColorResource | null,
callback: (res: Color | null) => void

View file

@ -4,7 +4,7 @@ import type { Color } from "./types";
// Colors
export const WHITE: Color = { r: 255, g: 255, b: 255 };
export const MIN_CONTRAST_TITLE = 4.5;
export const COLOR_B3 = "hsl(var(--b3))";
export const COLOR_B3 = "oklch(var(--b3))";
// Layout
export const MIN_CONTRAST_BODY = 7;

View file

@ -1,5 +1,4 @@
import type { Page } from "@sveltejs/kit";
import { DATE_LANG } from "./constants";
import { CLEAR_SEARCH, hub } from "./events";
import {
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);
if (parsed === null) return null;
if (parsed.month) {
const dt = new Date(parsed.year, parsed.month, parsed.day || 1);
return dt.toLocaleDateString(DATE_LANG, {
day: parsed.day ? "numeric" : undefined,
month: "short",
return dt.toLocaleDateString(locale, {
day: parsed.day ? "2-digit" : undefined,
month: "2-digit",
year: "numeric",
});
}
return parsed.year.toString();
}
export function formatDate(date: Date): string {
return date.toLocaleDateString(DATE_LANG, {
day: "numeric",
month: "short",
export function formatDate(date: Date, locale: string): string {
return date.toLocaleDateString(locale, {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
@ -249,3 +248,9 @@ export function arrMoveMulti<T>(arr: T[], positions: number[], target: number) {
toInsert.reverse();
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 =
"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 =
"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 =

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 { page } from "$app/stores";
import { afterNavigate } from "$app/navigation";
import { setLocale } from "$i18n/i18n-svelte";
import type { LayoutData } from "./$types";
import {
clientState,
@ -31,7 +33,10 @@
import NavbarMobile from "$lib/components/nav/NavbarMobile.svelte";
import PlayerbarMobile from "$lib/components/player/PlayerbarMobile.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
const NAVBAR_MIN = 150;
@ -160,70 +165,68 @@
});
</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
id="app"
class:mobile={!showNavbar}
bind:clientWidth={$screenWidth}
bind:clientHeight={$screenHeight}
on:dragend={onDragEnd}
id="main-content"
style={`--navbar-width: ${navbarWidthSty}px; --queuebar-width: ${queuebarWidthSty}px;`}
>
<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}
<Playerbar />
{:else}
<PlayerbarMobile />
<NavbarMobile />
<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>
</ParaglideJsProvider>
{#if showNavbar}
<Playerbar />
{:else}
<PlayerbarMobile />
<NavbarMobile />
{/if}
</div>
<style lang="postcss">
#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">
import * as m from "$paraglide/messages";
import {
availableLanguageTags,
languageTag,
setLanguageTag,
} from "$paraglide/runtime";
import LL, { setLocale, locale } from "$i18n/i18n-svelte";
import { locales } from "$i18n/i18n-util";
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
import DummyText from "$lib/components/ui/DummyText.svelte";
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) =>
`[${lang}] ${new Intl.DisplayNames(lang, { type: "language" }).of(lang)}`;
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>
<SimpleWrapper>
<h1 class="text-4xl">{m.hello_world()}</h1>
<h1 class="text-4xl">{$LL.hello_world()}</h1>
<div class="my-2">
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</div>
@ -34,11 +39,11 @@
<div class="my-2">
<label class="form-control">
<div class="label">
<span class="label-text">{m.language()}</span>
<span class="label-text">{$LL.language()}</span>
</div>
<select class="select select-bordered w-full max-w-xs" on:change={langChange}>
{#each availableLanguageTags as lang}
<option selected={lang === languageTag()}>{lf(lang)}</option>
{#each locales as lang}
<option selected={lang === $locale}>{lf(lang)}</option>
{/each}
</select>
</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 { 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 {
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 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 {
header: {
title: m.albums,
title: LL.albums(),
searchCtx: SearchCtx.Content,
} satisfies HeaderData,
};
}) satisfies PageLoad;
};

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LL, { locale } from "$i18n/i18n-svelte";
import type { PageData } from "./$types";
import { searchTerm } from "$lib/stores/layout";
@ -23,8 +24,8 @@
{#each data.album.artists as artist}
<AvatarBadge link={artistLink(artist)} imageUrl={artist.imageUrl} bold />
{/each}
<span>{formatDateStr(data.album.releaseDate)}</span>
<span>11 tracks, 31:23</span>
<span>{formatDateStr(data.album.releaseDate, $locale)}</span>
<span>{$LL.n_tracks(11)}, 31:23</span>
</div>
</ContentHeader>

View file

@ -1,11 +1,14 @@
import * as m from "$paraglide/messages";
import type { HeaderData } from "$lib/util/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 {
header: {
title: m.artists,
title: LL.artists(),
} satisfies HeaderData,
};
}) satisfies PageLoad;
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import type { PageData } from "./$types";
import { searchTerm } from "$lib/stores/layout";
@ -20,10 +20,10 @@
{#if !$searchTerm}
<TrackList
{tracks}
name={m.top_tracks_of({ name: data.artist.name })}
name={$LL.top_tracks_of(data.artist.name)}
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}
<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 { 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 {
header: {
title: m.playlists,
title: LL.title(),
} satisfies HeaderData,
};
}) satisfies PageLoad;
};

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LL from "$i18n/i18n-svelte";
import type { PageData } from "./$types";
import { searchTerm } from "$lib/stores/layout";
@ -31,7 +32,7 @@
<div class="badges">
<AvatarBadge link={userLink("Spotify", false)} bold />
<span>2023</span>
<span>11 tracks, 31:23</span>
<span>{$LL.n_tracks(11)}, 31:23</span>
</div>
</ContentHeader>

View file

@ -1,12 +1,15 @@
import * as m from "$paraglide/messages";
import { SearchCtx, type HeaderData } from "$lib/util/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 {
header: {
title: m.search,
title: LL.search(),
searchCtx: SearchCtx.Global,
} satisfies HeaderData,
};
}) satisfies PageLoad;
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as m from "$paraglide/messages";
import LL from "$i18n/i18n-svelte";
import ContentHeader from "$lib/components/header/ContentHeader.svelte";
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
import TrackList from "$lib/components/list/TrackList.svelte";
@ -14,16 +14,16 @@
});
</script>
<ContentHeader title={m.tracks()} imageUrl="">
<ContentHeader title={$LL.tracks()} imageUrl="">
<div class="badges">
<span>11 tracks, 31:23</span>
<span>{$LL.n_tracks(11)}, 31:23</span>
</div>
</ContentHeader>
<SimpleWrapper>
<TrackList
{tracks}
name={m.tracks()}
name={$LL.tracks()}
searchTerm={$searchTerm}
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 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 {
header: {
title: m.tracks,
title: LL.tracks(),
playLink: {
title: "Tracks",
title: LL.tracks(),
linkType: LinkType.Playlist,
id: "",
},
@ -15,4 +18,4 @@ export const load = (({ params }) => {
searchCtx: SearchCtx.Content,
} satisfies HeaderData,
};
}) satisfies PageLoad;
};

View file

@ -7,7 +7,7 @@
}
html {
scrollbar-color: hsl(var(--bc) / 0.4) transparent;
scrollbar-color: oklch(var(--bc) / 0.4) transparent;
}
@media (min-width: 768px) {
@ -17,11 +17,11 @@ html {
}
::-webkit-scrollbar-thumb {
background-color: hsl(var(--bc) / 0.4);
background-color: oklch(var(--bc) / 0.4);
}
:focus-visible {
outline: 2px solid hsl(var(--bc));
outline: 2px solid oklch(var(--bc));
}
.bg-cover {
@ -60,8 +60,8 @@ html {
}
mark {
background-color: hsl(var(--p));
color: hsl(var(--pc));
background-color: oklch(var(--p));
color: oklch(var(--pc));
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 { vitePreprocess } from "@sveltejs/kit/vite";
import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { preprocessMeltUI } from "@melt-ui/pp";
import sequence from "svelte-sequential-preprocessor";
@ -12,12 +12,11 @@ const config = {
kit: {
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
fallback: "index.html",
// precompress: true,
precompress: true,
}),
alias: {
$paraglide: "./src/paraglide",
$i18n: "./src/i18n",
},
},
};

View file

@ -36,7 +36,7 @@ module.exports = {
"--rounded-btn": "1.9rem",
},
"tiraya-light": {
primary: "#0065ff",
primary: "#11c2bf",
secondary: "#1e3ca1",
accent: "#d99330",
neutral: "#181a2a",

View file

@ -8,8 +8,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "Bundler"
"strict": true
}
// 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 { defineConfig } from "vitest/config";
import { paraglide } from "@inlang/paraglide-js-adapter-vite";
import path from "path";
export default defineConfig({
plugins: [
sveltekit(),
paraglide({ project: "./project.inlang", outdir: "./src/paraglide" }),
],
plugins: [sveltekit()],
test: {
include: ["src/**/*.{test,spec}.{js,ts}"],
},