First working POC

This commit is contained in:
2026-01-26 22:21:54 +01:00
commit cfddc1edea
255 changed files with 77606 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { invoke } from "@tauri-apps/api/core";
import { downloads, activeDownloads, completedDownloads, pendingDownloads, failedDownloads } from "$lib/stores/downloads";
import { auth } from "$lib/stores/auth";
import DownloadItem from "$lib/components/downloads/DownloadItem.svelte";
import StorageManagement from "$lib/components/downloads/StorageManagement.svelte";
type TabType = "active" | "completed";
let activeTab = $state<TabType>("active");
let loading = $state(true);
onMount(async () => {
await loadDownloads();
});
async function loadDownloads() {
try {
loading = true;
const userId = $auth.user?.id;
if (userId) {
await downloads.refresh(userId);
}
} catch (error) {
console.error("Failed to load downloads:", error);
} finally {
loading = false;
}
}
const activeDownloadsList = $derived($activeDownloads.concat($pendingDownloads));
const completedDownloadsList = $derived($completedDownloads.concat($failedDownloads));
async function pauseAll() {
for (const download of $activeDownloads) {
if (download.status === "downloading") {
try {
await downloads.pause(download.id);
} catch (error) {
console.error(`Failed to pause download ${download.id}:`, error);
}
}
}
// Refresh to update UI with new states
const userId = $auth.user?.id;
if (userId) {
await downloads.refresh(userId);
}
}
async function resumeAll() {
for (const download of activeDownloadsList) {
if (download.status === "paused" || download.status === "failed") {
try {
await downloads.resume(download.id);
} catch (error) {
console.error(`Failed to resume download ${download.id}:`, error);
}
}
}
// Refresh to update UI with new states
const userId = $auth.user?.id;
if (userId) {
await downloads.refresh(userId);
}
}
async function clearCompleted() {
for (const download of $completedDownloads) {
try {
await downloads.delete(download.id);
} catch (error) {
console.error(`Failed to delete download ${download.id}:`, error);
}
}
// Refresh to update UI
const userId = $auth.user?.id;
if (userId) {
await downloads.refresh(userId);
}
}
async function clearStale() {
try {
const userId = $auth.user?.id;
if (userId) {
await invoke("clear_stale_downloads", { userId });
await downloads.refresh(userId);
}
} catch (error) {
console.error("Failed to clear stale downloads:", error);
}
}
</script>
<div class="max-w-4xl mx-auto space-y-6 p-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<button
onclick={() => goto("/library")}
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-white transition-colors"
title="Back to library"
>
<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="M15 19l-7-7 7-7"
/>
</svg>
</button>
<div>
<h1 class="text-3xl font-bold text-white mb-2">Downloads</h1>
<p class="text-gray-400">Manage your offline media downloads</p>
</div>
</div>
<button
onclick={loadDownloads}
class="p-2 rounded-full hover:bg-white/10 text-gray-400 hover:text-white transition-colors"
title="Refresh downloads"
>
<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="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>
</div>
<!-- Storage Management -->
<StorageManagement />
<!-- Tabs -->
<div class="flex gap-4 border-b border-gray-700">
<button
onclick={() => (activeTab = "active")}
class="pb-3 px-1 font-medium transition-colors relative {activeTab === 'active'
? 'text-[var(--color-jellyfin)]'
: 'text-gray-400 hover:text-white'}"
>
Active
{#if activeDownloadsList.length > 0}
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-blue-500/20 text-blue-400">
{activeDownloadsList.length}
</span>
{/if}
{#if activeTab === "active"}
<div class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-jellyfin)]"></div>
{/if}
</button>
<button
onclick={() => (activeTab = "completed")}
class="pb-3 px-1 font-medium transition-colors relative {activeTab === 'completed'
? 'text-[var(--color-jellyfin)]'
: 'text-gray-400 hover:text-white'}"
>
Completed
{#if completedDownloadsList.length > 0}
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-green-500/20 text-green-400">
{completedDownloadsList.length}
</span>
{/if}
{#if activeTab === "completed"}
<div class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-jellyfin)]"></div>
{/if}
</button>
</div>
<!-- Color coding legend -->
<div class="bg-[var(--color-surface)] rounded-lg p-3 border border-gray-700">
<div class="flex items-center gap-6 text-xs text-gray-400">
<div class="flex items-center gap-2">
<div class="w-1 h-4 rounded bg-green-500/50"></div>
<span><span class="text-green-400 font-medium">Green</span> = Downloads you chose</span>
</div>
<div class="flex items-center gap-2">
<div class="w-1 h-4 rounded bg-blue-500/50"></div>
<span><span class="text-blue-400 font-medium">Blue</span> = Auto-cached content</span>
</div>
</div>
</div>
{#if loading}
<div class="text-center py-12 text-gray-400">
<p>Loading downloads...</p>
</div>
{:else}
<!-- Bulk Actions -->
{#if activeTab === "active" && activeDownloadsList.length > 0}
<div class="flex gap-3">
<button
onclick={pauseAll}
class="px-4 py-2 bg-[var(--color-surface)] text-white rounded-lg font-medium hover:bg-[var(--color-surface-hover)] transition-colors text-sm"
>
Pause All
</button>
<button
onclick={resumeAll}
class="px-4 py-2 bg-[var(--color-surface)] text-white rounded-lg font-medium hover:bg-[var(--color-surface-hover)] transition-colors text-sm"
>
Resume All
</button>
<button
onclick={clearStale}
class="px-4 py-2 bg-yellow-500/20 text-yellow-400 rounded-lg font-medium hover:bg-yellow-500/30 transition-colors text-sm"
title="Remove all pending, paused, and failed downloads"
>
Clear Stale
</button>
</div>
{:else if activeTab === "completed" && completedDownloadsList.length > 0}
<div class="flex gap-3">
<button
onclick={clearCompleted}
class="px-4 py-2 bg-red-500/20 text-red-400 rounded-lg font-medium hover:bg-red-500/30 transition-colors text-sm"
>
Clear Completed
</button>
<button
onclick={async () => {
const userId = $auth.user?.id;
if (userId) {
if (confirm('Delete ALL downloads (including completed)? This cannot be undone.')) {
await invoke("delete_all_downloads", { userId });
await downloads.refresh(userId);
}
}
}}
class="px-4 py-2 bg-red-600/30 text-red-300 rounded-lg font-medium hover:bg-red-600/40 transition-colors text-sm border border-red-500/50"
title="Delete all downloads and files"
>
Delete All Content
</button>
</div>
{/if}
<!-- Downloads List -->
<div class="space-y-3">
{#if activeTab === "active"}
{#if activeDownloadsList.length === 0}
<div class="bg-[var(--color-surface)] rounded-lg p-12 text-center">
<svg
class="w-16 h-16 mx-auto text-gray-600 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 4v12m0 0l-4-4m4 4l4-4m-9 7h10"
/>
</svg>
<p class="text-gray-400 text-lg font-medium">No active downloads</p>
<p class="text-gray-500 text-sm mt-2">
Downloads you start will appear here
</p>
</div>
{:else}
{#each activeDownloadsList as download (download.id)}
<DownloadItem {download} />
{/each}
{/if}
{:else}
{#if completedDownloadsList.length === 0}
<div class="bg-[var(--color-surface)] rounded-lg p-12 text-center">
<svg
class="w-16 h-16 mx-auto text-gray-600 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M5 13l4 4L19 7"
/>
</svg>
<p class="text-gray-400 text-lg font-medium">No completed downloads</p>
<p class="text-gray-500 text-sm mt-2">
Finished downloads will appear here
</p>
</div>
{:else}
{#each completedDownloadsList as download (download.id)}
<DownloadItem {download} />
{/each}
{/if}
{/if}
</div>
<!-- Info Box -->
{#if activeDownloadsList.length === 0 && completedDownloadsList.length === 0}
<div class="bg-blue-900/20 border border-blue-800 rounded-lg p-4">
<div class="flex gap-3">
<svg
class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
<div class="text-sm text-blue-300">
<p class="font-semibold mb-1">Getting started with downloads:</p>
<ul class="list-disc list-inside space-y-1 text-blue-200">
<li>Look for the download icon next to tracks, albums, and playlists</li>
<li>Downloaded media is available for offline playback</li>
<li>Configure download settings in the Settings page</li>
</ul>
</div>
</div>
</div>
{/if}
{/if}
</div>