feat(export): single-file offline keepsake viewer + guest download

Rebuild the guest "keepsake" (offline HTML gallery) so it renders when the
extracted index.html is opened over file://. Browsers block external ES-module
scripts and fetch() at origin null, so a normal multi-file SvelteKit build shows
a blank window. Build the viewer as one self-contained index.html (inlined
JS/CSS via vite-plugin-singlefile, standalone non-SvelteKit entry) and inject
the data as window.__EXPORT_DATA__; the backend embeds the built viewer via
include_dir! and writes the data global into index.html when zipping.

Also surface the guest-facing download: a feed banner and the BottomNav Export
tab, both shown once the host releases the export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-18 17:28:54 +02:00
parent f243bfe89a
commit 3654aca18b
26 changed files with 271 additions and 92 deletions

View File

@@ -99,9 +99,18 @@
onMount(async () => {
try {
const res = await fetch('./data.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data = await res.json();
// The keepsake is opened by double-clicking index.html (file://), where a
// cross-origin `fetch()` is blocked. The export injects the data as a global
// so it's available without any network request; fall back to fetching
// data.json only when the viewer is actually served over http(s).
const injected = (globalThis as { __EXPORT_DATA__?: ViewerData }).__EXPORT_DATA__;
if (injected) {
data = injected;
} else {
const res = await fetch('./data.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data = await res.json();
}
} catch {
error =
'Daten konnten nicht geladen werden. Stelle sicher, dass data.json im selben Ordner liegt.';

View File

@@ -0,0 +1,8 @@
// Standalone (non-SvelteKit) entry for the offline keepsake. Mounts the plain
// Svelte viewer component into a single, self-contained index.html so it works
// when opened by double-clicking from an extracted ZIP (file://).
import './app.css';
import { mount } from 'svelte';
import App from './routes/+page.svelte';
mount(App, { target: document.getElementById('app')! });