feat(webapp): add installable PWA support
This commit is contained in:
74
WebApp/src/lib/pwa.ts
Normal file
74
WebApp/src/lib/pwa.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user