9dc4786fd9
Replaces the M5b-era FlagPopover-only menu with the full track-actions surface: queue (Play next, Add to queue), collection (Like/Unlike, Add to playlist… reserved for #352), navigation (Go to album/artist), lifecycle (Flag, Hide/Unhide, Remove from library — admin-only). Remove from library opens a new RemoveTrackPopover with a single "Also stop Lidarr from finding a replacement" checkbox. The destructive flow always deletes file + DB through Minstrel; the checkbox controls whether Lidarr is also told to unmonitor. Lidarr unmonitor failure flows back as lidarr_unmonitor_failed in the response — destructive part already succeeded. The component's prop API (track, direction) is unchanged so TrackRow and PlayerBar pick up the new entries with no code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.4 KiB
Svelte
103 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { Trash2 } from 'lucide-svelte';
|
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
import { removeTrack } from '$lib/api/admin/tracks';
|
|
import { copyForCode } from '$lib/api/error-copy';
|
|
import { qk } from '$lib/api/queries';
|
|
import type { TrackRef } from '$lib/api/types';
|
|
|
|
let {
|
|
track,
|
|
onClose
|
|
}: {
|
|
track: TrackRef;
|
|
onClose: () => void;
|
|
} = $props();
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
let unmonitor = $state(false);
|
|
let busy = $state(false);
|
|
let error = $state<string | null>(null);
|
|
|
|
async function confirm() {
|
|
busy = true;
|
|
error = null;
|
|
try {
|
|
const result = await removeTrack(track.id, { unmonitor });
|
|
// Invalidate caches for any vanished entity. The album/artist that
|
|
// contained the track is always touched; if the parent rows were
|
|
// tidied up by the server cascade, evict those too. Home payload
|
|
// surfaces albums/artists on the landing page so it always
|
|
// refreshes.
|
|
await queryClient.invalidateQueries({ queryKey: qk.album(track.album_id) });
|
|
await queryClient.invalidateQueries({ queryKey: qk.artist(track.artist_id) });
|
|
await queryClient.invalidateQueries({ queryKey: qk.home() });
|
|
if (result.deleted_album_id) {
|
|
await queryClient.invalidateQueries({ queryKey: qk.album(result.deleted_album_id) });
|
|
}
|
|
if (result.deleted_artist_id) {
|
|
await queryClient.invalidateQueries({ queryKey: qk.artist(result.deleted_artist_id) });
|
|
}
|
|
// Note: result.lidarr_unmonitor_failed is true when unmonitor was
|
|
// requested AND Lidarr couldn't be reached. The destructive part
|
|
// already succeeded; we still close the popover. A future toast
|
|
// surface (if any) can pick up the flag.
|
|
onClose();
|
|
} catch (e: unknown) {
|
|
const code = (e as { code?: string })?.code ?? 'unknown';
|
|
error = copyForCode(code);
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
role="dialog"
|
|
aria-modal="false"
|
|
aria-labelledby="remove-track-popover-title"
|
|
class="absolute right-0 z-30 mt-1 w-72 rounded-lg border border-border bg-surface p-3 shadow-xl"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<h4 id="remove-track-popover-title" class="text-sm font-medium text-text-primary">
|
|
Remove "{track.title}" from your library?
|
|
</h4>
|
|
<p class="mt-1 text-xs text-text-muted">
|
|
This deletes the file from disk.
|
|
</p>
|
|
|
|
<label class="mt-3 flex items-start gap-2 text-sm text-text-primary">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={unmonitor}
|
|
class="mt-0.5 rounded border-border"
|
|
/>
|
|
<span>Also stop Lidarr from finding a replacement</span>
|
|
</label>
|
|
|
|
{#if error}
|
|
<p class="mt-2 text-xs text-action-destructive">{error}</p>
|
|
{/if}
|
|
|
|
<div class="mt-3 flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
class="rounded-md border border-border px-2.5 py-1 text-sm text-text-secondary hover:text-text-primary disabled:opacity-50"
|
|
onclick={onClose}
|
|
disabled={busy}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick={confirm}
|
|
disabled={busy}
|
|
class="inline-flex items-center gap-1 rounded-md border border-border bg-surface px-2.5 py-1 text-sm text-action-destructive hover:bg-surface-hover disabled:opacity-50"
|
|
>
|
|
<Trash2 size={14} strokeWidth={1} />
|
|
{busy ? 'Removing…' : 'Remove'}
|
|
</button>
|
|
</div>
|
|
</div>
|