Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
149 lines
4.1 KiB
Svelte
149 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { downloads } from "$lib/stores/downloads";
|
|
import { auth } from "$lib/stores/auth";
|
|
import { commands } from "$lib/api/bindings";
|
|
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
|
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
|
import { createLogger } from "$lib/utils/logger";
|
|
|
|
const log = createLogger("DownloadButton");
|
|
|
|
/**
|
|
* Single audio track download button
|
|
* @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
|
|
*/
|
|
|
|
interface Props {
|
|
itemId: string;
|
|
itemName?: string;
|
|
artistName?: string;
|
|
albumName?: string;
|
|
size?: "sm" | "md" | "lg";
|
|
className?: string;
|
|
}
|
|
|
|
let {
|
|
itemId,
|
|
itemName = "",
|
|
artistName = "",
|
|
albumName = "",
|
|
size = "md",
|
|
className = "",
|
|
}: Props = $props();
|
|
|
|
let isProcessing = $state(false);
|
|
|
|
// Find download for this item
|
|
const downloadInfo = $derived(
|
|
Object.values($downloads.downloads).find((d) => d.itemId === itemId),
|
|
);
|
|
|
|
const status = $derived(downloadInfo?.status || "not_downloaded");
|
|
const progress = $derived(downloadInfo?.progress || 0);
|
|
|
|
const buttonState = $derived<DownloadState>({
|
|
status: (status as DownloadState["status"]) || "not_downloaded",
|
|
progress: progress || 0,
|
|
});
|
|
|
|
async function handleClick() {
|
|
log.debug("🖱️ Download button clicked! Current status:", status);
|
|
if (isProcessing) return;
|
|
|
|
isProcessing = true;
|
|
try {
|
|
if (status === "completed") {
|
|
// Delete download
|
|
if (downloadInfo?.id) {
|
|
await downloads.delete(downloadInfo.id);
|
|
}
|
|
} else if (status === "downloading" || status === "pending") {
|
|
// Cancel download
|
|
if (downloadInfo?.id) {
|
|
await downloads.cancel(downloadInfo.id);
|
|
}
|
|
} else if (status === "failed") {
|
|
// Retry failed download
|
|
if (downloadInfo?.id) {
|
|
await downloads.resume(downloadInfo.id);
|
|
}
|
|
} else {
|
|
// Start download
|
|
const userId = $auth.user?.id;
|
|
if (!userId) {
|
|
log.error("No user ID found");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const repo = auth.getRepository();
|
|
|
|
log.debug("🎯 Starting download for item:", itemId);
|
|
|
|
// Get stream URL
|
|
const streamUrl = await repo.getAudioStreamUrl(itemId);
|
|
log.debug(" Stream URL obtained:", streamUrl?.substring(0, 50) + "...");
|
|
if (!streamUrl) {
|
|
throw new Error("Failed to get stream URL");
|
|
}
|
|
|
|
// Get target directory
|
|
const targetDir = await commands.storageGetPath();
|
|
log.debug(" Target directory:", targetDir);
|
|
|
|
// Queue and start download in single atomic operation
|
|
const downloadId = await commands.downloadItemAndStart({
|
|
itemId,
|
|
userId,
|
|
streamUrl,
|
|
targetDir,
|
|
itemName: itemName || null,
|
|
artistName: artistName || null,
|
|
albumName: albumName || null,
|
|
});
|
|
log.debug(" Download queued and started with ID:", downloadId);
|
|
|
|
// Refresh downloads list
|
|
await downloads.refresh(userId);
|
|
} catch (e) {
|
|
log.error("❌ Failed to start download:", e);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.error("Download operation failed:", error);
|
|
} finally {
|
|
isProcessing = false;
|
|
}
|
|
}
|
|
|
|
function getTitle(): string {
|
|
switch (status) {
|
|
case "completed":
|
|
return "Downloaded - Click to remove";
|
|
case "downloading":
|
|
return `Downloading... ${Math.round(progress * 100)}%`;
|
|
case "pending":
|
|
return "Queued for download";
|
|
case "paused":
|
|
return "Download paused";
|
|
case "failed":
|
|
return "Download failed - Click to retry";
|
|
default:
|
|
return "Download for offline playback";
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="p-2 rounded-full">
|
|
<DownloadButtonCore
|
|
{size}
|
|
state={buttonState}
|
|
title={getTitle()}
|
|
onClick={handleClick}
|
|
{isProcessing}
|
|
{className}
|
|
/>
|
|
</div>
|