diff --git a/src/components/PwaUpdater.tsx b/src/components/PwaUpdater.tsx new file mode 100644 index 0000000..d86cae6 --- /dev/null +++ b/src/components/PwaUpdater.tsx @@ -0,0 +1,110 @@ +import { type Component, createEffect, onCleanup } from 'solid-js'; +import { useRegisterSW } from 'virtual:pwa-register/solid'; + +// We export a function so other parts of the app can manually trigger an update +let manualUpdateFn: (() => void) | undefined; + +export const forceServiceWorkerUpdate = () => { + if (manualUpdateFn) { + manualUpdateFn(); + } else { + console.warn("PWA Updater not initialized yet"); + } +}; + +export const PwaUpdater: Component = () => { + let registration: ServiceWorkerRegistration | undefined; + + const { + needRefresh: [needRefresh], + updateServiceWorker, + } = useRegisterSW({ + onRegistered(r: ServiceWorkerRegistration | undefined) { + console.log('SW Registered:', r); + registration = r; + + // Periodically check for updates (every 1 hour) + if (r) { + setInterval(() => { + console.log('Periodic SW update check...'); + r.update(); + }, 60 * 60 * 1000); + } + }, + onRegisterError(error: Error | any) { + console.error('SW registration error', error); + }, + }); + + createEffect(() => { + // Expose function globally + manualUpdateFn = () => { + if (needRefresh()) { + console.log('User manually forced an update. Reloading now...'); + updateServiceWorker(true); + } else if (registration) { + console.log('User manually asked to check for updates...'); + registration.update(); + } + }; + }); + + createEffect(() => { + // Check for updates when the app regains focus/visibility + const handleVisibilityFocus = () => { + if (document.visibilityState === 'visible' && registration) { + console.log('App became visible, checking for SW updates...'); + registration.update(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityFocus); + window.addEventListener('focus', handleVisibilityFocus); + + onCleanup(() => { + document.removeEventListener('visibilitychange', handleVisibilityFocus); + window.removeEventListener('focus', handleVisibilityFocus); + }); + }); + + createEffect(() => { + if (needRefresh()) { + console.log('New PWA version available. Monitoring for idle/hidden state to update...'); + + let idleTimeout: number | undefined; + + const triggerUpdate = () => { + console.log('Triggering PWA update...'); + updateServiceWorker(true); + }; + + const resetIdle = () => { + if (idleTimeout) window.clearTimeout(idleTimeout); + // 10 minutes idle + idleTimeout = window.setTimeout(triggerUpdate, 10 * 60 * 1000); + }; + + const handleVisibility = () => { + if (document.visibilityState === 'hidden') { + triggerUpdate(); + } + }; + + // Start idle timer + resetIdle(); + + // Listen for user activity + const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart']; + events.forEach(e => window.addEventListener(e, resetIdle, { passive: true })); + document.addEventListener('visibilitychange', handleVisibility); + + onCleanup(() => { + if (idleTimeout) window.clearTimeout(idleTimeout); + events.forEach(e => window.removeEventListener(e, resetIdle)); + document.removeEventListener('visibilitychange', handleVisibility); + }); + } + }); + + return null; +}; diff --git a/src/index.tsx b/src/index.tsx index 81ed90b..9febf77 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,12 +3,14 @@ import './index.css' import App from './App.tsx' import { ThemeProvider } from './components/ThemeProvider' import { Toaster } from './components/ui/sonner' +import { PwaUpdater } from './components/PwaUpdater' const root = document.getElementById('root') render(() => { return ( + diff --git a/src/store/index.ts b/src/store/index.ts index 90f8a04..c1d597d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -221,10 +221,12 @@ export const startContextPolling = () => { } }; -createEffect(() => { - // Re-run whenever ctx changes - currentTaskContext(); - startContextPolling(); +createRoot(() => { + createEffect(() => { + // Re-run whenever ctx changes + currentTaskContext(); + startContextPolling(); + }); }); export interface FilterTemplate { diff --git a/src/views/SettingsView.tsx b/src/views/SettingsView.tsx index 26c30b7..eced159 100644 --- a/src/views/SettingsView.tsx +++ b/src/views/SettingsView.tsx @@ -77,7 +77,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props -
+
{/* Resources Category (Mostly for Mobile) */} @@ -1086,6 +1086,23 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
+ + ); }; diff --git a/tsconfig.app.json b/tsconfig.app.json index 1846f10..fac347c 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -10,7 +10,9 @@ "DOM.Iterable" ], "types": [ - "vite/client" + "vite/client", + "vite-plugin-pwa/client", + "vite-plugin-pwa/solid" ], "skipLibCheck": true, /* Bundler mode */ diff --git a/vite.config.ts b/vite.config.ts index 25afbde..8fb6361 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ solid(), tailwindcss(), VitePWA({ - registerType: 'autoUpdate', + registerType: 'prompt', includeAssets: ['icon.webp', 'vite.svg'], manifest: { name: 'Tasgrid',