97 lines
3.4 KiB
Svelte
97 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Core download button UI component - Renders download state with progress
|
|
* Used as base for all download button variants (tracks, albums, videos, series)
|
|
*
|
|
* @req: UR-011 - Download for offline playback
|
|
* @req: UR-018 - Download entire albums or playlists
|
|
* @req: DR-018 - Download buttons on library/album/player screens
|
|
*/
|
|
|
|
export interface DownloadState {
|
|
status: "not_downloaded" | "downloading" | "pending" | "completed" | "failed";
|
|
progress: number; // 0-1
|
|
}
|
|
|
|
interface Props {
|
|
state: DownloadState;
|
|
size?: "sm" | "md" | "lg";
|
|
title: string;
|
|
onClick?: () => void | Promise<void>;
|
|
isProcessing?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
let { state, size = "md", title, onClick, isProcessing = false, className = "" }: Props = $props();
|
|
|
|
const sizeMap = {
|
|
sm: { icon: "w-4 h-4", ring: "w-8 h-8" },
|
|
md: { icon: "w-5 h-5", ring: "w-10 h-10" },
|
|
lg: { icon: "w-6 h-6", ring: "w-12 h-12" },
|
|
};
|
|
|
|
const colorMap = {
|
|
not_downloaded: "text-gray-400 hover:text-white",
|
|
downloading: "text-blue-500",
|
|
pending: "text-yellow-500",
|
|
completed: "text-green-500",
|
|
failed: "text-red-500",
|
|
};
|
|
|
|
const circumference = 2 * Math.PI * 15;
|
|
const offset = $derived(circumference - (state.progress || 0) * circumference);
|
|
</script>
|
|
|
|
<button
|
|
onclick={onClick}
|
|
disabled={isProcessing || state.status === "downloading"}
|
|
aria-label={title}
|
|
title={title}
|
|
class={`relative transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${colorMap[state.status]} ${className}`}
|
|
>
|
|
{#if state.status === "downloading"}
|
|
<!-- Progress Ring -->
|
|
<svg class="{sizeMap[size].ring} -rotate-90" viewBox="0 0 36 36">
|
|
<circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" stroke-width="2" class="opacity-20" />
|
|
<circle
|
|
cx="18"
|
|
cy="18"
|
|
r="15"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-dasharray={circumference}
|
|
stroke-dashoffset={offset}
|
|
stroke-linecap="round"
|
|
class="transition-all"
|
|
style="transition: stroke-dashoffset 0.3s ease;"
|
|
/>
|
|
<!-- Download Icon in Center -->
|
|
<text x="18" y="20" text-anchor="middle" class="text-xs font-bold fill-current">
|
|
{Math.round(state.progress * 100)}%
|
|
</text>
|
|
</svg>
|
|
{:else if state.status === "completed"}
|
|
<!-- Checkmark Icon -->
|
|
<svg class={sizeMap[size].icon} fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
|
|
</svg>
|
|
{:else if state.status === "failed"}
|
|
<!-- Error Icon -->
|
|
<svg class={sizeMap[size].icon} fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
{:else if state.status === "pending"}
|
|
<!-- Pending Icon (clock) -->
|
|
<svg class={sizeMap[size].icon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="9" stroke-width="2" />
|
|
<path stroke-width="2" stroke-linecap="round" d="M12 6v6l4 2" />
|
|
</svg>
|
|
{:else}
|
|
<!-- Download Icon -->
|
|
<svg class={sizeMap[size].icon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|