Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca9917d8cd | ||
|
|
b1b10b0c77 | ||
|
|
94a9c8cbe3 | ||
|
|
beba5b2082 | ||
|
|
fecd030b68 | ||
|
|
0500f6aac5 | ||
|
|
c83933b61a |
@@ -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
@@ -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
|
||||
|
||||
Generated
+668
-1062
File diff suppressed because it is too large
Load Diff
+12
-9
@@ -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/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
+71
-4
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { ApiError } from '$lib/api/client';
|
||||
import type { ApiError } from '#lib/api/client.js';
|
||||
|
||||
let {
|
||||
error,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { coverUrl } from '$lib/media/covers';
|
||||
import { coverUrl } from '#lib/media/covers.js';
|
||||
import {
|
||||
player,
|
||||
togglePlay,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import type { TrackRef } from '$lib/api/types';
|
||||
import type { TrackRef } from '#lib/api/types.js';
|
||||
|
||||
export type PersistedPlayerState = {
|
||||
v: 1;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user