ekke 876e884509 files for /src
Mapview.js must be placed into /src/components
2026-01-27 09:51:21 +00:00

318 lines
8.3 KiB
JavaScript

/**
* PWA Module
*
* Handles Progressive Web App functionality:
* - Service Worker registration
* - Install prompt handling
* - Offline detection
* - Update notifications
*
* Note: The Service Worker (sw.js) handles caching.
* The SharedWorker (shared-db-worker.js) handles database.
* They are separate workers with different purposes.
*/
// ============================================================================
// Service Worker Registration
// ============================================================================
let swRegistration = null;
export async function registerServiceWorker() {
if (!('serviceWorker' in navigator)) {
console.warn('[PWA] Service Workers not supported');
return null;
}
try {
swRegistration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('[PWA] Service Worker registered:', swRegistration.scope);
// Handle updates
swRegistration.addEventListener('updatefound', () => {
const newWorker = swRegistration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
console.log('[PWA] New version available');
showUpdateNotification();
}
});
});
return swRegistration;
} catch (error) {
console.error('[PWA] Service Worker registration failed:', error);
return null;
}
}
// ============================================================================
// Install Prompt
// ============================================================================
let deferredPrompt = null;
let installButton = null;
/**
* Initialize install prompt handling
* @param {string|HTMLElement} buttonSelector - Button element or selector
*/
export function initInstallPrompt(buttonSelector = '#install-btn') {
installButton = typeof buttonSelector === 'string'
? document.querySelector(buttonSelector)
: buttonSelector;
if (!installButton) {
console.warn('[PWA] Install button not found:', buttonSelector);
return;
}
// Initially hide the button
installButton.style.display = 'none';
// Listen for the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Show the install button
installButton.style.display = 'block';
console.log('[PWA] Install prompt ready');
});
// Handle install button click
installButton.addEventListener('click', async () => {
if (!deferredPrompt) {
// Show manual instructions for Safari
showManualInstallInstructions();
return;
}
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('[PWA] Install prompt outcome:', outcome);
deferredPrompt = null;
installButton.style.display = 'none';
});
// Hide button if app is already installed
window.addEventListener('appinstalled', () => {
console.log('[PWA] App installed');
deferredPrompt = null;
installButton.style.display = 'none';
});
// Check if running as installed PWA
if (window.matchMedia('(display-mode: standalone)').matches) {
installButton.style.display = 'none';
}
}
function showManualInstallInstructions() {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
let message = 'To install this app:\n\n';
if (isIOS) {
message += '1. Tap the Share button (square with arrow)\n';
message += '2. Scroll down and tap "Add to Home Screen"';
} else if (isSafari) {
message += '1. Click File menu\n';
message += '2. Click "Add to Dock"';
} else {
message += '1. Click the menu button (three dots)\n';
message += '2. Click "Install" or "Add to Home Screen"';
}
alert(message);
}
// ============================================================================
// Offline Detection
// ============================================================================
let offlineIndicator = null;
const offlineListeners = new Set();
/**
* Initialize offline detection
* @param {string|HTMLElement} indicatorSelector - Element to show when offline
*/
export function initOfflineDetection(indicatorSelector = '#offline-indicator') {
offlineIndicator = typeof indicatorSelector === 'string'
? document.querySelector(indicatorSelector)
: indicatorSelector;
// Set initial state
updateOfflineUI(!navigator.onLine);
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('[PWA] Back online');
updateOfflineUI(false);
notifyOfflineListeners(false);
});
window.addEventListener('offline', () => {
console.log('[PWA] Gone offline');
updateOfflineUI(true);
notifyOfflineListeners(true);
});
}
function updateOfflineUI(isOffline) {
if (offlineIndicator) {
offlineIndicator.style.display = isOffline ? 'block' : 'none';
}
// Also toggle a class on body for CSS styling
document.body.classList.toggle('is-offline', isOffline);
}
/**
* Subscribe to offline state changes
* @param {Function} listener - Callback(isOffline: boolean)
* @returns {Function} Unsubscribe function
*/
export function onOfflineChange(listener) {
offlineListeners.add(listener);
// Immediately call with current state
listener(!navigator.onLine);
return () => offlineListeners.delete(listener);
}
function notifyOfflineListeners(isOffline) {
for (const listener of offlineListeners) {
try {
listener(isOffline);
} catch (e) {
console.error('[PWA] Offline listener error:', e);
}
}
}
/**
* Check if currently online
*/
export function isOnline() {
return navigator.onLine;
}
// ============================================================================
// Update Handling
// ============================================================================
let updateCallback = null;
/**
* Set callback for when updates are available
* @param {Function} callback - Called when new version is ready
*/
export function onUpdateAvailable(callback) {
updateCallback = callback;
}
function showUpdateNotification() {
if (updateCallback) {
updateCallback();
return;
}
// Default behavior
if (confirm('A new version is available. Reload now?')) {
applyUpdate();
}
}
/**
* Apply pending update (reload with new version)
*/
export function applyUpdate() {
if (swRegistration?.waiting) {
swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
// ============================================================================
// Communication with Service Worker
// ============================================================================
/**
* Send a message to the service worker
* @param {Object} message - Message to send
*/
export function postToServiceWorker(message) {
navigator.serviceWorker.controller?.postMessage(message);
}
/**
* Request the service worker to cache specific modules
* @param {string[]} moduleNames - Array of module names to cache
*/
export function cacheModules(moduleNames) {
postToServiceWorker({
type: 'CACHE_MODULES',
payload: { modules: moduleNames }
});
}
/**
* Request the service worker to clear user-specific caches
* (Call this on logout)
*/
export function clearUserCaches() {
postToServiceWorker({
type: 'CLEAR_USER_CACHE'
});
}
// ============================================================================
// Auto-initialization
// ============================================================================
/**
* Initialize all PWA features
* @param {Object} options
*/
export async function initPWA(options = {}) {
const {
installButton = '#install-btn',
offlineIndicator = '#offline-indicator',
autoRegisterSW = true
} = options;
if (autoRegisterSW) {
await registerServiceWorker();
}
initInstallPrompt(installButton);
initOfflineDetection(offlineIndicator);
console.log('[PWA] Initialized');
}
// Export for direct use
export default {
registerServiceWorker,
initInstallPrompt,
initOfflineDetection,
initPWA,
isOnline,
onOfflineChange,
onUpdateAvailable,
applyUpdate,
postToServiceWorker,
cacheModules,
clearUserCaches
};