diff --git a/web/src/lib/components/AlbumCard.svelte b/web/src/lib/components/AlbumCard.svelte
index 828daa79..4eeb0aed 100644
--- a/web/src/lib/components/AlbumCard.svelte
+++ b/web/src/lib/components/AlbumCard.svelte
@@ -3,6 +3,7 @@
import { FALLBACK_COVER } from '$lib/media/covers';
import { api } from '$lib/api/client';
import { enqueueTracks } from '$lib/player/store.svelte';
+ import LikeButton from './LikeButton.svelte';
let { album }: { album: AlbumRef } = $props();
@@ -35,10 +36,13 @@
{album.year}
{/if}
-
+
+
+
+
diff --git a/web/src/lib/components/AlbumCard.test.ts b/web/src/lib/components/AlbumCard.test.ts
index 1f512b6a..6a95cbd0 100644
--- a/web/src/lib/components/AlbumCard.test.ts
+++ b/web/src/lib/components/AlbumCard.test.ts
@@ -2,6 +2,7 @@ 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 { readable } from 'svelte/store';
vi.mock('$lib/api/client', () => ({
api: { get: vi.fn() }
@@ -10,6 +11,21 @@ vi.mock('$lib/player/store.svelte', () => ({
enqueueTracks: vi.fn()
}));
+vi.mock('$lib/api/likes', () => ({
+ createLikedIdsQuery: () => readable({
+ data: { track_ids: [], album_ids: [], artist_ids: [] },
+ isPending: false,
+ isError: false
+ }),
+ likeEntity: vi.fn(),
+ unlikeEntity: vi.fn()
+}));
+
+vi.mock('@tanstack/svelte-query', async (orig) => {
+ const actual = (await orig()) as Record;
+ return { ...actual, useQueryClient: () => ({}) };
+});
+
import AlbumCard from './AlbumCard.svelte';
import { api } from '$lib/api/client';
import { enqueueTracks } from '$lib/player/store.svelte';
diff --git a/web/src/lib/components/ArtistRow.svelte b/web/src/lib/components/ArtistRow.svelte
index 92aee7ad..33fd5698 100644
--- a/web/src/lib/components/ArtistRow.svelte
+++ b/web/src/lib/components/ArtistRow.svelte
@@ -1,5 +1,6 @@
-
+
+
{initial}
-
{artist.name}
-
{countLabel}
-
›
-
+
{artist.name}
+
{countLabel}
+
+
+
+
›
+
diff --git a/web/src/lib/components/ArtistRow.test.ts b/web/src/lib/components/ArtistRow.test.ts
index 5dec7fe4..333b78c2 100644
--- a/web/src/lib/components/ArtistRow.test.ts
+++ b/web/src/lib/components/ArtistRow.test.ts
@@ -1,8 +1,24 @@
-import { describe, expect, test } from 'vitest';
+import { describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
+import { readable } from 'svelte/store';
import ArtistRow from './ArtistRow.svelte';
import type { ArtistRef } from '$lib/api/types';
+vi.mock('$lib/api/likes', () => ({
+ createLikedIdsQuery: () => readable({
+ data: { track_ids: [], album_ids: [], artist_ids: [] },
+ isPending: false,
+ isError: false
+ }),
+ likeEntity: vi.fn(),
+ unlikeEntity: vi.fn()
+}));
+
+vi.mock('@tanstack/svelte-query', async (orig) => {
+ const actual = (await orig()) as Record;
+ return { ...actual, useQueryClient: () => ({}) };
+});
+
const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 };
describe('ArtistRow', () => {
@@ -11,14 +27,14 @@ describe('ArtistRow', () => {
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/artists/abc');
- expect(link).toHaveTextContent('alice');
- expect(link).toHaveTextContent('12 albums');
- expect(link).toHaveTextContent('A'); // initial letter
+ expect(screen.getByText('alice')).toBeInTheDocument();
+ expect(screen.getByText('12 albums')).toBeInTheDocument();
+ expect(screen.getByText('A')).toBeInTheDocument();
});
test('singular "1 album" when album_count is 1', () => {
render(ArtistRow, { props: { artist: { ...artist, album_count: 1 } } });
- expect(screen.getByRole('link')).toHaveTextContent('1 album');
+ expect(screen.getByText('1 album')).toBeInTheDocument();
});
test('empty name still renders a safe initial', () => {
diff --git a/web/src/lib/components/PlayerBar.svelte b/web/src/lib/components/PlayerBar.svelte
index a35c8932..e2e593b3 100644
--- a/web/src/lib/components/PlayerBar.svelte
+++ b/web/src/lib/components/PlayerBar.svelte
@@ -6,6 +6,7 @@
} from '$lib/player/store.svelte';
import { formatDuration } from '$lib/media/duration';
import { FALLBACK_COVER } from '$lib/media/covers';
+ import LikeButton from './LikeButton.svelte';
const current = $derived(player.current);
@@ -48,7 +49,7 @@
onerror={onCoverError}
/>
-
diff --git a/web/src/lib/components/PlayerBar.test.ts b/web/src/lib/components/PlayerBar.test.ts
index e817aa64..67c31e02 100644
--- a/web/src/lib/components/PlayerBar.test.ts
+++ b/web/src/lib/components/PlayerBar.test.ts
@@ -1,6 +1,7 @@
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 { readable } from 'svelte/store';
// Mutable state the mocked store reads from.
const state = vi.hoisted(() => ({
@@ -40,6 +41,21 @@ vi.mock('$lib/player/store.svelte', () => ({
playQueue: vi.fn()
}));
+vi.mock('$lib/api/likes', () => ({
+ createLikedIdsQuery: () => readable({
+ data: { track_ids: [], album_ids: [], artist_ids: [] },
+ isPending: false,
+ isError: false
+ }),
+ likeEntity: vi.fn(),
+ unlikeEntity: vi.fn()
+}));
+
+vi.mock('@tanstack/svelte-query', async (orig) => {
+ const actual = (await orig()) as Record;
+ return { ...actual, useQueryClient: () => ({}) };
+});
+
import PlayerBar from './PlayerBar.svelte';
import {
togglePlay, skipNext, skipPrev, seekTo, setVolume,
diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte
index e19df1f6..8a81e8a8 100644
--- a/web/src/lib/components/Shell.svelte
+++ b/web/src/lib/components/Shell.svelte
@@ -22,9 +22,10 @@
}
const navItems = [
- { href: '/', label: 'Library' },
- { href: '/search', label: 'Search' },
- { href: '/playlists', label: 'Playlists' }
+ { href: '/', label: 'Library' },
+ { href: '/library/liked', label: 'Liked' },
+ { href: '/search', label: 'Search' },
+ { href: '/playlists', label: 'Playlists' }
];
diff --git a/web/src/lib/components/TrackRow.svelte b/web/src/lib/components/TrackRow.svelte
index 5cad7134..68a76432 100644
--- a/web/src/lib/components/TrackRow.svelte
+++ b/web/src/lib/components/TrackRow.svelte
@@ -2,6 +2,7 @@
import type { TrackRef } from '$lib/api/types';
import { formatDuration } from '$lib/media/duration';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
+ import LikeButton from './LikeButton.svelte';
type PlayHandler = (tracks: TrackRef[], index: number) => void;
@@ -41,12 +42,13 @@
aria-label={track.title}
onclick={onRowClick}
onkeydown={onRowKey}
- class="grid w-full cursor-pointer grid-cols-[32px_1fr_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"
+ class="grid w-full cursor-pointer grid-cols-[32px_1fr_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"
>
{track.track_number ?? '—'}
{track.title}
+