jellytau/src/lib/components/library/DownloadButtonCore.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 = 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>