From 8bb208a613d81c2783f2d59ec38eea8577ac33d1 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:57:49 +0200 Subject: [PATCH] feat(pwa): Admin-Tab "App" mit Install + Sync + Cache-Reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer vierter Admin-Tab (Smartphone-Icon) mit drei Karten: 1. Installieren — fängt beforeinstallprompt (Android), zeigt iOS-Teilen-Hinweis, sonst Info "nicht verfügbar". 2. Offline-Synchronisation — Status + "Jetzt synchronisieren"- Button, disabled wenn offline. 3. Cache — "Offline-Cache leeren" löscht alle kochwas-*-Caches via caches.keys() + delete. install-prompt.svelte.ts hält das deferred-Event und die Plattform (android/ios/other) per UA-Detection. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/client/install-prompt.svelte.ts | 44 ++++++++ src/routes/+layout.svelte | 2 + src/routes/admin/+layout.svelte | 5 +- src/routes/admin/app/+page.svelte | 142 ++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/lib/client/install-prompt.svelte.ts create mode 100644 src/routes/admin/app/+page.svelte diff --git a/src/lib/client/install-prompt.svelte.ts b/src/lib/client/install-prompt.svelte.ts new file mode 100644 index 0000000..505c75d --- /dev/null +++ b/src/lib/client/install-prompt.svelte.ts @@ -0,0 +1,44 @@ +// Captures the beforeinstallprompt event (Android Chrome) and holds it for +// manual triggering by the user. On iOS Safari this event does not exist — +// we detect the browser via UserAgent and show an info hint instead. +class InstallPromptStore { + available = $state(false); + platform = $state<'android' | 'ios' | 'other'>('other'); + private deferred: BeforeInstallPromptEvent | null = null; + + init(): void { + if (typeof window === 'undefined') return; + this.platform = detectPlatform(); + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + this.deferred = e as BeforeInstallPromptEvent; + this.available = true; + }); + window.addEventListener('appinstalled', () => { + this.deferred = null; + this.available = false; + }); + } + + async prompt(): Promise { + if (!this.deferred) return; + await this.deferred.prompt(); + this.deferred = null; + this.available = false; + } +} + +function detectPlatform(): 'android' | 'ios' | 'other' { + const ua = navigator.userAgent; + if (/iPhone|iPad|iPod/i.test(ua)) return 'ios'; + if (/Android/i.test(ua)) return 'android'; + return 'other'; +} + +// Minimal type for the Chrome-specific event +type BeforeInstallPromptEvent = Event & { + prompt: () => Promise; + userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; +}; + +export const installPrompt = new InstallPromptStore(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4a2866d..6450400 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,6 +15,7 @@ import Toast from '$lib/components/Toast.svelte'; import SyncIndicator from '$lib/components/SyncIndicator.svelte'; import { network } from '$lib/client/network.svelte'; + import { installPrompt } from '$lib/client/install-prompt.svelte'; import { registerServiceWorker } from '$lib/client/sw-register'; import type { SearchHit } from '$lib/server/recipes/search-local'; import type { WebHit } from '$lib/server/search/searxng'; @@ -207,6 +208,7 @@ void searchFilterStore.load(); void pwaStore.init(); network.init(); + installPrompt.init(); void registerServiceWorker(); document.addEventListener('click', handleClickOutside); document.addEventListener('keydown', handleKey); diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index dc11c60..febb7ed 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -1,13 +1,14 @@ diff --git a/src/routes/admin/app/+page.svelte b/src/routes/admin/app/+page.svelte new file mode 100644 index 0000000..47d4afe --- /dev/null +++ b/src/routes/admin/app/+page.svelte @@ -0,0 +1,142 @@ + + +

App

+

Einstellungen für die Installation und den Offline-Cache.

+ +
+

Installieren

+ {#if installPrompt.platform === 'ios'} +

+ Öffne das Teilen-Menü in Safari und wähle „Zum Home-Bildschirm hinzufügen". +

+ {:else if installPrompt.available} + + {:else} +

+ Installation aktuell nicht möglich (entweder schon installiert oder Browser unterstützt es + nicht). +

+ {/if} +
+ +
+

Offline-Synchronisation

+ {#if syncStatus.state.kind === 'syncing'} +

Lädt gerade: {syncStatus.state.current}/{syncStatus.state.total} Rezepte.

+ {:else if syncStatus.state.kind === 'error'} +

Fehler: {syncStatus.state.message}

+ {:else} +

Zuletzt synchronisiert: {formatTime(syncStatus.lastSynced)}

+ {/if} + +
+ +
+

Cache

+

Nur bei Problemen: entfernt alle Offline-Daten.

+ +
+ +