chore: migrate all FA icons to Lucide (@lucide/svelte)
- Replaced all active FontAwesome <span class="fas fa-*"> icons with
Lucide components across 145 files (excluding /idaa/ which is intentional)
- Fixed merge script bug: consolidated lucide-svelte imports into @lucide/svelte
- Replaced dynamic toggle patterns (fa-toggle-on/off) with ToggleRight/ToggleLeft
- Replaced fa-eye/fa-eye-slash with Eye/EyeOff
- Replaced fa-bug/fa-bug-slash with Bug/BugOff
- Replaced fa-sync fa-spin with RefreshCw + animate-spin
- Replaced fa-microchip with Cpu
- Fixed {@const} placement in element_manage_event_file_li.svelte
- Removed obsolete CSS hover rules for .unlock_icon/.lock_icon
- svelte-check: 0 errors, 0 warnings
This commit is contained in:
142
src/lib/pwa/pwa_install.svelte.ts
Normal file
142
src/lib/pwa/pwa_install.svelte.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* src/lib/pwa/pwa_install.svelte.ts
|
||||
*
|
||||
* Global PWA install prompt state using Svelte 5 universal reactivity.
|
||||
*
|
||||
* Usage:
|
||||
* - Call `pwa_install.init()` once in the root +layout.svelte (browser $effect).
|
||||
* - Import `pwa_install` in any component to read state and trigger the prompt.
|
||||
*
|
||||
* Platform behaviour:
|
||||
* - Chrome / Android / Desktop Chrome: intercepts `beforeinstallprompt`,
|
||||
* lets us show a custom button that calls the native install flow.
|
||||
* - iOS Safari: `beforeinstallprompt` never fires; we detect iOS + not-standalone
|
||||
* and show manual "Share → Add to Home Screen" instructions instead.
|
||||
* - If already running in standalone mode (i.e. already installed) → hide everything.
|
||||
*
|
||||
* Dismiss: persists for DISMISS_DAYS days via localStorage key `ae_pwa_install_dismissed`.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// `BeforeInstallPromptEvent` is non-standard and not included in TypeScript's default lib.
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
||||
}
|
||||
|
||||
const DISMISS_KEY = 'ae_pwa_install_dismissed';
|
||||
const DISMISS_DAYS = 7;
|
||||
|
||||
// --- Module-level reactive state (Svelte 5 universal reactivity) ---
|
||||
let _deferred_prompt = $state<BeforeInstallPromptEvent | null>(null);
|
||||
let _is_installed = $state(false);
|
||||
let _is_dismissed = $state(false);
|
||||
let _initialized = false; // plain boolean — not reactive, guards against double-init
|
||||
|
||||
// --- Private helpers (SSR-safe) ---
|
||||
|
||||
function _in_standalone(): boolean {
|
||||
if (!browser) return false;
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
// iOS Safari sets navigator.standalone (non-standard)
|
||||
('standalone' in window.navigator && (window.navigator as any).standalone === true)
|
||||
);
|
||||
}
|
||||
|
||||
function _is_ios(): boolean {
|
||||
if (!browser) return false;
|
||||
// Cover iPhone/iPod/iPad (incl. iPadOS 13+ which reports MacIntel with touch points)
|
||||
return (
|
||||
/iphone|ipad|ipod/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
export const pwa_install = {
|
||||
/**
|
||||
* True if the browser has captured a deferred install prompt
|
||||
* (Chrome / Android / PWA-capable desktop browsers).
|
||||
*/
|
||||
get can_prompt(): boolean {
|
||||
return _deferred_prompt !== null && !_is_installed && !_is_dismissed;
|
||||
},
|
||||
|
||||
/**
|
||||
* True if on iOS Safari and NOT already installed.
|
||||
* We cannot trigger a native prompt on iOS so we show manual instructions instead.
|
||||
*/
|
||||
get is_ios_nudge(): boolean {
|
||||
return _is_ios() && !_in_standalone() && !_is_dismissed && !_is_installed;
|
||||
},
|
||||
|
||||
/** True if the install UI should be rendered at all. */
|
||||
get should_show(): boolean {
|
||||
return pwa_install.can_prompt || pwa_install.is_ios_nudge;
|
||||
},
|
||||
|
||||
/** True once the app is confirmed installed (standalone mode or appinstalled event). */
|
||||
get is_installed(): boolean {
|
||||
return _is_installed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register browser event listeners. Call ONCE from root +layout.svelte.
|
||||
* Safe to call multiple times (idempotent).
|
||||
*/
|
||||
init(): void {
|
||||
if (!browser || _initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
// Already running as installed PWA — nothing to prompt.
|
||||
if (_in_standalone()) {
|
||||
_is_installed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore dismiss state from localStorage (with expiry check).
|
||||
const raw = localStorage.getItem(DISMISS_KEY);
|
||||
if (raw) {
|
||||
const days_since = (Date.now() - parseInt(raw, 10)) / 86_400_000;
|
||||
if (days_since < DISMISS_DAYS) {
|
||||
_is_dismissed = true;
|
||||
} else {
|
||||
// Expired — clear so the nudge can reappear.
|
||||
localStorage.removeItem(DISMISS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the deferred install prompt (Chrome/Android/Desktop Chrome).
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
_deferred_prompt = e as BeforeInstallPromptEvent;
|
||||
});
|
||||
|
||||
// Fired by the browser after the user accepts the install prompt.
|
||||
window.addEventListener('appinstalled', () => {
|
||||
_is_installed = true;
|
||||
_deferred_prompt = null;
|
||||
});
|
||||
},
|
||||
|
||||
/** Trigger the native install prompt. No-op if no deferred prompt is available. */
|
||||
async prompt(): Promise<void> {
|
||||
if (!_deferred_prompt) return;
|
||||
// Grab and clear to prevent double-fire.
|
||||
const evt = _deferred_prompt;
|
||||
_deferred_prompt = null;
|
||||
await evt.prompt();
|
||||
const { outcome } = await evt.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
_is_installed = true;
|
||||
}
|
||||
},
|
||||
|
||||
/** Dismiss the nudge; it will not re-appear for DISMISS_DAYS days. */
|
||||
dismiss(): void {
|
||||
_is_dismissed = true;
|
||||
if (browser) localStorage.setItem(DISMISS_KEY, Date.now().toString());
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user