feat(webapp): add installable PWA support

This commit is contained in:
2026-03-19 16:40:00 +00:00
parent 65d113046a
commit 22608b80f1
12 changed files with 868 additions and 1 deletions

74
WebApp/src/lib/pwa.ts Normal file
View File

@@ -0,0 +1,74 @@
import { writable } from 'svelte/store';
const createInitialState = () => ({
installAvailable: false,
updateAvailable: false,
offlineReady: false,
installing: false
});
export const pwaState = writable(createInitialState());
let deferredInstallPrompt: any = null;
export const setInstallPrompt = (event: any) => {
deferredInstallPrompt = event;
pwaState.update((state) => ({
...state,
installAvailable: true
}));
};
export const clearInstallPrompt = () => {
deferredInstallPrompt = null;
pwaState.update((state) => ({
...state,
installAvailable: false,
installing: false
}));
};
export const setUpdateAvailable = (updateAvailable: boolean) => {
pwaState.update((state) => ({
...state,
updateAvailable
}));
};
export const setOfflineReady = (offlineReady: boolean) => {
pwaState.update((state) => ({
...state,
offlineReady
}));
};
export const dismissOfflineReady = () => {
pwaState.update((state) => ({
...state,
offlineReady: false
}));
};
export const promptInstall = async () => {
if (!deferredInstallPrompt) {
return false;
}
pwaState.update((state) => ({
...state,
installing: true
}));
try {
await deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice;
clearInstallPrompt();
return choice?.outcome === 'accepted';
} catch (error) {
pwaState.update((state) => ({
...state,
installing: false
}));
throw error;
}
};

View File

@@ -1,6 +1,7 @@
<script lang="ts">
// @ts-nocheck
import { appController } from '$lib/app/controller';
import { dismissOfflineReady, promptInstall, pwaState } from '$lib/pwa';
import { appState, unreadNotificationsCount } from '$lib/app/store';
import { Alert, AlertAction, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Avatar, AvatarFallback } from '$lib/components/ui/avatar/index.js';
@@ -36,6 +37,9 @@
const userInitial = () => ($appState.session?.user?.name?.[0] || '?').toUpperCase();
const navVariant = (active: boolean) => (active ? 'secondary' : 'ghost');
const toastTitle = (type: string) => (type ? `${type[0].toUpperCase()}${type.slice(1)}` : 'Notice');
const reloadForUpdate = () => {
window.location.reload();
};
</script>
<div data-sim-page={pageKey} class="flex h-full w-full">
@@ -62,6 +66,54 @@
</AlertAction>
</Alert>
{/each}
{#if $pwaState.installAvailable}
<Alert class="glass-card border-white/10 bg-background/95 shadow-lg">
<AlertTitle class="text-xs">Install App</AlertTitle>
<AlertDescription class="pr-8 text-xs text-gray-300">
Add PhoneCam to your home screen for a more native dashboard experience.
</AlertDescription>
<AlertAction>
<Button
variant="outline"
size="sm"
class="border-white/10 text-gray-200"
disabled={$pwaState.installing}
onclick={() => void promptInstall()}
>
{$pwaState.installing ? 'Installing...' : 'Install'}
</Button>
</AlertAction>
</Alert>
{/if}
{#if $pwaState.updateAvailable}
<Alert class="glass-card border-blue-500/20 bg-background/95 shadow-lg">
<AlertTitle class="text-xs">Update Ready</AlertTitle>
<AlertDescription class="pr-8 text-xs text-gray-300">
A newer version of PhoneCam is ready. Reload to use the latest app shell.
</AlertDescription>
<AlertAction>
<Button variant="outline" size="sm" class="border-blue-500/30 text-gray-200" onclick={reloadForUpdate}>
Reload
</Button>
</AlertAction>
</Alert>
{/if}
{#if $pwaState.offlineReady}
<Alert class="glass-card border-emerald-500/20 bg-background/95 shadow-lg">
<AlertTitle class="text-xs">Offline Shell Ready</AlertTitle>
<AlertDescription class="pr-8 text-xs text-gray-300">
The app shell is now cached for quicker relaunches, but live camera features still need connectivity.
</AlertDescription>
<AlertAction>
<Button variant="ghost" size="sm" class="text-gray-300" onclick={dismissOfflineReady}>
Dismiss
</Button>
</AlertAction>
</Alert>
{/if}
</div>
<aside