196 lines
6.6 KiB
Svelte
196 lines
6.6 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { getToken } from '$lib/auth';
|
|
import { api } from '$lib/api';
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { connectSse, disconnectSse, onSseEvent } from '$lib/sse';
|
|
|
|
interface JobStatus {
|
|
status: 'locked' | 'pending' | 'running' | 'done' | 'failed';
|
|
progress_pct: number;
|
|
}
|
|
|
|
interface ExportStatus {
|
|
released: boolean;
|
|
zip: JobStatus;
|
|
html: JobStatus;
|
|
}
|
|
|
|
let status = $state<ExportStatus | null>(null);
|
|
let showHtmlGuide = $state(false);
|
|
let loading = $state(true);
|
|
|
|
let unsubscribers: (() => void)[] = [];
|
|
|
|
onMount(async () => {
|
|
if (!getToken()) {
|
|
goto('/join');
|
|
return;
|
|
}
|
|
|
|
await loadStatus();
|
|
connectSse();
|
|
|
|
unsubscribers.push(
|
|
onSseEvent('export-progress', async () => {
|
|
await loadStatus();
|
|
}),
|
|
onSseEvent('export-available', async () => {
|
|
await loadStatus();
|
|
})
|
|
);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
disconnectSse();
|
|
for (const unsub of unsubscribers) unsub();
|
|
});
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
status = await api.get<ExportStatus>('/export/status');
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function jobLabel(type: 'zip' | 'html'): string {
|
|
return type === 'zip' ? 'ZIP-Archiv (Gallery.zip)' : 'HTML-Viewer (Memories.zip)';
|
|
}
|
|
|
|
function statusText(job: JobStatus): string {
|
|
switch (job.status) {
|
|
case 'locked': return 'Noch nicht freigegeben';
|
|
case 'pending': return 'Wird vorbereitet…';
|
|
case 'running': return `Wird erstellt (${job.progress_pct} %)`;
|
|
case 'done': return 'Bereit zum Download';
|
|
case 'failed': return 'Fehlgeschlagen';
|
|
}
|
|
}
|
|
|
|
function downloadZip() {
|
|
window.location.href = '/api/v1/export/zip';
|
|
}
|
|
|
|
function downloadHtml() {
|
|
showHtmlGuide = true;
|
|
}
|
|
|
|
function confirmHtmlDownload() {
|
|
showHtmlGuide = false;
|
|
window.location.href = '/api/v1/export/html';
|
|
}
|
|
</script>
|
|
|
|
<!-- HTML guide modal -->
|
|
{#if showHtmlGuide}
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
<div class="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
|
|
<h2 class="mb-3 text-lg font-bold text-gray-900">Hinweis zum HTML-Viewer</h2>
|
|
<ol class="mb-4 space-y-2 text-sm text-gray-700">
|
|
<li class="flex gap-2"><span class="font-bold text-blue-600">1.</span> ZIP-Datei entpacken (Windows: Rechtsklick → "Alle extrahieren"; Mac: Doppelklick).</li>
|
|
<li class="flex gap-2"><span class="font-bold text-blue-600">2.</span> <strong>Memories.html</strong> im Browser öffnen.</li>
|
|
<li class="flex gap-2"><span class="font-bold text-blue-600">3.</span> Kein Internet nötig — alles ist lokal gespeichert.</li>
|
|
</ol>
|
|
<p class="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
|
Tipp: Am besten im WLAN herunterladen — die Datei kann mehrere GB groß sein.
|
|
</p>
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={() => (showHtmlGuide = false)}
|
|
class="flex-1 rounded-lg border border-gray-300 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onclick={confirmHtmlDownload}
|
|
class="flex-1 rounded-lg bg-blue-600 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
>
|
|
Herunterladen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="min-h-screen bg-gray-50">
|
|
<div class="border-b border-gray-200 bg-white">
|
|
<div class="mx-auto flex max-w-lg items-center justify-between px-4 py-4">
|
|
<h1 class="text-xl font-bold text-gray-900">Export</h1>
|
|
<a href="/feed" class="text-sm text-blue-600 hover:underline">Zur Galerie</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mx-auto max-w-lg space-y-4 p-4">
|
|
{#if loading}
|
|
<div class="py-16 text-center text-gray-400">Laden…</div>
|
|
{:else if !status?.released}
|
|
<div class="rounded-xl border border-gray-200 bg-white p-6 text-center">
|
|
<svg class="mx-auto mb-3 h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
<p class="font-medium text-gray-700">Export noch nicht verfügbar</p>
|
|
<p class="mt-1 text-sm text-gray-500">Schau nach der Veranstaltung noch einmal vorbei.</p>
|
|
</div>
|
|
{:else if status}
|
|
<p class="text-sm text-gray-500">Wähle dein bevorzugtes Format:</p>
|
|
|
|
<!-- ZIP card -->
|
|
<div class="rounded-xl border border-gray-200 bg-white p-5">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="min-w-0">
|
|
<h2 class="font-semibold text-gray-900">ZIP-Archiv</h2>
|
|
<p class="mt-0.5 text-sm text-gray-500">Alle Original-Fotos und Videos in strukturierten Ordnern.</p>
|
|
<p class="mt-1 text-xs {status.zip.status === 'done' ? 'text-green-600' : status.zip.status === 'failed' ? 'text-red-500' : 'text-gray-400'}">
|
|
{statusText(status.zip)}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onclick={downloadZip}
|
|
disabled={status.zip.status !== 'done'}
|
|
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.zip.status === 'done' ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-gray-100 text-gray-400 cursor-not-allowed'}"
|
|
>
|
|
Download
|
|
</button>
|
|
</div>
|
|
{#if status.zip.status === 'running'}
|
|
<div class="mt-3">
|
|
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200">
|
|
<div class="h-full rounded-full bg-blue-500 transition-all" style="width: {status.zip.progress_pct}%"></div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- HTML card -->
|
|
<div class="rounded-xl border border-gray-200 bg-white p-5">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="min-w-0">
|
|
<h2 class="font-semibold text-gray-900">HTML-Viewer</h2>
|
|
<p class="mt-0.5 text-sm text-gray-500">Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig.</p>
|
|
<p class="mt-1 text-xs {status.html.status === 'done' ? 'text-green-600' : status.html.status === 'failed' ? 'text-red-500' : 'text-gray-400'}">
|
|
{statusText(status.html)}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onclick={downloadHtml}
|
|
disabled={status.html.status !== 'done'}
|
|
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.html.status === 'done' ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-gray-100 text-gray-400 cursor-not-allowed'}"
|
|
>
|
|
Download
|
|
</button>
|
|
</div>
|
|
{#if status.html.status === 'running'}
|
|
<div class="mt-3">
|
|
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200">
|
|
<div class="h-full rounded-full bg-blue-500 transition-all" style="width: {status.html.progress_pct}%"></div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|