Compare commits

..
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 ca9917d8cd chore(web): clear kit 3's alias and Vite's config-loader deprecations (#5021)
release / govulncheck (push) Successful in 23s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m49s
release / android (push) Successful in 5m32s
release / Build signed APK (releases and dev) (push) Successful in 5m39s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 28s
release / Verify release artifacts (tag releases only) (push) Skipped
CI run 8789 warned on both:

- kit 3 deprecates `config.alias`. The $test-utils alias becomes a
  `#test-utils/*` subpath import, matching #lib, and its 43 import sites
  move with it.
- Vite's coming native config loader needs the extension on
  vitest.config.ts's import of vite.config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:03:38 -04:00
bvandeusenandClaude Opus 5.5 b1b10b0c77 fix(web): run Tailwind 4 through @tailwindcss/vite, not PostCSS (#5021)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 56s
release / go (push) Successful in 2m22s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 5m54s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m14s
release / Verify release artifacts (tag releases only) (push) Skipped
The image build on fecd030b failed in `vite build`:

  [postcss] ENOENT: no such file or directory, open '/web/tailwindcss'

With a PostCSS config present, Vite's own @import inliner resolves
`@import 'tailwindcss'` before @tailwindcss/postcss sees it, and reads it
as a relative file. svelte-check and Vitest never build CSS, so only the
image job caught it. The Vite plugin is Tailwind's documented setup for
Vite projects and handles the import itself, so postcss.config.cjs and
the direct postcss and @tailwindcss/postcss dependencies go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:55:48 -04:00
bvandeusenandClaude Opus 5.5 94a9c8cbe3 chore(deps): SvelteKit 3 with adapter-static 4 and TypeScript 6, full-tree npm audit (#5021)
Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps,
plus the migration they need. The mechanical part is `sv migrate
sveltekit-3`, run one task at a time and reviewed:

- svelte.config.js is gone. Its options move into sveltekit() in
  vite.config.ts, exported as kitOptions so vitest.config.ts runs the
  same kit setup, including the $test-utils alias the tests import.
- $lib becomes #lib through package.json "imports". There is no
  src/lib/index, so only the "#lib/*" entry is kept.
- tsconfig extends $app/tsconfig.
- Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite
  ^8.0.12, svelte-check ^4.7.5.

By hand, from the codemod's list of non-automated tasks:

- goto's replaceState option is now replace; keepFocus becomes
  reset: false. For the search typeahead, reset: false also stops the
  scroll-to-top, which is wanted while typing.
- The test setup mocks drop pushState/replaceState and $app/paths
  base/assets, which kit 3 removed, and mock refreshAll in place of
  invalidateAll.
- The other flagged files only read page.url or goto internal routes,
  so they needed no change.

TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares
typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to
7 once both accept it.

With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0,
so the web lane now audits every dependency rather than only what
ships to browsers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:50:39 -04:00
bvandeusen beba5b2082 Merge remote-tracking branch 'origin/renovate/sveltejs-kit-3.x' into dev 2026-10-08 10:49:44 -04:00
bvandeusenandClaude Opus 5.5 fecd030b68 chore(deps): Tailwind 4 (#5021)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m53s
release / integration (push) Successful in 5m37s
release / android (push) Successful in 6m54s
release / Build signed APK (releases and dev) (push) Successful in 7m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Failing after 34s
release / Verify release artifacts (tag releases only) (push) Skipped
Renovate's tailwindcss bump (PR #150) plus the migration it needs:

- Theme moves from tailwind.config.js into app.css as `@theme inline`,
  mapping the same FabledSword tokens. tailwind.config.js is gone.
- PostCSS runs @tailwindcss/postcss; autoprefixer is dropped, since
  Tailwind 4 prefixes through Lightning CSS.
- Class renames from @tailwindcss/upgrade 4.3.3, reviewed: outline-none
  -> outline-hidden, focus-visible:outline -> outline-solid, shadow ->
  shadow-sm, shadow-sm -> shadow-xs, flex-shrink-0 -> shrink-0. Bare
  `rounded` stays: v4 keeps it at 0.25rem, as before.
- Three v3 preflight defaults kept in a base layer so nothing changes on
  screen: gray-200 default border colour, gray-400 placeholder text and
  the pointer cursor on buttons.
- The unused class-based dark variant is not carried over; no template
  uses `dark:`.

Clears the five high and two moderate npm audit findings that came in
through Tailwind 3 (braces, chokidar, micromatch, fast-glob,
postcss-selector-parser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:46:07 -04:00
Renovate Bot 0500f6aac5 chore(deps): update dependency @sveltejs/kit to v3
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:45:01 +00:00
Renovate Bot c83933b61a chore(deps): update dependency @sveltejs/adapter-static to v4
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:45:00 +00:00
195 changed files with 1751 additions and 2146 deletions
+7 -6
View File
@@ -295,12 +295,13 @@ jobs:
- name: Install deps
run: npm ci
# What ships to browsers: `dependencies` and the runtime they pull in
# (svelte, devalue). Build and test tooling (vite, vitest, tailwind,
# kit's dev server) is left out because none of it reaches a user, and
# its open advisories need major-version upgrades tracked separately.
- name: npm audit (shipped dependencies)
run: npm audit --omit=dev --audit-level=moderate
# The whole tree, build and test tooling included. Until #5021 this
# audited only what ships to browsers (`--omit=dev`), because vite,
# vitest, tailwind and kit carried advisories that needed major
# upgrades. Those upgrades landed and the full tree audits clean, so
# the tooling that builds the shipped bundle is held to the same bar.
- name: npm audit (all dependencies)
run: npm audit --audit-level=moderate
- name: Type-check + svelte-check
run: npm run check
+1 -1
View File
@@ -94,7 +94,7 @@ Keystore, so a copy of the app's files doesn't yield a usable session.
Nothing is published unless every check passes: the Go, integration, web and
Android test suites, `govulncheck` against the toolchain that builds the
image, and `npm audit` on the packages that ship to the browser. See
image, and `npm audit` on every web dependency, build tooling included. See
`.gitea/workflows/release.yml`.
## Reporting a problem
+668 -1062
View File
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -12,25 +12,28 @@
"tokens": "node scripts/tokens-to-css.js"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/adapter-static": "^4.0.0",
"@sveltejs/kit": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@types/node": "^25.9.1",
"autoprefixer": "^10.5.0",
"jsdom": "^25.0.1",
"postcss": "^8.5.15",
"svelte": "^5.1.9",
"svelte-check": "^4.4.8",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.3",
"vite": "^8.0.0",
"svelte": "^5.57.1",
"svelte-check": "^4.7.5",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.0",
"vite": "^8.0.12",
"vitest": "^5.0.0"
},
"dependencies": {
"@neodrag/svelte": "^2.3.3",
"@tanstack/svelte-query": "^5.90.2",
"lucide-svelte": "^1.0.1"
},
"imports": {
"#lib/*": "./src/lib/*",
"#test-utils/*": "./src/test-utils/*"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+71 -4
View File
@@ -1,8 +1,75 @@
@import './lib/styles/tokens.generated.css';
@import './lib/styles/tokens.generated.css' layer(base);
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
/* The FabledSword tokens as Tailwind's theme. `inline` makes each utility emit
* the var(--fs-*) itself, so a theme swap that redefines the tokens reaches the
* utilities without going through a second variable. */
@theme inline {
--color-background: var(--fs-obsidian);
--color-surface: var(--fs-iron);
--color-surface-hover: var(--fs-slate);
--color-border: var(--fs-pewter);
--color-text-primary: var(--fs-parchment);
--color-text-secondary: var(--fs-vellum);
--color-text-muted: var(--fs-ash);
--color-action-primary: var(--fs-moss);
--color-action-secondary: var(--fs-bronze);
--color-action-destructive: var(--fs-oxblood);
--color-action-fg: var(--fs-on-action);
--color-accent: var(--fs-accent);
--color-accent-tint: color-mix(in srgb, var(--fs-accent) 12%, transparent);
--color-accent-fg: var(--fs-accent-fg);
--color-warning: var(--fs-warning);
--color-warning-fg: var(--fs-warning-fg);
--color-error: var(--fs-error);
--color-error-fg: var(--fs-error-fg);
--color-info: var(--fs-info);
--color-info-fg: var(--fs-info-fg);
--radius-sm: var(--fs-radius-sm);
--radius-md: var(--fs-radius-md);
--radius-lg: var(--fs-radius-lg);
--radius-xl: var(--fs-radius-xl);
--font-display: var(--fs-font-display);
--font-sans: var(--fs-font-body);
--font-mono: var(--fs-font-mono);
}
/* Tailwind 3's preflight defaults that 4 changed (#5021). Kept so the upgrade
* changes nothing on screen; each can go once the elements relying on it name
* the value themselves.
* - A border with no colour utility was gray-200; 4 makes it currentcolor.
* - Placeholder text was gray-400; 4 makes it the text colour at 50%.
* - Buttons had a pointer cursor; 4 leaves the browser default. */
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
input::placeholder,
textarea::placeholder {
color: var(--color-gray-400);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
:root {
color-scheme: dark;
+2 -2
View File
@@ -83,7 +83,7 @@ describe('api.get/post/del', () => {
describe('apiFetch 401 interceptor', () => {
test('401 response triggers auth.logout({silent:true}) and still throws', async () => {
const logoutSpy = vi.fn();
vi.doMock('$lib/auth/store.svelte', () => ({
vi.doMock('#lib/auth/store.svelte.js', () => ({
logout: logoutSpy,
login: vi.fn(),
bootstrap: vi.fn(),
@@ -97,6 +97,6 @@ describe('apiFetch 401 interceptor', () => {
status: 401
});
expect(logoutSpy).toHaveBeenCalledWith({ silent: true });
vi.doUnmock('$lib/auth/store.svelte');
vi.doUnmock('#lib/auth/store.svelte.js');
});
});
+1 -1
View File
@@ -44,7 +44,7 @@ export async function apiFetch(path: string, init?: RequestInit): Promise<unknow
// Lazy import: auth/store imports from this file, so a top-level
// import would be circular. By the time any 401 actually happens
// at runtime, both modules have finished loading.
const { logout } = await import('$lib/auth/store.svelte');
const { logout } = await import('#lib/auth/store.svelte.js');
await logout({ silent: true });
}
// Two error envelope shapes ship in this codebase today:
+4 -4
View File
@@ -1,10 +1,10 @@
import { api, type User, type LoginResponse } from '$lib/api/client';
import { putMyTimezone } from '$lib/api/me';
import { queryClient } from '$lib/query/client';
import { api, type User, type LoginResponse } from '#lib/api/client.js';
import { putMyTimezone } from '#lib/api/me.js';
import { queryClient } from '#lib/query/client.js';
import { signalSessionEnd } from './sessionEnd.svelte';
import { user, setUser } from './user.svelte';
// Re-export so existing `import { user } from '$lib/auth/store.svelte'`
// Re-export so existing `import { user } from '#lib/auth/store.svelte'`
// callers keep working. New code can import directly from auth/user.svelte.
export { user };
+4 -4
View File
@@ -1,14 +1,14 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { apiClientMock } from '../../test-utils/mocks/client';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('$lib/query/client', () => ({
vi.mock('#lib/query/client.js', () => ({
queryClient: { clear: vi.fn() }
}));
import { api } from '$lib/api/client';
import { queryClient } from '$lib/query/client';
import { api } from '#lib/api/client.js';
import { queryClient } from '#lib/query/client.js';
import { bootstrap, login, logout, user } from './store.svelte';
beforeEach(() => {
+1 -1
View File
@@ -4,7 +4,7 @@
// component that needs the current user can import `user` from either
// this module or auth/store — auth/store re-exports it for backward
// compatibility.
import type { User } from '$lib/api/client';
import type { User } from '#lib/api/client.js';
let _user = $state<User | null>(null);
+6 -6
View File
@@ -7,9 +7,9 @@
updateAcoustIDSettings,
type AcoustIDOverview,
type AcoustIDUnsettled
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// AcoustID recording-id lookup (M401): fills the MusicBrainz ids that
// untagged tracks lack, so the similarity arms can see them. Off until the
@@ -125,7 +125,7 @@
const rowClass = 'flex items-center justify-between gap-4 py-2';
const inputClass =
'rounded-md border border-border bg-background px-3 py-1.5 text-sm text-text-primary ' +
'placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent';
'placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent';
</script>
<section class="space-y-4 rounded-xl border border-border bg-surface p-5" data-testid="acoustid-card">
@@ -238,7 +238,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || !scoreOk}
onclick={save}
@@ -293,7 +293,7 @@
<button
type="button"
class="rounded-md border border-border px-4 py-2 text-sm text-text-primary hover:bg-background
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!overview.ready || overview.status.running || starting}
title={overview.ready ? undefined : 'Switch the lookup on and add a key first'}
+4 -4
View File
@@ -1,15 +1,15 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/svelte";
import type { AcoustIDOverview } from "$lib/api/admin";
import type { AcoustIDOverview } from "#lib/api/admin.js";
vi.mock("$lib/api/admin", () => ({
vi.mock("#lib/api/admin.js", () => ({
getAcoustID: vi.fn(),
updateAcoustIDSettings: vi.fn(),
runAcoustIDLookup: vi.fn(),
listUnsettledAcoustID: vi.fn(),
}));
vi.mock("$lib/stores/toast.svelte", () => ({ pushToast: vi.fn() }));
vi.mock("#lib/stores/toast.svelte.js", () => ({ pushToast: vi.fn() }));
import AcoustIDCard from "./AcoustIDCard.svelte";
import {
@@ -17,7 +17,7 @@ import {
listUnsettledAcoustID,
runAcoustIDLookup,
updateAcoustIDSettings,
} from "$lib/api/admin";
} from "#lib/api/admin.js";
const base: AcoustIDOverview = {
settings: { enabled: true, min_score: 0.85, api_key_set: true },
+5 -5
View File
@@ -6,9 +6,9 @@
revokeSession,
revokeOtherSessions,
type ActiveSession
} from '$lib/api/me';
import { errCode } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/me.js';
import { errCode } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Self-contained: nothing else in the app reads this data, so it holds its
// own state and reloads explicitly rather than joining the query cache.
@@ -143,7 +143,7 @@
<li class="flex items-start gap-3 py-3">
<MonitorSmartphone
size={18}
class="mt-0.5 flex-shrink-0 text-text-secondary"
class="mt-0.5 shrink-0 text-text-secondary"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
@@ -173,7 +173,7 @@
{#if !s.current}
<button
type="button"
class="flex-shrink-0 rounded border border-border px-2 py-1 text-sm
class="shrink-0 rounded border border-border px-2 py-1 text-sm
hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent
disabled:opacity-50"
disabled={busy}
@@ -6,13 +6,13 @@ const listSessions = vi.fn();
const revokeSession = vi.fn();
const revokeOtherSessions = vi.fn();
vi.mock('$lib/api/me', () => ({
vi.mock('#lib/api/me.js', () => ({
listSessions: (...a: unknown[]) => listSessions(...a),
revokeSession: (...a: unknown[]) => revokeSession(...a),
revokeOtherSessions: (...a: unknown[]) => revokeOtherSessions(...a)
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
type Row = {
id: string;
@@ -1,12 +1,12 @@
<script lang="ts">
import { Plus } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createPlaylistsQuery, appendTracks, createPlaylist } from '$lib/api/playlists';
import { qk } from '$lib/api/queries';
import { errMessage } from '$lib/api/errors';
import { createPlaylistsQuery, appendTracks, createPlaylist } from '#lib/api/playlists.js';
import { qk } from '#lib/api/queries.js';
import { errMessage } from '#lib/api/errors.js';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
tracks,
@@ -1,7 +1,7 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { readable } from 'svelte/store';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const playlistsData = vi.hoisted(() => ({
owned: [
@@ -43,11 +43,11 @@ const playlistsData = vi.hoisted(() => ({
public: [] as unknown[]
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u-self', username: 'me', is_admin: false } }
}));
vi.mock('$lib/api/playlists', () => ({
vi.mock('#lib/api/playlists.js', () => ({
createPlaylistsQuery: () =>
readable({ data: playlistsData, isPending: false, isError: false }),
appendTracks: vi.fn().mockResolvedValue({ id: 'p1', tracks: [] }),
@@ -86,7 +86,7 @@ describe('AddToPlaylistMenu', () => {
test('clicking a playlist appends and closes', async () => {
const onClose = vi.fn();
const { appendTracks } = await import('$lib/api/playlists');
const { appendTracks } = await import('#lib/api/playlists.js');
render(AddToPlaylistMenu, { props: { tracks: [track], onClose } });
await fireEvent.click(screen.getByRole('menuitem', { name: /A-list/ }));
await waitFor(() => expect(onClose).toHaveBeenCalled());
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/admin')
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type { AlbumRef, AlbumDetail } from '$lib/api/types';
import { api } from '$lib/api/client';
import { enqueueTracks, playQueue } from '$lib/player/store.svelte';
import type { AlbumRef, AlbumDetail } from '#lib/api/types.js';
import { api } from '#lib/api/client.js';
import { enqueueTracks, playQueue } from '#lib/player/store.svelte.js';
import { Play } from 'lucide-svelte';
import AlbumMenu from './AlbumMenu.svelte';
import CardActionCluster from './CardActionCluster.svelte';
@@ -31,7 +31,7 @@
>
<div
class="art-wrap relative aspect-square w-full overflow-hidden rounded-md
shadow-sm transition-all duration-150
shadow-xs transition-all duration-150
group-hover:shadow-lg group-hover:ring-1 group-hover:ring-accent/40"
>
<Cover
+10 -10
View File
@@ -1,27 +1,27 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { AlbumRef, AlbumDetail, TrackRef } from '$lib/api/types';
import { FALLBACK_COVER } from '$lib/media/covers';
import type { AlbumRef, AlbumDetail, TrackRef } from '#lib/api/types.js';
import { FALLBACK_COVER } from '#lib/media/covers.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { apiClientMock } from '../../test-utils/mocks/client';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn(),
playQueue: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/albums', async (orig) => {
vi.mock('#lib/api/albums.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return { ...actual, listAlbumTracks: vi.fn().mockResolvedValue([]) };
});
vi.mock('$lib/api/playlists', async (orig) => {
vi.mock('#lib/api/playlists.js', async (orig) => {
const { readable } = await import('svelte/store');
const actual = (await orig()) as Record<string, unknown>;
return {
@@ -34,8 +34,8 @@ vi.mock('$lib/api/playlists', async (orig) => {
});
import AlbumCard from './AlbumCard.svelte';
import { api } from '$lib/api/client';
import { enqueueTracks, playQueue } from '$lib/player/store.svelte';
import { api } from '#lib/api/client.js';
import { enqueueTracks, playQueue } from '#lib/player/store.svelte.js';
const album: AlbumRef = {
id: 'xyz',
+4 -4
View File
@@ -2,13 +2,13 @@
import { MoreVertical, Heart, HeartOff, ListMusic, Plus, Disc3 } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { listAlbumTracks } from '$lib/api/albums';
import { enqueueTracks } from '$lib/player/store.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { listAlbumTracks } from '#lib/api/albums.js';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
albumId,
+9 -9
View File
@@ -2,28 +2,28 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import type { TrackRef } from '$lib/api/types';
import { makeTracks } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/albums', async (orig) => {
vi.mock('#lib/api/albums.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return { ...actual, listAlbumTracks: vi.fn().mockResolvedValue([]) };
});
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn()
}));
import AlbumMenu from './AlbumMenu.svelte';
import { enqueueTracks } from '$lib/player/store.svelte';
import { listAlbumTracks } from '$lib/api/albums';
import { likeEntity } from '$lib/api/likes';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import { listAlbumTracks } from '#lib/api/albums.js';
import { likeEntity } from '#lib/api/likes.js';
import { goto } from '$app/navigation';
const TRACKS: TrackRef[] = makeTracks(2, {
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { ApiError } from '$lib/api/client';
import type { ApiError } from '#lib/api/client.js';
let {
error,
+4 -4
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import type { ArtistRef, TrackRef } from '$lib/api/types';
import { api } from '$lib/api/client';
import { playQueue, enqueueTracks } from '$lib/player/store.svelte';
import type { ArtistRef, TrackRef } from '#lib/api/types.js';
import { api } from '#lib/api/client.js';
import { playQueue, enqueueTracks } from '#lib/player/store.svelte.js';
import { Play } from 'lucide-svelte';
import ArtistMenu from './ArtistMenu.svelte';
import CardActionCluster from './CardActionCluster.svelte';
import Cover from './Cover.svelte';
import { listArtistTracks } from '$lib/api/artists';
import { listArtistTracks } from '#lib/api/artists.js';
let { artist }: { artist: ArtistRef } = $props();
+10 -10
View File
@@ -2,21 +2,21 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { apiClientMock } from '../../test-utils/mocks/client';
import type { ArtistRef, TrackRef } from '$lib/api/types';
import { makeTrack, makeTracks } from '$test-utils/fixtures/track';
import type { ArtistRef, TrackRef } from '#lib/api/types.js';
import { makeTrack, makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTracks: vi.fn()
}));
vi.mock('$lib/api/artists', () => ({
vi.mock('#lib/api/artists.js', () => ({
listArtistTracks: vi.fn().mockResolvedValue([])
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
import ArtistCard from './ArtistCard.svelte';
import { api } from '$lib/api/client';
import { playQueue } from '$lib/player/store.svelte';
import { api } from '#lib/api/client.js';
import { playQueue } from '#lib/player/store.svelte.js';
const artist: ArtistRef = {
id: 'art-1',
@@ -82,7 +82,7 @@ describe('ArtistCard', () => {
const fakeTracks = [
makeTrack({ artist_id: 'art-1', artist_name: 'BoC' })
];
const { listArtistTracks } = await import('$lib/api/artists');
const { listArtistTracks } = await import('#lib/api/artists.js');
(listArtistTracks as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(fakeTracks);
render(ArtistCard, { props: { artist } });
const btn = screen.getByRole('button', { name: /Add Boards of Canada to queue/i });
@@ -90,7 +90,7 @@ describe('ArtistCard', () => {
// Allow promise microtask to flush.
await new Promise((r) => setTimeout(r, 0));
expect(listArtistTracks).toHaveBeenCalledWith('art-1');
const { enqueueTracks } = await import('$lib/player/store.svelte');
const { enqueueTracks } = await import('#lib/player/store.svelte.js');
expect(enqueueTracks).toHaveBeenCalledWith(fakeTracks);
});
});
+4 -4
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import { MoreVertical, Heart, HeartOff, ListMusic, Plus } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { listArtistTracks } from '$lib/api/artists';
import { enqueueTracks } from '$lib/player/store.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { listArtistTracks } from '#lib/api/artists.js';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
artistId,
+9 -9
View File
@@ -2,27 +2,27 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import type { TrackRef } from '$lib/api/types';
import { makeTracks } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/artists', () => ({
vi.mock('#lib/api/artists.js', () => ({
listArtistTracks: vi.fn().mockResolvedValue([])
}));
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn()
}));
import ArtistMenu from './ArtistMenu.svelte';
import { enqueueTracks } from '$lib/player/store.svelte';
import { listArtistTracks } from '$lib/api/artists';
import { likeEntity, unlikeEntity } from '$lib/api/likes';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import { listArtistTracks } from '#lib/api/artists.js';
import { likeEntity, unlikeEntity } from '#lib/api/likes.js';
const TRACKS: TrackRef[] = makeTracks(2, {
album_id: 'al1', artist_id: 'a1', artist_name: 'Test'
@@ -1,8 +1,8 @@
<script lang="ts">
import { Plus } from 'lucide-svelte';
import type { TrackRef } from '$lib/api/types';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
import { coverUrl } from '$lib/media/covers';
import type { TrackRef } from '#lib/api/types.js';
import { playQueue, enqueueTrack } from '#lib/player/store.svelte.js';
import { coverUrl } from '#lib/media/covers.js';
import TrackMenu from './TrackMenu.svelte';
import LikeButton from './LikeButton.svelte';
import Cover from './Cover.svelte';
@@ -43,7 +43,7 @@
class="flex w-full items-center gap-2 rounded-md p-1 pr-24 text-left
hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent"
>
<div class="h-12 w-12 flex-shrink-0 overflow-hidden rounded bg-surface-hover">
<div class="h-12 w-12 shrink-0 overflow-hidden rounded bg-surface-hover">
<Cover src={cover} />
</div>
<div class="min-w-0 flex-1">
@@ -1,31 +1,31 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTracks } from '$test-utils/fixtures/track';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return { id: 'u1', username: 'u', is_admin: false }; } }
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTrack: vi.fn(),
playNext: vi.fn()
}));
import CompactTrackCard from './CompactTrackCard.svelte';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
import { playQueue, enqueueTrack } from '#lib/player/store.svelte.js';
const titles = ['First', 'Second', 'Third'];
const tracks: TrackRef[] = makeTracks(3, { artist_name: 'Artist' }).map((t, i) => ({
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { FALLBACK_COVER } from '$lib/media/covers';
import { FALLBACK_COVER } from '#lib/media/covers.js';
import { Disc3 } from 'lucide-svelte';
// Shared cover-artwork renderer. The web counterpart of Android's
@@ -86,7 +86,7 @@
<button
type="button"
aria-label={`Bring ${title} back`}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-accent"
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onUnsnooze?.()}
>
Undo
@@ -112,7 +112,7 @@
type="button"
aria-label={`Not right now — hide ${title} for a while`}
title="Not right now"
class="rounded-md border border-border p-1.5 text-text-secondary hover:bg-surface-hover hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
class="rounded-md border border-border p-1.5 text-text-secondary hover:bg-surface-hover hover:text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onSnooze?.()}
>
<Clock size={16} strokeWidth={1} />
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/discover')
@@ -5,9 +5,9 @@
getFingerprintSettings,
updateFingerprintSettings,
type FingerprintSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// How Minstrel fingerprints tracks and when it looks for duplicates (#3913).
// Lives on the Duplicates page, beside the results these settings shape.
@@ -83,7 +83,7 @@
const inputClass =
'mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm text-text-primary ' +
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent';
'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent';
</script>
<section class="space-y-4 rounded-xl border border-border bg-surface p-5">
@@ -183,7 +183,7 @@
class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary"
data-testid="length-warning"
>
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Saving re-fingerprints {libraryTotal > 0
? `all ${libraryTotal.toLocaleString()} tracks`
@@ -206,7 +206,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || problems.length > 0}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { FingerprintSettings } from '$lib/api/admin';
import { ERROR_COPY } from '$lib/api/error-copy';
import type { FingerprintSettings } from '#lib/api/admin.js';
import { ERROR_COPY } from '#lib/api/error-copy.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getFingerprintSettings: vi.fn(),
updateFingerprintSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import FingerprintSettingsCard from './FingerprintSettingsCard.svelte';
import { getFingerprintSettings, updateFingerprintSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getFingerprintSettings, updateFingerprintSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: FingerprintSettings = {
enabled: true,
+6 -6
View File
@@ -2,10 +2,10 @@
import { untrack } from 'svelte';
import { Flag } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { flagTrack } from '$lib/api/quarantine';
import { errCode } from '$lib/api/errors';
import { qk } from '$lib/api/queries';
import type { TrackRef, LidarrQuarantineReason } from '$lib/api/types';
import { flagTrack } from '#lib/api/quarantine.js';
import { errCode } from '#lib/api/errors.js';
import { qk } from '#lib/api/queries.js';
import type { TrackRef, LidarrQuarantineReason } from '#lib/api/types.js';
let {
track,
@@ -59,7 +59,7 @@
<span class="block text-xs text-text-secondary">Reason</span>
<select
bind:value={reason}
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
>
<option value="bad_rip">Bad rip</option>
<option value="wrong_file">Wrong file</option>
@@ -74,7 +74,7 @@
bind:value={notes}
maxlength="200"
placeholder="What's wrong with it?"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent"
rows="2"
></textarea>
</label>
+3 -3
View File
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import FlagPopover from './FlagPopover.svelte';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const invalidateMock = vi.fn();
vi.mock('@tanstack/svelte-query', async (orig) => {
@@ -12,11 +12,11 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
};
});
vi.mock('$lib/api/quarantine', () => ({
vi.mock('#lib/api/quarantine.js', () => ({
flagTrack: vi.fn().mockResolvedValue({ track_id: 't1', reason: 'bad_rip' })
}));
import { flagTrack } from '$lib/api/quarantine';
import { flagTrack } from '#lib/api/quarantine.js';
const track = makeTrack();
+5 -5
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type { HistoryEvent } from '$lib/api/history';
import { playQueue } from '$lib/player/store.svelte';
import { coverUrl } from '$lib/media/covers';
import type { HistoryEvent } from '#lib/api/history.js';
import { playQueue } from '#lib/player/store.svelte.js';
import { coverUrl } from '#lib/media/covers.js';
let { event } = $props<{ event: HistoryEvent }>();
@@ -49,7 +49,7 @@
<img
src={coverUrl(event.track.album_id)}
alt=""
class="h-11 w-11 flex-shrink-0 rounded object-cover"
class="h-11 w-11 shrink-0 rounded object-cover"
loading="lazy"
/>
<div class="min-w-0 flex-1">
@@ -58,7 +58,7 @@
{event.track.artist_name} · {event.track.album_title}
</div>
</div>
<span title={tooltip} class="flex-shrink-0 text-xs text-text-secondary">
<span title={tooltip} class="shrink-0 text-xs text-text-secondary">
{timestamp}
</span>
</button>
+3 -3
View File
@@ -1,12 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import HistoryRow from './HistoryRow.svelte';
import type { HistoryEvent } from '$lib/api/history';
import { makeTrack } from '$test-utils/fixtures/track';
import type { HistoryEvent } from '#lib/api/history.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
const playQueue = vi.fn();
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: (...args: unknown[]) => playQueue(...args)
}));
@@ -67,7 +67,7 @@
each shelf a clear chapter break without adding chrome. -->
<h2
class="font-display text-2xl font-medium text-text-primary
flex items-baseline gap-3 after:h-[2px] after:w-12 after:flex-shrink-0
flex items-baseline gap-3 after:h-[2px] after:w-12 after:shrink-0
after:rounded-full after:bg-accent/60 after:content-['']"
>{title}</h2>
{:else}
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity, type EntityKind } from '$lib/api/likes';
import { createLikedIdsQuery, likeEntity, unlikeEntity, type EntityKind } from '#lib/api/likes.js';
let {
entityType,
+2 -2
View File
@@ -6,14 +6,14 @@ const cacheState = vi.hoisted(() => ({
data: { track_ids: ['t1'] as string[], album_ids: [] as string[], artist_ids: [] as string[] }
}));
vi.mock('$lib/api/likes', () => ({
vi.mock('#lib/api/likes.js', () => ({
createLikedIdsQuery: () => readable({ data: cacheState.data, isPending: false, isError: false }),
likeEntity: vi.fn().mockResolvedValue(undefined),
unlikeEntity: vi.fn().mockResolvedValue(undefined)
}));
import LikeButton from './LikeButton.svelte';
import { likeEntity, unlikeEntity } from '$lib/api/likes';
import { likeEntity, unlikeEntity } from '#lib/api/likes.js';
afterEach(() => {
vi.clearAllMocks();
@@ -6,9 +6,9 @@
updateLoudnessSettings,
type LoudnessCoverage,
type LoudnessSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Loudness analysis (#4995): the background measurement that loudness
// normalization levels playback from, its progress, and its knobs. The
@@ -79,7 +79,7 @@
const inputClass =
'mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm text-text-primary ' +
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent';
'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent';
</script>
<section class="space-y-4 rounded border border-border bg-surface p-4">
@@ -185,7 +185,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || !valid}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { LoudnessCoverage, LoudnessSettings } from '$lib/api/admin';
import type { LoudnessCoverage, LoudnessSettings } from '#lib/api/admin.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getLoudnessSettings: vi.fn(),
updateLoudnessSettings: vi.fn(),
getLoudnessCoverage: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import LoudnessSettingsCard from './LoudnessSettingsCard.svelte';
import { getLoudnessCoverage, getLoudnessSettings, updateLoudnessSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getLoudnessCoverage, getLoudnessSettings, updateLoudnessSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: LoudnessSettings = { enabled: true, backfill_concurrency: 2, leveled_cache_mb: 2048 };
const coverage: LoudnessCoverage = {
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Smartphone } from 'lucide-svelte';
import { api } from '$lib/api/client';
import { api } from '#lib/api/client.js';
// Polls /api/client/version once on mount. Renders nothing on 404
// (no bundled APK — graceful degradation in dev / pre-CI images)
@@ -5,8 +5,8 @@
getNetworkSettings,
updateNetworkSettings,
type NetworkSettings
} from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let settings = $state<NetworkSettings | null>(null);
let hops = $state(1);
@@ -77,7 +77,7 @@
max={settings.max_hops}
bind:value={hops}
class="w-24 rounded border border-border bg-background px-2 py-1
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
<button
@@ -116,7 +116,7 @@
<div class="space-y-2 rounded border border-border bg-background p-3 text-sm">
<p class="flex items-start gap-2 text-text-secondary">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Count your proxies — don't guess high. This number tells Minstrel how much of the
<span class="font-mono">X-Forwarded-For</span> header to believe, and that header is
@@ -5,12 +5,12 @@ import NetworkSettingsCard from './NetworkSettingsCard.svelte';
const getNetworkSettings = vi.fn();
const updateNetworkSettings = vi.fn();
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNetworkSettings: () => getNetworkSettings(),
updateNetworkSettings: (hops: number) => updateNetworkSettings(hops)
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
function settings(over: Record<string, unknown> = {}) {
return {
@@ -4,13 +4,13 @@
NORMALIZATION_MODES,
NORMALIZATION_TARGETS,
type NormalizationPrefs
} from '$lib/api/normalization';
} from '#lib/api/normalization.js';
import {
normalization,
loadNormalization,
setNormalization
} from '$lib/stores/normalization.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/stores/normalization.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Volume leveling (M464 #4998). The choice is the user's, stored on the
// server, so it follows them to the Android app and to casts.
@@ -1,17 +1,17 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
vi.mock('$lib/api/normalization', async (importOriginal) => ({
...(await importOriginal<typeof import('$lib/api/normalization')>()),
vi.mock('#lib/api/normalization.js', async (importOriginal) => ({
...(await importOriginal<typeof import('#lib/api/normalization.js')>()),
getNormalization: vi.fn(),
putNormalization: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NormalizationSetting from './NormalizationSetting.svelte';
import { getNormalization, putNormalization } from '$lib/api/normalization';
import { normalization } from '$lib/stores/normalization.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNormalization, putNormalization } from '#lib/api/normalization.js';
import { normalization } from '#lib/stores/normalization.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
afterEach(() => {
vi.clearAllMocks();
@@ -9,9 +9,9 @@
markAllNotificationsRead,
markNotificationRead,
type NotificationItem
} from '$lib/api/notifications';
import { relativeTime } from '$lib/utils/relativeTime';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/notifications.js';
import { relativeTime } from '#lib/utils/relativeTime.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// The notifications inbox (M489): a bell in the header with an unread badge,
// opening a panel of what happened while you were away.
@@ -67,7 +67,7 @@
<svelte:window onclick={handleWindowClick} onkeydown={(e) => e.key === 'Escape' && (open = false)} />
<div class="relative flex-shrink-0">
<div class="relative shrink-0">
<button
bind:this={buttonRef}
type="button"
@@ -92,7 +92,7 @@
{#if open}
<div
bind:this={panelRef}
class="absolute right-0 z-50 mt-1 flex max-h-[70vh] w-[min(22rem,calc(100vw-2rem))] flex-col rounded border border-border bg-surface shadow"
class="absolute right-0 z-50 mt-1 flex max-h-[70vh] w-[min(22rem,calc(100vw-2rem))] flex-col rounded border border-border bg-surface shadow-sm"
role="dialog"
aria-label="Notifications"
>
@@ -123,7 +123,7 @@
onclick={() => openItem(item)}
>
<span
class="mt-1.5 h-2 w-2 flex-shrink-0 rounded-full {item.read_at ? '' : 'bg-text-primary'}"
class="mt-1.5 h-2 w-2 shrink-0 rounded-full {item.read_at ? '' : 'bg-text-primary'}"
aria-hidden="true"
></span>
<span class="min-w-0 flex-1">
@@ -8,7 +8,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) };
});
vi.mock('$lib/api/notifications', async (orig) => {
vi.mock('#lib/api/notifications.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
badgeLabel: actual.badgeLabel,
@@ -19,7 +19,7 @@ vi.mock('$lib/api/notifications', async (orig) => {
};
});
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NotificationBell from './NotificationBell.svelte';
import { goto } from '$app/navigation';
@@ -30,7 +30,7 @@ import {
markAllNotificationsRead,
markNotificationRead,
type NotificationItem
} from '$lib/api/notifications';
} from '#lib/api/notifications.js';
function item(over: Partial<NotificationItem> = {}): NotificationItem {
return {
@@ -4,9 +4,9 @@
getNotificationEmailSettings,
updateNotificationEmailSettings,
type NotificationEmailSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// When notification emails go out (M489 #5346). Nothing is emailed per
// event: new music is a daily summary, everything else a batch.
@@ -84,7 +84,7 @@
<select
bind:value={form.summary_hour}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
>
{#each hours as h (h)}
@@ -104,7 +104,7 @@
max="1440"
bind:value={form.batch_window_minutes}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -114,7 +114,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving}
onclick={save}
@@ -1,16 +1,16 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNotificationEmailSettings: vi.fn(),
updateNotificationEmailSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NotificationEmailCard from './NotificationEmailCard.svelte';
import { getNotificationEmailSettings, updateNotificationEmailSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNotificationEmailSettings, updateNotificationEmailSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
afterEach(() => vi.clearAllMocks());
@@ -8,10 +8,10 @@
type NotificationKindSetting,
type NotificationSettingChange,
type NotificationSettings
} from '$lib/api/notifications';
import { qk } from '$lib/api/queries';
import { user } from '$lib/auth/store.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/notifications.js';
import { qk } from '#lib/api/queries.js';
import { user } from '#lib/auth/store.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Per-user notification settings (M489): one row per kind, a toggle per
// channel. Toggles are optimistic; a failed save puts the switch back.
@@ -8,7 +8,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ setQueryData, invalidateQueries: vi.fn() }) };
});
vi.mock('$lib/api/notifications', async (orig) => {
vi.mock('#lib/api/notifications.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
...actual,
@@ -18,12 +18,12 @@ vi.mock('$lib/api/notifications', async (orig) => {
});
const userState = vi.hoisted(() => ({ current: { id: '1', username: 'alice', is_admin: false } }));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } }
}));
const pushToast = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: (...a: unknown[]) => pushToast(...a) }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: (...a: unknown[]) => pushToast(...a) }));
import NotificationSettings from './NotificationSettings.svelte';
import {
@@ -31,7 +31,7 @@ import {
putNotificationSettings,
type NotificationKindSetting,
type NotificationSettings as Settings
} from '$lib/api/notifications';
} from '#lib/api/notifications.js';
const requesterKinds: NotificationKindSetting[] = [
{ kind: 'request_approved', admin_only: false, inbox: true, phone: true, email: true },
+9 -9
View File
@@ -10,11 +10,11 @@
togglePlay, skipNext, skipPrev, seekTo, setVolume,
toggleShuffle, cycleRepeat, playQueue,
toggleQueueDrawer
} from '$lib/player/store.svelte';
import { formatDuration } from '$lib/media/duration';
import { FALLBACK_COVER, coverUrl } from '$lib/media/covers';
import { dominantColorFromUrl, rgbToCssString } from '$lib/media/dominantColor';
import { useSmoothPosition } from '$lib/player/smoothPosition.svelte';
} from '#lib/player/store.svelte.js';
import { formatDuration } from '#lib/media/duration.js';
import { FALLBACK_COVER, coverUrl } from '#lib/media/covers.js';
import { dominantColorFromUrl, rgbToCssString } from '#lib/media/dominantColor.js';
import { useSmoothPosition } from '#lib/player/smoothPosition.svelte.js';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
@@ -118,7 +118,7 @@
<a
href="/now-playing"
aria-label="Open now playing"
class="flex min-w-0 items-center gap-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="flex min-w-0 items-center gap-2 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
style="flex: 1 1 35%;"
>
<img
@@ -153,7 +153,7 @@
type="button"
aria-label={player.isPlaying ? 'Pause' : 'Play'}
onclick={togglePlay}
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow-sm transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
>
{#if player.state === 'loading' || player.state === 'idle'}
<Loader2 data-testid="play-spinner" size={22} strokeWidth={1.5} class="animate-spin" />
@@ -281,7 +281,7 @@
<a
href="/now-playing"
aria-label="Open now playing"
class="shrink-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="shrink-0 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
>
<img
src={coverUrl(current.album_id)}
@@ -341,7 +341,7 @@
type="button"
aria-label={player.isPlaying ? 'Pause' : 'Play'}
onclick={togglePlay}
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow-sm transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
>
{#if player.state === 'loading' || player.state === 'idle'}
<Loader2 size={24} strokeWidth={1.5} class="animate-spin" />
+6 -6
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, within } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// Mutable state the mocked store reads from.
const state = vi.hoisted(() => ({
@@ -20,7 +20,7 @@ const state = vi.hoisted(() => ({
queueDrawerOpen: false
}));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
player: {
get queue() { return state.queue; },
get index() { return state.index; },
@@ -46,16 +46,16 @@ vi.mock('$lib/player/store.svelte', () => ({
toggleQueueDrawer: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
import PlayerBar from './PlayerBar.svelte';
import {
togglePlay, skipNext, skipPrev, seekTo, setVolume,
toggleShuffle, cycleRepeat, playQueue,
toggleQueueDrawer
} from '$lib/player/store.svelte';
} from '#lib/player/store.svelte.js';
function track(): TrackRef {
return makeTrack({
+11 -11
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { Play, MoreVertical, RefreshCcw } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import type { Playlist, PlaylistTrack, TrackRef } from '$lib/api/types';
import { user } from '$lib/auth/store.svelte';
import { getPlaylist, systemShuffle, refreshSystem } from '$lib/api/playlists';
import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef';
import { systemPlaylistRefetch } from '$lib/playlists/systemRefetch';
import { errCode } from '$lib/api/errors';
import { qk } from '$lib/api/queries';
import { playQueue } from '$lib/player/store.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
import type { Playlist, PlaylistTrack, TrackRef } from '#lib/api/types.js';
import { user } from '#lib/auth/store.svelte.js';
import { getPlaylist, systemShuffle, refreshSystem } from '#lib/api/playlists.js';
import { playlistTrackToRef } from '#lib/playlists/playlistTrackToRef.js';
import { systemPlaylistRefetch } from '#lib/playlists/systemRefetch.js';
import { errCode } from '#lib/api/errors.js';
import { qk } from '#lib/api/queries.js';
import { playQueue } from '#lib/player/store.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let { playlist }: { playlist: Playlist } = $props();
@@ -184,7 +184,7 @@
>
<div
class="art-wrap relative aspect-square w-full overflow-hidden rounded-md
bg-surface-hover shadow-sm transition-all duration-150
bg-surface-hover shadow-xs transition-all duration-150
group-hover:shadow-lg group-hover:ring-1 group-hover:ring-accent/40"
>
{#if playlist.cover_url}
@@ -203,7 +203,7 @@
is often a 2x2 collage; the gradient mutes the collage so
the playlist NAME becomes the dominant element. -->
<div
class="pointer-events-none absolute inset-0 bg-gradient-to-t
class="pointer-events-none absolute inset-0 bg-linear-to-t
from-black/85 via-black/40 to-transparent"
></div>
<!-- Title burned in at the bottom of the cover. Two lines max;
+15 -15
View File
@@ -1,13 +1,13 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import PlaylistCard from './PlaylistCard.svelte';
import type { Playlist, PlaylistDetail } from '$lib/api/types';
import type { Playlist, PlaylistDetail } from '#lib/api/types.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u-self', username: 'me', is_admin: false } }
}));
vi.mock('$lib/api/playlists', () => ({
vi.mock('#lib/api/playlists.js', () => ({
getPlaylist: vi.fn().mockResolvedValue({
id: 'p-1',
user_id: 'u-self',
@@ -75,11 +75,11 @@ vi.mock('$lib/api/playlists', () => ({
refreshSystem: vi.fn().mockResolvedValue({ playlist_id: 'p-1', track_count: 5, track_ids: ['t-1'] })
}));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn()
}));
vi.mock('$lib/api/queries', () => ({
vi.mock('#lib/api/queries.js', () => ({
qk: {
playlist: (id: string) => ['playlist', id],
playlists: (kind?: string) => ['playlists', { kind: kind ?? 'user' }]
@@ -162,8 +162,8 @@ describe('PlaylistCard', () => {
});
test('play button calls playQueue with tracks when clicked', async () => {
const { getPlaylist } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
@@ -222,7 +222,7 @@ describe('PlaylistCard', () => {
});
test('clicking Refresh calls refreshSystem with the raw variant', async () => {
const { refreshSystem } = await import('$lib/api/playlists');
const { refreshSystem } = await import('#lib/api/playlists.js');
const discoverPlaylist: Playlist = {
...base,
kind: 'system',
@@ -237,8 +237,8 @@ describe('PlaylistCard', () => {
});
test('For-You play calls systemShuffle, not getPlaylist', async () => {
const { getPlaylist, systemShuffle } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist, systemShuffle } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
const forYouPlaylist: Playlist = {
...base,
kind: 'system',
@@ -261,7 +261,7 @@ describe('PlaylistCard', () => {
});
test('For-You play passes source:for_you to playQueue (no client shuffle)', async () => {
const { playQueue } = await import('$lib/player/store.svelte');
const { playQueue } = await import('#lib/player/store.svelte.js');
const forYouPlaylist: Playlist = {
...base,
kind: 'system',
@@ -281,8 +281,8 @@ describe('PlaylistCard', () => {
});
test('user-playlist play uses getPlaylist + plain playQueue (no source)', async () => {
const { getPlaylist, systemShuffle } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist, systemShuffle } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
await fireEvent.click(playButton);
@@ -294,8 +294,8 @@ describe('PlaylistCard', () => {
});
test('user-playlist play does not refresh anything', async () => {
const { refreshSystem, getPlaylist } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { refreshSystem, getPlaylist } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
@@ -3,9 +3,9 @@
import { draggable, type DragEventData } from '@neodrag/svelte';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
import type { PlaylistTrack } from '$lib/api/types';
import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef';
import { player } from '$lib/player/store.svelte';
import type { PlaylistTrack } from '#lib/api/types.js';
import { playlistTrackToRef } from '#lib/playlists/playlistTrackToRef.js';
import { player } from '#lib/player/store.svelte.js';
import { offsetToDelta } from './queue-row-math';
let {
@@ -103,8 +103,8 @@
aria-label="Reorder track (drag or use arrow keys)"
aria-keyshortcuts="ArrowUp ArrowDown"
onkeydown={handleHandleKeydown}
class="flex-shrink-0 cursor-grab text-text-muted hover:text-text-primary
active:cursor-grabbing focus-visible:outline focus-visible:outline-2
class="shrink-0 cursor-grab text-text-muted hover:text-text-primary
active:cursor-grabbing focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50"
>
<GripVertical size={14} strokeWidth={1} />
@@ -129,7 +129,7 @@
</div>
</button>
<span class="flex-shrink-0 text-xs text-text-muted">{format(row.duration_sec)}</span>
<span class="shrink-0 text-xs text-text-muted">{format(row.duration_sec)}</span>
{#if !isUnavailable && liveTrack}
<LikeButton entityType="track" entityId={liveTrack.id} />
@@ -141,7 +141,7 @@
type="button"
onclick={() => onRemove(row.position)}
aria-label={`Remove ${row.title} from playlist`}
class="flex-shrink-0 rounded p-1 text-text-muted hover:bg-surface hover:text-action-destructive"
class="shrink-0 rounded p-1 text-text-muted hover:bg-surface hover:text-action-destructive"
>
<X size={14} strokeWidth={1} />
</button>
@@ -2,14 +2,14 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import type { PlaylistTrack } from '$lib/api/types';
import type { PlaylistTrack } from '#lib/api/types.js';
// LikeButton + TrackMenu transitively pull in TanStack Query, the auth
// store, likes/quarantine APIs, the player store, and SvelteKit
// navigation. Mock all of them at module level so the component can be
// rendered in isolation.
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u1', username: 'me', is_admin: false } }
}));
@@ -17,11 +17,11 @@ vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playQueue: vi.fn(),
@@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Save, TriangleAlert } from 'lucide-svelte';
import { getNetworkSettings, updatePublicUrl } from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNetworkSettings, updatePublicUrl } from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let saved = $state<string | null>(null);
let value = $state('');
@@ -68,7 +68,7 @@
placeholder="https://music.example.com"
bind:value
class="w-full rounded border border-border bg-background px-2 py-1 font-mono
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
{#if here && value.trim() !== here}
@@ -96,7 +96,7 @@
{#if !saved}
<p class="flex items-start gap-2 text-sm text-text-secondary" role="status">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Not set, so password-reset emails are not being sent. Minstrel won't build the link from
whatever address a request claims, because anyone can claim any address.
@@ -5,13 +5,13 @@ import PublicAddressCard from './PublicAddressCard.svelte';
const getNetworkSettings = vi.fn();
const updatePublicUrl = vi.fn();
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNetworkSettings: () => getNetworkSettings(),
updatePublicUrl: (url: string) => updatePublicUrl(url)
}));
const pushToast = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({
vi.mock('#lib/stores/toast.svelte.js', () => ({
pushToast: (...args: unknown[]) => pushToast(...args)
}));
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { player, closeQueueDrawer } from '$lib/player/store.svelte';
import { player, closeQueueDrawer } from '#lib/player/store.svelte.js';
import QueueList from './QueueList.svelte';
let previouslyFocused: HTMLElement | null = null;
+4 -4
View File
@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
// QueueTrackRow (rendered inside QueueDrawer → QueueList) now renders a
// LikeButton, which reads createLikedIdsQuery. Stub the likes API so the
@@ -10,14 +10,14 @@ import { makeTrack } from '$test-utils/fixtures/track';
// emptyLikesMock import — must precede the QueueDrawer import below, since
// loading the component evaluates the mocked modules and the hoisted
// factories need their referenced bindings already initialized.
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
const closeQueueDrawer = vi.fn();
let queueValue: TrackRef[] = [];
let indexValue = 0;
let openValue = true;
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
closeQueueDrawer: (...args: unknown[]) => closeQueueDrawer(...args),
player: {
get queue() { return queueValue; },
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { untrack } from 'svelte';
import { X, Trash2, ArrowDown } from 'lucide-svelte';
import { player, clearQueue } from '$lib/player/store.svelte';
import { player, clearQueue } from '#lib/player/store.svelte.js';
import QueueTrackRow from './QueueTrackRow.svelte';
// onClose: when provided, renders an X button in the header so the
+7 -7
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { GripVertical, X } from 'lucide-svelte';
import { draggable, type DragEventData } from '@neodrag/svelte';
import type { TrackRef } from '$lib/api/types';
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '$lib/player/store.svelte';
import { coverUrl, FALLBACK_COVER } from '$lib/media/covers';
import type { TrackRef } from '#lib/api/types.js';
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '#lib/player/store.svelte.js';
import { coverUrl, FALLBACK_COVER } from '#lib/media/covers.js';
import { offsetToDelta } from './queue-row-math';
import LikeButton from './LikeButton.svelte';
@@ -71,7 +71,7 @@
hover/focus so the artwork stays legible the rest of the time; the drop
shadow is what keeps the glyph readable over pale covers without one.
-->
<div class="relative h-10 w-10 flex-shrink-0">
<div class="relative h-10 w-10 shrink-0">
<img
src={coverUrl(track.album_id)}
alt=""
@@ -86,7 +86,7 @@
class="group absolute inset-0 flex cursor-grab items-center justify-center rounded
text-white/70 transition hover:bg-black/45 hover:text-white
focus-visible:bg-black/45 focus-visible:text-white
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
style="filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.9))"
>
<GripVertical size={16} />
@@ -101,7 +101,7 @@
>
<div class="flex items-center gap-2 min-w-0">
{#if isCurrent}
<span class="text-xs text-accent-fg uppercase tracking-wide flex-shrink-0">Now playing</span>
<span class="text-xs text-accent-fg uppercase tracking-wide shrink-0">Now playing</span>
{/if}
<span class="font-medium truncate text-text-primary">{track.title}</span>
</div>
@@ -114,7 +114,7 @@
type="button"
onclick={handleRemove}
aria-label="Remove from queue"
class="text-text-secondary hover:text-text-primary flex-shrink-0"
class="text-text-secondary hover:text-text-primary shrink-0"
>
<X size={16} />
</button>
+3 -3
View File
@@ -1,14 +1,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// QueueTrackRow now renders a LikeButton, which reads createLikedIdsQuery.
// Stub the likes API so the row doesn't need a real QueryClient in context.
// The mock (and its emptyLikesMock import) must precede the component import
// below: importing QueueTrackRow transitively loads LikeButton → the mocked
// module, and the hoisted factory needs emptyLikesMock already initialized.
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
import QueueTrackRow from './QueueTrackRow.svelte';
@@ -16,7 +16,7 @@ const playFromQueueIndex = vi.fn();
const removeFromQueue = vi.fn();
const moveQueueItem = vi.fn();
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playFromQueueIndex: (...args: unknown[]) => playFromQueueIndex(...args),
removeFromQueue: (...args: unknown[]) => removeFromQueue(...args),
moveQueueItem: (...args: unknown[]) => moveQueueItem(...args)
+1 -1
View File
@@ -58,7 +58,7 @@
oninput={onInput}
onkeydown={onKey}
class="w-full rounded border border-border bg-surface pl-7 pr-7 py-1 text-sm
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
{#if raw}
<button
@@ -5,9 +5,9 @@
getReacquisitionSettings,
updateReacquisitionSettings,
type ReacquisitionSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Policy for turning a missing file back into a Lidarr request
// (milestone #290). Lives on the missing-files page rather than under
@@ -127,7 +127,7 @@
max="720"
bind:value={form.grace_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -144,7 +144,7 @@
max="10"
bind:value={form.max_attempts}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -157,7 +157,7 @@
max="168"
bind:value={form.backoff_base_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -170,7 +170,7 @@
max="720"
bind:value={form.backoff_max_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -186,7 +186,7 @@
max="200"
bind:value={form.max_per_pass}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -213,7 +213,7 @@
watching those rows never get a request would reasonably conclude
the feature is broken. -->
<p class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
{saved.unnameable_albums}
{saved.unnameable_albums === 1 ? 'album has' : 'albums have'} missing files but no
@@ -227,7 +227,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { ReacquisitionSettings } from '$lib/api/admin';
import { ERROR_COPY } from '$lib/api/error-copy';
import type { ReacquisitionSettings } from '#lib/api/admin.js';
import { ERROR_COPY } from '#lib/api/error-copy.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getReacquisitionSettings: vi.fn(),
updateReacquisitionSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import ReacquisitionSettingsCard from './ReacquisitionSettingsCard.svelte';
import { getReacquisitionSettings, updateReacquisitionSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getReacquisitionSettings, updateReacquisitionSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: ReacquisitionSettings = {
enabled: true,
+7 -7
View File
@@ -18,16 +18,16 @@
function navigate(next: string) {
const onSearchPage = page.url.pathname.startsWith('/search');
const trimmed = next.trim();
// keepFocus: SvelteKit blurs the active element on navigation by default.
// For a typeahead-style URL update we want the user to keep typing
// without re-clicking the input every 250 ms.
// reset: false: SvelteKit blurs the active element and scrolls to the top
// on navigation by default. For a typeahead-style URL update we want the
// user to keep typing without re-clicking the input every 250 ms.
if (trimmed === '') {
if (onSearchPage) goto('/search', { replaceState: true, keepFocus: true });
if (onSearchPage) goto('/search', { replace: true, reset: false });
return;
}
goto(`/search?q=${encodeURIComponent(trimmed)}`, {
replaceState: onSearchPage,
keepFocus: true,
replace: onSearchPage,
reset: false,
});
}
@@ -66,6 +66,6 @@
bind:value
oninput={onInput}
onkeydown={onKey}
class="w-full rounded border border-border bg-surface pl-7 pr-3 py-1 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="w-full rounded border border-border bg-surface pl-7 pr-3 py-1 text-sm focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
+7 -7
View File
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/')
@@ -40,7 +40,7 @@ describe('SearchInput', () => {
expect(goto).not.toHaveBeenCalled();
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/search?q=h', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=h', { replace: false, reset: false });
});
test('rapid keystrokes debounce to a single goto', async () => {
@@ -53,16 +53,16 @@ describe('SearchInput', () => {
await fireEvent.input(input, { target: { value: 'hel' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replace: false, reset: false });
});
test('typing while on /search uses replaceState=true', async () => {
test('typing while on /search uses replace=true', async () => {
state.pageUrl = new URL('http://localhost/search?q=h');
render(SearchInput);
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: 'he' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search?q=he', { replaceState: true, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=he', { replace: true, reset: false });
});
test('clearing input on /search calls goto("/search") (drops ?q=)', async () => {
@@ -71,7 +71,7 @@ describe('SearchInput', () => {
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: '' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search', { replace: true, reset: false });
});
test('Escape clears the input value', async () => {
@@ -88,6 +88,6 @@ describe('SearchInput', () => {
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: 'a b&c' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replace: false, reset: false });
});
});
+3 -3
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { X, ListPlus, Plus, ListMusic, Heart } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { selection, clearSelection } from '$lib/selection/store.svelte';
import { playNextMany, enqueueTracks } from '$lib/player/store.svelte';
import { likeEntity, createLikedIdsQuery } from '$lib/api/likes';
import { selection, clearSelection } from '#lib/selection/store.svelte.js';
import { playNextMany, enqueueTracks } from '#lib/player/store.svelte.js';
import { likeEntity, createLikedIdsQuery } from '#lib/api/likes.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
const queryClient = useQueryClient();
+7 -7
View File
@@ -2,9 +2,9 @@
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { ChevronDown, Compass, House, LibraryBig } from 'lucide-svelte';
import { user, logout } from '$lib/auth/store.svelte';
import { player } from '$lib/player/store.svelte';
import { appName } from '$lib/branding';
import { user, logout } from '#lib/auth/store.svelte.js';
import { player } from '#lib/player/store.svelte.js';
import { appName } from '#lib/branding.js';
import MinstrelMark from './MinstrelMark.svelte';
import NotificationBell from './NotificationBell.svelte';
import PlayerBar from './PlayerBar.svelte';
@@ -35,7 +35,7 @@
async function handleLogout() {
menuOpen = false;
await logout();
goto('/login', { replaceState: true });
goto('/login', { replace: true });
}
function isActive(prefix: string): boolean {
@@ -94,13 +94,13 @@
</nav>
<div class="flex items-center gap-1 md:gap-3 justify-self-end min-w-0">
<div class="hidden md:block min-w-0 max-w-xs flex-shrink">
<div class="hidden md:block min-w-0 max-w-xs shrink">
<SearchInput />
</div>
<NotificationBell />
<div class="relative flex-shrink-0">
<div class="relative shrink-0">
<button
bind:this={menuButtonRef}
type="button"
@@ -120,7 +120,7 @@
{#if menuOpen}
<div
bind:this={menuRef}
class="absolute right-0 z-50 mt-1 w-40 rounded border border-border bg-surface shadow"
class="absolute right-0 z-50 mt-1 w-40 rounded border border-border bg-surface shadow-sm"
role="menu"
tabindex="-1"
>
+4 -4
View File
@@ -19,14 +19,14 @@ const userState = vi.hoisted(() => ({
} | null
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } },
logout: vi.fn().mockResolvedValue(undefined)
}));
// The bell's queries need a QueryClient the Shell tests do not provide; its own
// behaviour is covered in NotificationBell.test.ts.
vi.mock('$lib/api/notifications', async () => {
vi.mock('#lib/api/notifications.js', async () => {
const { mockQuery } = await import('../../test-utils/query');
return {
createUnreadCountQuery: () => mockQuery({ data: 0 }),
@@ -38,7 +38,7 @@ vi.mock('$lib/api/notifications', async () => {
});
import Shell from './Shell.svelte';
import { logout } from '$lib/auth/store.svelte';
import { logout } from '#lib/auth/store.svelte.js';
import { goto } from '$app/navigation';
afterEach(() => {
@@ -110,6 +110,6 @@ describe('Shell', () => {
await fireEvent.click(screen.getByRole('button', { name: /alice/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /log out/i }));
expect(logout).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/login', { replaceState: true });
expect(goto).toHaveBeenCalledWith('/login', { replace: true });
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts" module>
import type { LidarrRequestStatus } from '$lib/api/types';
import type { LidarrRequestStatus } from '#lib/api/types.js';
type Tone = 'warning' | 'info' | 'success' | 'error';
+6 -6
View File
@@ -5,12 +5,12 @@
createSnoozesQuery,
snoozeSuggestion,
unsnoozeSuggestion
} from '$lib/api/suggestions';
import { createRequest } from '$lib/api/requests';
import { qk } from '$lib/api/queries';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/suggestions.js';
import { createRequest } from '#lib/api/requests.js';
import { qk } from '#lib/api/queries.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import DiscoverResultCard from './DiscoverResultCard.svelte';
import type { ArtistSuggestion, SeedContribution, SuggestionSnooze } from '$lib/api/types';
import type { ArtistSuggestion, SeedContribution, SuggestionSnooze } from '#lib/api/types.js';
const client = useQueryClient();
const queryStore = createSuggestionsQuery();
@@ -199,7 +199,7 @@
<button
type="button"
aria-label={`Bring ${snoozed.name} back now`}
class="shrink-0 rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-accent"
class="shrink-0 rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onUnsnooze(snoozed.mbid, snoozed.name)}
>
Bring back
@@ -8,19 +8,19 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) };
});
vi.mock('$lib/api/suggestions', () => ({
vi.mock('#lib/api/suggestions.js', () => ({
createSuggestionsQuery: vi.fn(),
createSnoozesQuery: vi.fn(),
snoozeSuggestion: vi.fn().mockResolvedValue(undefined),
unsnoozeSuggestion: vi.fn().mockResolvedValue(undefined)
}));
vi.mock('$lib/api/requests', () => ({
vi.mock('#lib/api/requests.js', () => ({
createRequest: vi.fn().mockResolvedValue({})
}));
const pushToastMock = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({
vi.mock('#lib/stores/toast.svelte.js', () => ({
pushToast: (...args: unknown[]) => pushToastMock(...args)
}));
@@ -30,9 +30,9 @@ import {
createSnoozesQuery,
snoozeSuggestion,
unsnoozeSuggestion
} from '$lib/api/suggestions';
import { createRequest } from '$lib/api/requests';
import type { ArtistSuggestion, SuggestionSnooze } from '$lib/api/types';
} from '#lib/api/suggestions.js';
import { createRequest } from '#lib/api/requests.js';
import type { ArtistSuggestion, SuggestionSnooze } from '#lib/api/types.js';
const oneSeed: ArtistSuggestion = {
mbid: 'mb1',
+2 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts">
// Single global toast renderer. Mounted once in +layout.svelte; pages call
// pushToast() from $lib/stores/toast.svelte. The {#key} re-mount on id
// pushToast() from #lib/stores/toast.svelte. The {#key} re-mount on id
// change is what makes screen readers re-announce a replacement toast.
import { toast } from '$lib/stores/toast.svelte';
import { toast } from '#lib/stores/toast.svelte.js';
</script>
{#if toast.value}
+5 -5
View File
@@ -18,11 +18,11 @@
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { createMyQuarantineQuery, unflagTrack } from '$lib/api/quarantine';
import { qk } from '$lib/api/queries';
import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import type { TrackRef } from '$lib/api/types';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { createMyQuarantineQuery, unflagTrack } from '#lib/api/quarantine.js';
import { qk } from '#lib/api/queries.js';
import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import type { TrackRef } from '#lib/api/types.js';
let {
track,
+7 -7
View File
@@ -3,7 +3,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// Mutable mock-user handle so individual tests can flip is_admin / null
// without re-importing the module under test.
@@ -13,7 +13,7 @@ const userState = vi.hoisted(() => ({
| null
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } }
}));
@@ -21,20 +21,20 @@ vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn()
}));
import TrackMenu from './TrackMenu.svelte';
import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
const track = makeTrack({ title: 'Roygbiv' });
+5 -5
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import type { TrackRef } from '$lib/api/types';
import { formatDuration } from '$lib/media/duration';
import { player, playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import type { TrackRef } from '#lib/api/types.js';
import { formatDuration } from '#lib/media/duration.js';
import { player, playQueue, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import { Plus, Check } from 'lucide-svelte';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
import {
selection, toggleOne, selectRange
} from '$lib/selection/store.svelte';
} from '#lib/selection/store.svelte.js';
type PlayHandler = (tracks: TrackRef[], index: number) => void;
@@ -76,7 +76,7 @@
data-selected={isSelected}
onclick={onRowClick}
onkeydown={onRowKey}
class="group grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent {isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : ''} {isSelected ? 'bg-surface-hover' : ''}"
class="group grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent {isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : ''} {isSelected ? 'bg-surface-hover' : ''}"
>
<span class="relative flex h-6 w-8 items-center justify-end">
<!-- Track number: visible by default; hidden when the row is hovered
+7 -7
View File
@@ -1,24 +1,24 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn(),
player: { current: undefined }
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
import TrackRow from './TrackRow.svelte';
import { playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import { selection, clearSelection } from '$lib/selection/store.svelte';
import { playQueue, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import { selection, clearSelection } from '#lib/selection/store.svelte.js';
const tracks: TrackRef[] = [
makeTrack({ title: 'So What', duration_sec: 545 }),
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { audioLoader, setAudioLoader, __resetAudioLoader } from './audioLoader';
const sampleTrack: TrackRef = {
+1 -1
View File
@@ -9,7 +9,7 @@
// no-op (the browser handles it via the prefetch <audio> element);
// Tauri can warm its native cache here.
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
export type AudioLoader = {
resolve: (track: TrackRef) => string;
+4 -4
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { flushSync } from 'svelte';
vi.mock('$lib/api/client', () => ({
vi.mock('#lib/api/client.js', () => ({
api: {
post: vi.fn().mockResolvedValue({ play_event_id: 'pe-1', session_id: 's-1' })
}
@@ -19,9 +19,9 @@ import {
reportDuration,
skipNext,
} from './store.svelte';
import { api } from '$lib/api/client';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import { api } from '#lib/api/client.js';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string, dur = 200): TrackRef {
return makeTrack({
+2 -2
View File
@@ -1,7 +1,7 @@
import { player } from './store.svelte';
import { api } from '$lib/api/client';
import { api } from '#lib/api/client.js';
import { getOrCreateClientId } from './clientId';
import type { PlayStartedResponse, EventOkResponse } from '$lib/api/types';
import type { PlayStartedResponse, EventOkResponse } from '#lib/api/types.js';
// useEventsDispatcher installs $effect-based watchers that emit play events
// to /api/events. Call once from +layout.svelte's mount.
+3 -3
View File
@@ -1,8 +1,8 @@
import { describe, expect, test } from 'vitest';
import { dbToLinear, gainDb, playingAsAlbum, MAX_BOOST_DB } from './gain';
import type { NormalizationPrefs } from '$lib/api/normalization';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { NormalizationPrefs } from '#lib/api/normalization.js';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
const prefs = (over: Partial<NormalizationPrefs> = {}): NormalizationPrefs => ({
mode: 'auto',
+2 -2
View File
@@ -1,8 +1,8 @@
// Loudness normalization math for the web player (M464 #4999). Pure, so the
// rules are tested without an <audio> element or an AudioContext.
import type { NormalizationPrefs, ReplayGain } from '$lib/api/normalization';
import type { TrackRef } from '$lib/api/types';
import type { NormalizationPrefs, ReplayGain } from '#lib/api/normalization.js';
import type { TrackRef } from '#lib/api/types.js';
// The server's gains are to the ReplayGain 2.0 reference.
const REFERENCE_LUFS = -18;
+5 -5
View File
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: {
get value() {
return { id: 'test-user' };
@@ -11,8 +11,8 @@ vi.mock('$lib/auth/store.svelte', () => ({
}));
const getReplayGain = vi.fn();
vi.mock('$lib/api/normalization', async (orig) => ({
...(await orig<typeof import('$lib/api/normalization')>()),
vi.mock('#lib/api/normalization.js', async (orig) => ({
...(await orig<typeof import('#lib/api/normalization.js')>()),
getReplayGain: (ids: string[]) => getReplayGain(ids)
}));
+2 -2
View File
@@ -10,8 +10,8 @@
// what the track sounded like before leveling existed.
import { SvelteMap } from 'svelte/reactivity';
import { getReplayGain, type ReplayGain } from '$lib/api/normalization';
import { normalization } from '$lib/stores/normalization.svelte';
import { getReplayGain, type ReplayGain } from '#lib/api/normalization.js';
import { normalization } from '#lib/stores/normalization.svelte.js';
import { player } from './store.svelte';
import { dbToLinear, gainDb, playingAsAlbum } from './gain';
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { flushSync } from 'svelte';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string): TrackRef {
return makeTrack({
+1 -1
View File
@@ -1,4 +1,4 @@
import { coverUrl } from '$lib/media/covers';
import { coverUrl } from '#lib/media/covers.js';
import {
player,
togglePlay,
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const sampleTrack = makeTrack();
+1 -1
View File
@@ -1,4 +1,4 @@
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
export type PersistedPlayerState = {
v: 1;
+2 -2
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { flushSync } from 'svelte';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string): TrackRef {
return makeTrack({
+5 -5
View File
@@ -1,9 +1,9 @@
import type { TrackRef, RadioResponse } from '$lib/api/types';
import type { TrackRef, RadioResponse } from '#lib/api/types.js';
import { untrack } from 'svelte';
import { api } from '$lib/api/client';
import { user } from '$lib/auth/user.svelte';
import { sessionEnd } from '$lib/auth/sessionEnd.svelte';
import * as storage from '$lib/util/safeLocalStorage';
import { api } from '#lib/api/client.js';
import { user } from '#lib/auth/user.svelte.js';
import { sessionEnd } from '#lib/auth/sessionEnd.svelte.js';
import * as storage from '#lib/util/safeLocalStorage.js';
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
+4 -4
View File
@@ -17,11 +17,11 @@ import {
__setShuffleRng,
restoreQueue
} from './store.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { writePersistedQueue, clearPersistedQueue } from './persisted';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: {
get value() {
return { id: 'test-user' };
@@ -345,7 +345,7 @@ import {
toggleQueueDrawer,
closeQueueDrawer
} from './store.svelte';
import * as client from '$lib/api/client';
import * as client from '#lib/api/client.js';
describe('player store — enqueue + playRadio', () => {
test('enqueueTrack appends to a non-empty queue, _index unchanged', () => {
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import type { PlaylistTrack } from '$lib/api/types';
import type { PlaylistTrack } from '#lib/api/types.js';
import { playlistTrackToRef } from './playlistTrackToRef';
const row: PlaylistTrack = {
+1 -1
View File
@@ -11,7 +11,7 @@
// Callers filter nulls, which is what makes a missing file "get skipped"
// everywhere a queue is built rather than at each call site.
import type { PlaylistTrack, TrackRef } from '$lib/api/types';
import type { PlaylistTrack, TrackRef } from '#lib/api/types.js';
export function playlistTrackToRef(row: PlaylistTrack): TrackRef | null {
if (!row.track_id || row.unavailable) return null;

Some files were not shown because too many files have changed in this diff Show More