First working POC
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<script lang="ts">
|
||||
import { downloads, type DownloadInfo } from "$lib/stores/downloads";
|
||||
|
||||
interface Props {
|
||||
download: DownloadInfo;
|
||||
}
|
||||
|
||||
let { download }: Props = $props();
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
function formatProgress(): string {
|
||||
if (!download.fileSize) {
|
||||
return formatBytes(download.bytesDownloaded);
|
||||
}
|
||||
return `${formatBytes(download.bytesDownloaded)} / ${formatBytes(download.fileSize)}`;
|
||||
}
|
||||
|
||||
function getStatusColor(): string {
|
||||
switch (download.status) {
|
||||
case "downloading":
|
||||
return "bg-blue-500";
|
||||
case "completed":
|
||||
return "bg-green-500";
|
||||
case "failed":
|
||||
return "bg-red-500";
|
||||
case "paused":
|
||||
return "bg-yellow-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(): string {
|
||||
switch (download.status) {
|
||||
case "pending":
|
||||
return "Queued";
|
||||
case "downloading":
|
||||
return "Downloading";
|
||||
case "completed":
|
||||
return "Completed";
|
||||
case "failed":
|
||||
return download.errorMessage || "Failed";
|
||||
case "paused":
|
||||
return "Paused";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function getSourceBorderColor(): string {
|
||||
// Green for user downloads, blue for auto-cached
|
||||
return download.downloadSource === 'user' ? 'border-green-500/50' : 'border-blue-500/50';
|
||||
}
|
||||
|
||||
function getSourceLabel(): string {
|
||||
return download.downloadSource === 'user' ? 'Downloaded' : 'Auto-Cached';
|
||||
}
|
||||
|
||||
async function handlePause() {
|
||||
try {
|
||||
await downloads.pause(download.id);
|
||||
// Refresh to update UI
|
||||
await downloads.refresh(download.userId);
|
||||
} catch (error) {
|
||||
console.error("Failed to pause download:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResume() {
|
||||
try {
|
||||
await downloads.resume(download.id);
|
||||
// Refresh to update UI
|
||||
await downloads.refresh(download.userId);
|
||||
} catch (error) {
|
||||
console.error("Failed to resume download:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel() {
|
||||
try {
|
||||
await downloads.cancel(download.id);
|
||||
// Refresh to update UI
|
||||
await downloads.refresh(download.userId);
|
||||
} catch (error) {
|
||||
console.error("Failed to cancel download:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await downloads.delete(download.id);
|
||||
// Refresh to update UI
|
||||
await downloads.refresh(download.userId);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete download:", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-[var(--color-surface)] rounded-lg p-4 hover:bg-[var(--color-surface-hover)] transition-colors border-l-4 {getSourceBorderColor()}">
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Status Indicator -->
|
||||
<div class="w-2 h-2 rounded-full {getStatusColor()} flex-shrink-0"></div>
|
||||
|
||||
<!-- Media Type Icon -->
|
||||
<div class="flex-shrink-0 text-gray-500">
|
||||
{#if download.mediaType === "video"}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-1.5A1.125 1.125 0 0118 18.375M20.625 4.5H3.375m17.25 0c.621 0 1.125.504 1.125 1.125M20.625 4.5h-1.5C18.504 4.5 18 5.004 18 5.625m3.75 0v1.5c0 .621-.504 1.125-1.125 1.125M3.375 4.5c-.621 0-1.125.504-1.125 1.125M3.375 4.5h1.5C5.496 4.5 6 5.004 6 5.625m-3.75 0v1.5c0 .621.504 1.125 1.125 1.125m0 0h1.5m-1.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m1.5-3.75C5.496 8.25 6 7.746 6 7.125v-1.5M4.875 8.25C5.496 8.25 6 8.754 6 9.375v1.5m0-5.25v5.25m0-5.25C6 5.004 6.504 4.5 7.125 4.5h9.75c.621 0 1.125.504 1.125 1.125m1.125 2.625h1.5m-1.5 0A1.125 1.125 0 0118 7.125v-1.5m1.125 2.625c-.621 0-1.125.504-1.125 1.125v1.5m2.625-2.625c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125M18 5.625v5.25M7.125 12h9.75m-9.75 0A1.125 1.125 0 016 10.875M7.125 12C6.504 12 6 12.504 6 13.125m0-2.25C6 11.496 5.496 12 4.875 12M18 10.875c0 .621-.504 1.125-1.125 1.125M18 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m-12 5.25v-5.25m0 5.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125m-12 0v-1.5c0-.621-.504-1.125-1.125-1.125M18 18.375v-5.25m0 5.25v-1.5c0-.621.504-1.125 1.125-1.125M18 13.125v1.5c0 .621.504 1.125 1.125 1.125M18 13.125c0-.621.504-1.125 1.125-1.125M6 13.125v1.5c0 .621-.504 1.125-1.125 1.125M6 13.125C6 12.504 5.496 12 4.875 12m-1.5 0h1.5m-1.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M19.125 12h1.5m0 0c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h1.5m14.25 0h1.5" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Download Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-white font-medium truncate">{download.itemName || download.itemId}</p>
|
||||
{#if download.mediaType === "video" && download.seriesName}
|
||||
<!-- Video: Show series info and episode number -->
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
{download.seriesName}
|
||||
{#if download.seasonNumber !== undefined && download.episodeNumber !== undefined}
|
||||
<span class="text-gray-500"> • S{String(download.seasonNumber).padStart(2, '0')}E{String(download.episodeNumber).padStart(2, '0')}</span>
|
||||
{/if}
|
||||
{#if download.qualityPreset && download.qualityPreset !== "original"}
|
||||
<span class="ml-2 px-1.5 py-0.5 bg-gray-700 rounded text-[10px] uppercase">{download.qualityPreset}</span>
|
||||
{/if}
|
||||
</p>
|
||||
{:else if download.mediaType === "video"}
|
||||
<!-- Movie: Show quality badge -->
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
Movie
|
||||
{#if download.qualityPreset && download.qualityPreset !== "original"}
|
||||
<span class="ml-2 px-1.5 py-0.5 bg-gray-700 rounded text-[10px] uppercase">{download.qualityPreset}</span>
|
||||
{/if}
|
||||
</p>
|
||||
{:else if download.artistName || download.albumName}
|
||||
<!-- Audio: Show artist and album -->
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
{download.artistName}{download.artistName && download.albumName ? ' • ' : ''}{download.albumName}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-2 flex-shrink-0">
|
||||
<span class="text-xs text-gray-400">{getStatusText()}</span>
|
||||
{#if download.downloadSource === 'auto'}
|
||||
<span class="text-[10px] px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded uppercase font-semibold" title="Automatically cached">Auto</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar (for active/paused downloads) -->
|
||||
{#if download.status === "downloading" || download.status === "paused"}
|
||||
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300 {getStatusColor()}"
|
||||
style="width: {download.progress * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||
<span>{Math.round(download.progress * 100)}%</span>
|
||||
<span>{formatProgress()}</span>
|
||||
</div>
|
||||
{:else if download.status === "completed"}
|
||||
<p class="text-xs text-gray-400">{formatBytes(download.bytesDownloaded)}</p>
|
||||
{:else if download.status === "failed"}
|
||||
<p class="text-xs text-red-400">{download.errorMessage || "Download failed"}</p>
|
||||
{:else if download.status === "pending"}
|
||||
<p class="text-xs text-gray-400">
|
||||
{#if download.fileSize}
|
||||
{formatBytes(download.fileSize)}
|
||||
{:else}
|
||||
Waiting to start...
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
{#if download.status === "downloading"}
|
||||
<!-- Pause Button -->
|
||||
<button
|
||||
onclick={handlePause}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-white transition-colors"
|
||||
title="Pause download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Cancel Button -->
|
||||
<button
|
||||
onclick={handleCancel}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Cancel download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if download.status === "paused"}
|
||||
<!-- Resume Button -->
|
||||
<button
|
||||
onclick={handleResume}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-white transition-colors"
|
||||
title="Resume download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Cancel Button -->
|
||||
<button
|
||||
onclick={handleCancel}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Cancel download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if download.status === "pending"}
|
||||
<!-- Cancel Button -->
|
||||
<button
|
||||
onclick={handleCancel}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Cancel download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if download.status === "completed"}
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Delete download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else if download.status === "failed"}
|
||||
<!-- Retry Button -->
|
||||
<button
|
||||
onclick={handleResume}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-white transition-colors"
|
||||
title="Retry download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Delete failed download"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { downloads } from "$lib/stores/downloads";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
interface AlbumStorageInfo {
|
||||
album_id: string;
|
||||
album_name: string;
|
||||
artist_name: string | null;
|
||||
bytes_used: number;
|
||||
track_count: number;
|
||||
}
|
||||
|
||||
interface StorageStats {
|
||||
total_bytes: number;
|
||||
total_items: number;
|
||||
albums: AlbumStorageInfo[];
|
||||
}
|
||||
|
||||
let stats = $state<StorageStats | null>(null);
|
||||
let loading = $state(true);
|
||||
let deleting = $state(false);
|
||||
let deletingAlbum = $state<string | null>(null);
|
||||
let showDeleteAllConfirm = $state(false);
|
||||
let showBreakdown = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
loadStats();
|
||||
});
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
loading = true;
|
||||
const userId = $auth.user?.id;
|
||||
if (userId) {
|
||||
stats = await invoke<StorageStats>("get_download_storage_stats", { userId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load storage stats:", error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
async function deleteAllDownloads() {
|
||||
try {
|
||||
deleting = true;
|
||||
const userId = $auth.user?.id;
|
||||
if (userId) {
|
||||
await invoke("delete_all_downloads", { userId });
|
||||
await downloads.refresh(userId);
|
||||
await loadStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete all downloads:", error);
|
||||
} finally {
|
||||
deleting = false;
|
||||
showDeleteAllConfirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAlbumDownloads(albumId: string) {
|
||||
try {
|
||||
deletingAlbum = albumId;
|
||||
const userId = $auth.user?.id;
|
||||
if (userId) {
|
||||
await invoke("delete_album_downloads", { albumId, userId });
|
||||
await downloads.refresh(userId);
|
||||
await loadStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete album downloads:", error);
|
||||
} finally {
|
||||
deletingAlbum = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAlbumClick(albumId: string) {
|
||||
if (albumId !== "unknown") {
|
||||
goto(`/library/${albumId}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-[var(--color-surface)] rounded-xl p-6 space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Storage</h2>
|
||||
{#if stats && stats.total_items > 0}
|
||||
<button
|
||||
onclick={() => (showDeleteAllConfirm = true)}
|
||||
class="px-4 py-2 text-sm bg-red-500/20 text-red-400 rounded-lg hover:bg-red-500/30 transition-colors"
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="w-6 h-6 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
{:else if stats}
|
||||
<!-- Storage Summary -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 rounded-full bg-[var(--color-jellyfin)]/20 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-[var(--color-jellyfin)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-white">{formatBytes(stats.total_bytes)}</p>
|
||||
<p class="text-sm text-gray-400">
|
||||
{stats.total_items} {stats.total_items === 1 ? "item" : "items"} downloaded
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Breakdown Toggle -->
|
||||
{#if stats.albums.length > 0}
|
||||
<button
|
||||
onclick={() => (showBreakdown = !showBreakdown)}
|
||||
class="w-full flex items-center justify-between py-3 px-4 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<span class="text-sm text-gray-300">Storage by album</span>
|
||||
<svg
|
||||
class="w-5 h-5 text-gray-400 transition-transform {showBreakdown ? 'rotate-180' : ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Album Breakdown -->
|
||||
{#if showBreakdown}
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
||||
{#each stats.albums as album (album.album_id)}
|
||||
<div class="flex items-center gap-3 p-3 bg-white/5 rounded-lg group hover:bg-white/10 transition-colors">
|
||||
<button
|
||||
onclick={() => handleAlbumClick(album.album_id)}
|
||||
class="flex-1 min-w-0 text-left"
|
||||
>
|
||||
<p class="text-sm font-medium text-white truncate group-hover:text-[var(--color-jellyfin)] transition-colors">
|
||||
{album.album_name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
{album.artist_name || "Unknown Artist"} • {album.track_count} {album.track_count === 1 ? "track" : "tracks"}
|
||||
</p>
|
||||
</button>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<span class="text-sm text-gray-400">{formatBytes(album.bytes_used)}</span>
|
||||
<button
|
||||
onclick={() => deleteAlbumDownloads(album.album_id)}
|
||||
disabled={deletingAlbum === album.album_id}
|
||||
class="p-1.5 rounded-full text-gray-400 hover:text-red-400 hover:bg-red-500/20 transition-colors disabled:opacity-50"
|
||||
title="Delete album downloads"
|
||||
>
|
||||
{#if deletingAlbum === album.album_id}
|
||||
<div class="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Empty State -->
|
||||
{#if stats.total_items === 0}
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-400 text-sm">No downloads yet</p>
|
||||
<p class="text-gray-500 text-xs mt-1">Downloaded media will appear here</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Delete All Confirmation Modal -->
|
||||
{#if showDeleteAllConfirm}
|
||||
<div class="fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-[var(--color-surface)] rounded-2xl w-full max-w-sm shadow-2xl">
|
||||
<div class="p-6 text-center">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-red-500" 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>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Delete All Downloads?</h3>
|
||||
<p class="text-sm text-gray-400 mb-6">
|
||||
This will remove {stats?.total_items || 0} downloaded items and free up {formatBytes(stats?.total_bytes || 0)} of storage. This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
onclick={() => (showDeleteAllConfirm = false)}
|
||||
class="flex-1 px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={deleteAllDownloads}
|
||||
disabled={deleting}
|
||||
class="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{#if deleting}
|
||||
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Deleting...
|
||||
{:else}
|
||||
Delete All
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user