4067be04a6
Variant of TrackRow specialised for playlist detail. Drag handle visible to owner only; remove button (X) visible to owner only; greyed-out + strikethrough when track_id is null (upstream track removed from library). Reuses the existing LikeButton and TrackMenu when the track is still alive.
98 lines
2.9 KiB
Svelte
98 lines
2.9 KiB
Svelte
<script lang="ts">
|
|
import { GripVertical, X } from 'lucide-svelte';
|
|
import LikeButton from './LikeButton.svelte';
|
|
import TrackMenu from './TrackMenu.svelte';
|
|
import type { PlaylistTrack, TrackRef } from '$lib/api/types';
|
|
|
|
let {
|
|
row,
|
|
isOwner,
|
|
onRemove,
|
|
onPlay,
|
|
onDragStart,
|
|
onDragOver,
|
|
onDrop
|
|
}: {
|
|
row: PlaylistTrack;
|
|
isOwner: boolean;
|
|
onRemove: (position: number) => void;
|
|
onPlay: (position: number) => void;
|
|
onDragStart?: (position: number) => void;
|
|
onDragOver?: (e: DragEvent, position: number) => void;
|
|
onDrop?: (e: DragEvent, position: number) => void;
|
|
} = $props();
|
|
|
|
const isUnavailable = $derived(row.track_id === null);
|
|
|
|
// Construct a minimal TrackRef for the kebab menu when the upstream
|
|
// track still exists. When unavailable, the menu is hidden.
|
|
const liveTrack = $derived<TrackRef | null>(
|
|
row.track_id
|
|
? ({
|
|
id: row.track_id,
|
|
title: row.title,
|
|
album_id: row.album_id ?? '',
|
|
album_title: row.album_title,
|
|
artist_id: row.artist_id ?? '',
|
|
artist_name: row.artist_name,
|
|
duration_sec: row.duration_sec,
|
|
stream_url: row.stream_url ?? ''
|
|
} as TrackRef)
|
|
: null
|
|
);
|
|
|
|
function format(sec: number): string {
|
|
const m = Math.floor(sec / 60);
|
|
const s = sec % 60;
|
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="flex items-center gap-3 px-3 py-2 text-sm transition-colors hover:bg-surface-hover
|
|
{isUnavailable ? 'text-text-muted' : 'text-text-primary'}"
|
|
draggable={isOwner && !isUnavailable}
|
|
ondragstart={() => onDragStart?.(row.position)}
|
|
ondragover={(e) => { e.preventDefault(); onDragOver?.(e, row.position); }}
|
|
ondrop={(e) => { e.preventDefault(); onDrop?.(e, row.position); }}
|
|
>
|
|
{#if isOwner}
|
|
<span
|
|
class="flex-shrink-0 cursor-grab text-text-muted active:cursor-grabbing"
|
|
aria-label="Drag handle"
|
|
>
|
|
<GripVertical size={14} strokeWidth={1} />
|
|
</span>
|
|
{/if}
|
|
|
|
<button
|
|
type="button"
|
|
class="min-w-0 flex-1 text-left"
|
|
onclick={() => !isUnavailable && onPlay(row.position)}
|
|
disabled={isUnavailable}
|
|
>
|
|
<div class="truncate {isUnavailable ? 'line-through' : ''}">{row.title}</div>
|
|
<div class="truncate text-xs text-text-muted">
|
|
{row.artist_name} · {row.album_title}
|
|
</div>
|
|
</button>
|
|
|
|
<span class="flex-shrink-0 text-xs text-text-muted">{format(row.duration_sec)}</span>
|
|
|
|
{#if !isUnavailable && liveTrack}
|
|
<LikeButton entityType="track" entityId={liveTrack.id} />
|
|
<TrackMenu track={liveTrack} />
|
|
{/if}
|
|
|
|
{#if isOwner}
|
|
<button
|
|
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"
|
|
>
|
|
<X size={14} strokeWidth={1} />
|
|
</button>
|
|
{/if}
|
|
</div>
|