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.

+ +
+ +