First working POC
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<script lang="ts">
|
||||
import { downloads } from "$lib/stores/downloads";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
|
||||
interface Props {
|
||||
albumId: string;
|
||||
albumName: string;
|
||||
tracks: MediaItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { albumId, albumName, tracks, className = "" }: Props = $props();
|
||||
|
||||
let isProcessing = $state(false);
|
||||
|
||||
// Calculate download status for all tracks in album
|
||||
const downloadStatuses = $derived(
|
||||
tracks.map((track) =>
|
||||
Object.values($downloads.downloads).find((d) => d.itemId === track.id)
|
||||
)
|
||||
);
|
||||
|
||||
const completedCount = $derived(
|
||||
downloadStatuses.filter((d) => d?.status === "completed").length
|
||||
);
|
||||
|
||||
const downloadingCount = $derived(
|
||||
downloadStatuses.filter(
|
||||
(d) => d?.status === "downloading" || d?.status === "pending"
|
||||
).length
|
||||
);
|
||||
|
||||
const failedCount = $derived(
|
||||
downloadStatuses.filter((d) => d?.status === "failed").length
|
||||
);
|
||||
|
||||
const totalProgress = $derived(() => {
|
||||
if (tracks.length === 0) return 0;
|
||||
const activeDownloads = downloadStatuses.filter(
|
||||
(d) => d?.status === "downloading"
|
||||
);
|
||||
if (activeDownloads.length === 0) return completedCount / tracks.length;
|
||||
|
||||
const downloadingProgress = activeDownloads.reduce(
|
||||
(sum, d) => sum + (d?.progress || 0),
|
||||
0
|
||||
);
|
||||
return (completedCount + downloadingProgress) / tracks.length;
|
||||
});
|
||||
|
||||
const isFullyDownloaded = $derived(completedCount === tracks.length && tracks.length > 0);
|
||||
const isDownloading = $derived(downloadingCount > 0);
|
||||
const hasPartialDownload = $derived(completedCount > 0 && completedCount < tracks.length);
|
||||
|
||||
async function handleClick() {
|
||||
if (isProcessing) return;
|
||||
|
||||
isProcessing = true;
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFullyDownloaded) {
|
||||
// Delete all downloads for this album
|
||||
for (const status of downloadStatuses) {
|
||||
if (status?.id) {
|
||||
await downloads.delete(status.id);
|
||||
}
|
||||
}
|
||||
} else if (isDownloading) {
|
||||
// Cancel all active downloads for this album
|
||||
for (const status of downloadStatuses) {
|
||||
if (
|
||||
status?.id &&
|
||||
(status.status === "downloading" || status.status === "pending")
|
||||
) {
|
||||
await downloads.cancel(status.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the album
|
||||
const basePath = `albums/${albumId}`;
|
||||
await downloads.downloadAlbum(albumId, userId, basePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Album download operation failed:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTitle(): string {
|
||||
if (isFullyDownloaded) {
|
||||
return "Downloaded - Click to remove";
|
||||
}
|
||||
if (isDownloading) {
|
||||
return `Downloading ${completedCount + downloadingCount}/${tracks.length}... Click to cancel`;
|
||||
}
|
||||
if (hasPartialDownload) {
|
||||
return `${completedCount}/${tracks.length} downloaded - Click to download remaining`;
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
return `${failedCount} failed - Click to retry`;
|
||||
}
|
||||
return "Download for offline playback";
|
||||
}
|
||||
|
||||
function getStatusText(): string {
|
||||
if (isFullyDownloaded) return "";
|
||||
if (isDownloading || hasPartialDownload) {
|
||||
return `${completedCount}/${tracks.length}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={handleClick}
|
||||
disabled={isProcessing || tracks.length === 0}
|
||||
class="px-6 py-2 rounded-lg font-medium flex items-center gap-2 transition-colors {isFullyDownloaded
|
||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||||
: isDownloading
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)]'} {isProcessing
|
||||
? 'opacity-50 cursor-wait'
|
||||
: ''} {className}"
|
||||
title={getTitle()}
|
||||
aria-label={getTitle()}
|
||||
>
|
||||
<div class="relative w-5 h-5">
|
||||
{#if isDownloading}
|
||||
<!-- Progress ring -->
|
||||
<svg class="absolute inset-0 -rotate-90" viewBox="0 0 24 24">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
opacity="0.3"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-dasharray={2 * Math.PI * 10}
|
||||
stroke-dashoffset={2 * Math.PI * 10 * (1 - totalProgress())}
|
||||
stroke-linecap="round"
|
||||
class="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Small download icon inside -->
|
||||
<svg
|
||||
class="absolute inset-0 m-auto w-3 h-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4v12m0 0l-4-4m4 4l4-4m-9 7h10"
|
||||
/>
|
||||
</svg>
|
||||
{:else if isFullyDownloaded}
|
||||
<!-- Checkmark icon -->
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{:else if failedCount > 0}
|
||||
<!-- Error icon -->
|
||||
<svg
|
||||
class="w-5 h-5 text-red-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Download icon -->
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4v12m0 0l-4-4m4 4l4-4m-9 7h10"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isDownloading || hasPartialDownload}
|
||||
<span>{getStatusText()}</span>
|
||||
{:else if isFullyDownloaded}
|
||||
<span>Downloaded</span>
|
||||
{:else}
|
||||
<span>Download</span>
|
||||
{/if}
|
||||
</button>
|
||||
Reference in New Issue
Block a user