feat: auto-sync recipes and show sync button only in PWA mode

- Auto-sync recipes every 30 minutes when online in PWA mode
- Only show offline sync button when running as installed PWA
- Detect standalone mode via display-mode media query and iOS check
- Trigger initial sync on PWA install (appinstalled event)
- Listen for online event to sync when coming back online
- Store last sync time in localStorage to track sync intervals
This commit is contained in:
2026-01-29 09:58:01 +01:00
parent c86a734da0
commit 86f28fa1b7
2 changed files with 137 additions and 4 deletions

View File

@@ -19,7 +19,8 @@
onMount(async () => {
mounted = true;
await pwaStore.checkAvailability();
// Initialize PWA store (checks standalone mode, starts auto-sync if needed)
await pwaStore.initialize();
});
async function handleSync() {
@@ -147,7 +148,7 @@
}
</style>
{#if mounted}
{#if mounted && pwaStore.isStandalone}
<div class="offline-sync">
<button
class="sync-button"

View File

@@ -1,12 +1,18 @@
import { browser } from '$app/environment';
import { isOfflineDataAvailable, getLastSync, clearOfflineData } from '$lib/offline/db';
import { downloadAllRecipes, type SyncResult } from '$lib/offline/sync';
const AUTO_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes
const LAST_SYNC_KEY = 'bocken-last-sync-time';
type PWAState = {
isOfflineAvailable: boolean;
isSyncing: boolean;
lastSyncDate: string | null;
recipeCount: number;
error: string | null;
isStandalone: boolean;
isInitialized: boolean;
};
function createPWAStore() {
@@ -15,10 +21,60 @@ function createPWAStore() {
isSyncing: false,
lastSyncDate: null,
recipeCount: 0,
error: null
error: null,
isStandalone: false,
isInitialized: false
});
return {
let autoSyncInterval: ReturnType<typeof setInterval> | null = null;
// Check if running as installed PWA (standalone mode)
function checkStandaloneMode(): boolean {
if (!browser) return false;
// Check display-mode media query (works on most browsers)
const standaloneQuery = window.matchMedia('(display-mode: standalone)');
if (standaloneQuery.matches) return true;
// Check iOS Safari standalone mode
if ('standalone' in navigator && (navigator as any).standalone === true) return true;
// Check if launched from home screen on Android
if (document.referrer.includes('android-app://')) return true;
return false;
}
// Check if we should auto-sync (online and enough time has passed)
function shouldAutoSync(): boolean {
if (!browser || !navigator.onLine) return false;
const lastSync = localStorage.getItem(LAST_SYNC_KEY);
if (!lastSync) return true; // Never synced, should sync
const lastSyncTime = parseInt(lastSync, 10);
const now = Date.now();
return now - lastSyncTime >= AUTO_SYNC_INTERVAL;
}
// Record sync time
function recordSyncTime() {
if (browser) {
localStorage.setItem(LAST_SYNC_KEY, Date.now().toString());
}
}
// Auto-sync if conditions are met
async function autoSync() {
if (!navigator.onLine || state.isSyncing) return;
if (shouldAutoSync()) {
console.log('[PWA] Auto-syncing recipes...');
await store.syncForOffline();
}
}
const store = {
get isOfflineAvailable() {
return state.isOfflineAvailable;
},
@@ -34,6 +90,75 @@ function createPWAStore() {
get error() {
return state.error;
},
get isStandalone() {
return state.isStandalone;
},
get isInitialized() {
return state.isInitialized;
},
async initialize() {
if (!browser || state.isInitialized) return;
state.isStandalone = checkStandaloneMode();
await this.checkAvailability();
// Listen for display mode changes (e.g., when installed)
const standaloneQuery = window.matchMedia('(display-mode: standalone)');
standaloneQuery.addEventListener('change', (e) => {
state.isStandalone = e.matches;
if (e.matches) {
// Just became standalone (installed), trigger initial sync
console.log('[PWA] App installed, starting initial sync...');
this.syncForOffline();
}
});
// Listen for app installed event
window.addEventListener('appinstalled', () => {
console.log('[PWA] App installed event received');
state.isStandalone = true;
this.syncForOffline();
});
// Start auto-sync if in standalone mode
if (state.isStandalone) {
this.startAutoSync();
// Do initial sync if needed
if (shouldAutoSync()) {
this.syncForOffline();
}
}
// Listen for online/offline events
window.addEventListener('online', () => {
if (state.isStandalone && shouldAutoSync()) {
autoSync();
}
});
state.isInitialized = true;
},
startAutoSync() {
if (autoSyncInterval) return; // Already running
// Check every 5 minutes if we should sync
autoSyncInterval = setInterval(() => {
autoSync();
}, 5 * 60 * 1000); // Check every 5 minutes
console.log('[PWA] Auto-sync enabled (every 30 minutes)');
},
stopAutoSync() {
if (autoSyncInterval) {
clearInterval(autoSyncInterval);
autoSyncInterval = null;
console.log('[PWA] Auto-sync disabled');
}
},
async checkAvailability() {
try {
@@ -68,6 +193,7 @@ function createPWAStore() {
state.isOfflineAvailable = true;
state.lastSyncDate = new Date().toISOString();
state.recipeCount = result.recipeCount;
recordSyncTime();
} else {
state.error = result.error || 'Sync failed';
}
@@ -85,12 +211,18 @@ function createPWAStore() {
state.lastSyncDate = null;
state.recipeCount = 0;
state.error = null;
// Clear sync time so next sync happens immediately
if (browser) {
localStorage.removeItem(LAST_SYNC_KEY);
}
} catch (error) {
console.error('Failed to clear offline data:', error);
state.error = error instanceof Error ? error.message : 'Failed to clear data';
}
}
};
return store;
}
export const pwaStore = createPWAStore();