feat(webapp): add offline shell and iPhone install guidance
This commit is contained in:
@@ -1,52 +1,98 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const IOS_HINT_DISMISSED_KEY = 'phonecam-ios-install-hint-dismissed';
|
||||
|
||||
const createInitialState = () => ({
|
||||
installAvailable: false,
|
||||
updateAvailable: false,
|
||||
offlineReady: false,
|
||||
installing: false
|
||||
installing: false,
|
||||
isOffline: false,
|
||||
isStandalone: false,
|
||||
showIosInstallHint: false
|
||||
});
|
||||
|
||||
export const pwaState = writable(createInitialState());
|
||||
|
||||
let deferredInstallPrompt: any = null;
|
||||
|
||||
export const setInstallPrompt = (event: any) => {
|
||||
deferredInstallPrompt = event;
|
||||
const setPwaState = (partial: Record<string, unknown>) => {
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
installAvailable: true
|
||||
...partial
|
||||
}));
|
||||
};
|
||||
|
||||
export const setInstallPrompt = (event: any) => {
|
||||
deferredInstallPrompt = event;
|
||||
setPwaState({
|
||||
installAvailable: true
|
||||
});
|
||||
};
|
||||
|
||||
export const clearInstallPrompt = () => {
|
||||
deferredInstallPrompt = null;
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
setPwaState({
|
||||
installAvailable: false,
|
||||
installing: false
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
export const setUpdateAvailable = (updateAvailable: boolean) => {
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
updateAvailable
|
||||
}));
|
||||
setPwaState({ updateAvailable });
|
||||
};
|
||||
|
||||
export const setOfflineReady = (offlineReady: boolean) => {
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
offlineReady
|
||||
}));
|
||||
setPwaState({ offlineReady });
|
||||
};
|
||||
|
||||
export const dismissOfflineReady = () => {
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
offlineReady: false
|
||||
}));
|
||||
setPwaState({ offlineReady: false });
|
||||
};
|
||||
|
||||
export const setOnlineStatus = (isOnline: boolean) => {
|
||||
setPwaState({ isOffline: !isOnline });
|
||||
};
|
||||
|
||||
export const setStandaloneMode = (isStandalone: boolean) => {
|
||||
setPwaState({ isStandalone });
|
||||
};
|
||||
|
||||
const isIosSafariInstallable = () => {
|
||||
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const isIos = /iPad|iPhone|iPod/.test(userAgent);
|
||||
const isWebkit = /WebKit/.test(userAgent);
|
||||
const isCriOS = /CriOS/.test(userAgent);
|
||||
const isFxiOS = /FxiOS/.test(userAgent);
|
||||
|
||||
return isIos && isWebkit && !isCriOS && !isFxiOS;
|
||||
};
|
||||
|
||||
export const syncInstallHelpState = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStandalone =
|
||||
window.matchMedia?.('(display-mode: standalone)').matches || (window.navigator as any)?.standalone === true;
|
||||
const dismissed =
|
||||
typeof localStorage !== 'undefined' && localStorage.getItem(IOS_HINT_DISMISSED_KEY) === 'true';
|
||||
|
||||
setPwaState({
|
||||
isStandalone,
|
||||
showIosInstallHint: isIosSafariInstallable() && !isStandalone && !dismissed && !deferredInstallPrompt
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissIosInstallHint = () => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(IOS_HINT_DISMISSED_KEY, 'true');
|
||||
}
|
||||
setPwaState({ showIosInstallHint: false });
|
||||
};
|
||||
|
||||
export const promptInstall = async () => {
|
||||
@@ -54,21 +100,20 @@ export const promptInstall = async () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
setPwaState({
|
||||
installing: true
|
||||
}));
|
||||
});
|
||||
|
||||
try {
|
||||
await deferredInstallPrompt.prompt();
|
||||
const choice = await deferredInstallPrompt.userChoice;
|
||||
clearInstallPrompt();
|
||||
syncInstallHelpState();
|
||||
return choice?.outcome === 'accepted';
|
||||
} catch (error) {
|
||||
pwaState.update((state) => ({
|
||||
...state,
|
||||
setPwaState({
|
||||
installing: false
|
||||
}));
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { appController } from '$lib/app/controller';
|
||||
import { dismissOfflineReady, promptInstall, pwaState } from '$lib/pwa';
|
||||
import { dismissIosInstallHint, 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';
|
||||
@@ -40,6 +40,11 @@
|
||||
const reloadForUpdate = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
const offlineTitle = () => ($appState.session ? 'Offline Shell Mode' : 'Offline');
|
||||
const offlineDescription = () =>
|
||||
$appState.session
|
||||
? 'Cached screens remain available, but sign-in refresh, live monitoring, camera control, and realtime updates need connectivity.'
|
||||
: 'You can open the cached shell, but sign-in and device setup need an internet connection.';
|
||||
</script>
|
||||
|
||||
<div data-sim-page={pageKey} class="flex h-full w-full">
|
||||
@@ -87,6 +92,20 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if $pwaState.showIosInstallHint}
|
||||
<Alert class="glass-card border-white/10 bg-background/95 shadow-lg">
|
||||
<AlertTitle class="text-xs">Install on iPhone</AlertTitle>
|
||||
<AlertDescription class="pr-8 text-xs text-gray-300">
|
||||
In Safari, tap the Share button, then choose “Add to Home Screen” to install PhoneCam.
|
||||
</AlertDescription>
|
||||
<AlertAction>
|
||||
<Button variant="ghost" size="sm" class="text-gray-300" onclick={dismissIosInstallHint}>
|
||||
Got it
|
||||
</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>
|
||||
@@ -255,6 +274,16 @@
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
|
||||
{#if $pwaState.isOffline}
|
||||
<div class="px-4 pt-4 md:px-8 md:pt-6 lg:px-10">
|
||||
<Alert class="glass-card border-amber-500/20 bg-[#141418]/95 shadow-lg">
|
||||
<AlertTitle class="text-xs uppercase tracking-wide text-amber-300">{offlineTitle()}</AlertTitle>
|
||||
<AlertDescription class="text-xs text-gray-300">
|
||||
{offlineDescription()}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10 relative">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user