diff --git a/public/obs-overlay.html b/public/obs-overlay.html index bee9547..ffaf68d 100644 --- a/public/obs-overlay.html +++ b/public/obs-overlay.html @@ -37,6 +37,13 @@ } }); + setInterval(() => { + if (active && typeof active.remaining === 'number') { + active.remaining = Math.max(0, active.remaining - 1000); + render(); + } + }, 1000); + function render() { const el = document.getElementById('timer'); if (!active || active.remaining == null) { diff --git a/public/service-worker.js b/public/service-worker.js index 8bed9cd..e1d4dd5 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -10,6 +10,11 @@ self.addEventListener('install', (event) => { self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; event.respondWith( - fetch(event.request).catch(() => caches.match(event.request).then((response) => response || caches.match(OFFLINE_URL))) + fetch(event.request).catch(() => { + if (event.request.mode === 'navigate') { + return caches.match(OFFLINE_URL); + } + return caches.match(event.request); + }) ); }); diff --git a/src/components/TimerImportExport.tsx b/src/components/TimerImportExport.tsx index a2cd9d1..47546e7 100644 --- a/src/components/TimerImportExport.tsx +++ b/src/components/TimerImportExport.tsx @@ -32,6 +32,9 @@ const TimerImportExport: React.FC = () => { const format = ext === 'csv' ? 'csv' : 'json'; const timers = importTimers(text, format); dispatch({ type: 'setAll', timers }); + import('../services/timerSync').then(({ saveTimer }) => { + timers.forEach((tmr) => saveTimer(tmr)); + }); } catch (err) { console.error(err); alert(t('timerList.importError')); diff --git a/src/components/TimerList.tsx b/src/components/TimerList.tsx index b43d395..a42b361 100644 --- a/src/components/TimerList.tsx +++ b/src/components/TimerList.tsx @@ -13,10 +13,10 @@ const TimerList: React.FC = () => { return (
- {state.timers.map((t) => ( -
- -
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index de76ca2..fd117c2 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -25,12 +25,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const unsubscribe = auth.onAuthStateChanged(async (u) => { setUser(u); if (u) { - const r = await getUserRole(u); - setRole(r); + try { + const r = await getUserRole(u); + setRole(r); + } catch { + setRole(null); + } finally { + setLoading(false); + } } else { setRole(null); + setLoading(false); } - setLoading(false); }); return unsubscribe; }, []); diff --git a/src/context/MessagesContext.tsx b/src/context/MessagesContext.tsx index 76d2b05..986b953 100644 --- a/src/context/MessagesContext.tsx +++ b/src/context/MessagesContext.tsx @@ -46,10 +46,13 @@ export const MessagesProvider: React.FC<{ children: React.ReactNode }> = ({ chil useEffect(() => { let unsub: (() => void) | undefined; + let isMounted = true; import('../services/messageSync').then(({ listenMessage }) => { + if (!isMounted) return; unsub = listenMessage((msg) => dispatch({ type: 'set', message: msg })); }); return () => { + isMounted = false; if (unsub) unsub(); }; }, []); diff --git a/src/context/TimersContext.tsx b/src/context/TimersContext.tsx index 2402b32..06cf834 100644 --- a/src/context/TimersContext.tsx +++ b/src/context/TimersContext.tsx @@ -86,12 +86,15 @@ export const TimersProvider: React.FC<{ children: React.ReactNode }> = ({ childr // Listen to remote updates React.useEffect(() => { let unsubscribe: (() => void) | undefined; + let isMounted = true; import('../services/timerSync').then(({ listenTimers }) => { + if (!isMounted) return; unsubscribe = listenTimers((timers) => localDispatch({ type: 'setAll', timers }) ); }); return () => { + isMounted = false; if (unsubscribe) unsubscribe(); }; }, []); diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts index 1266861..e1e6e0a 100644 --- a/src/hooks/useTimer.ts +++ b/src/hooks/useTimer.ts @@ -13,7 +13,8 @@ export const useTimer = ( const storageKey = `timer-progress-${id}`; const [elapsed, setElapsed] = useState(0); const [running, setRunning] = useState(false); - const intervalRef = useRef(null); + const intervalRef = useRef | null>(null); + const autoStarted = useRef(false); // Load persisted progress useEffect(() => { @@ -49,21 +50,30 @@ export const useTimer = ( } }, [elapsed, running, storageKey]); + // reset auto-start flag if schedule changes + useEffect(() => { + autoStarted.current = false; + }, [startAt]); + // start the timer at a scheduled time if provided useEffect(() => { - if (startAt === undefined || running) return; + if (startAt === undefined || running || autoStarted.current) return; const now = warpedNow(); const startTime = startAt + getSyncDelay(); if (startTime <= now) { setRunning(true); + autoStarted.current = true; } else { - const timeout = setTimeout(() => setRunning(true), startTime - now); + const timeout = setTimeout(() => { + setRunning(true); + autoStarted.current = true; + }, startTime - now); return () => clearTimeout(timeout); } }, [startAt, running]); useEffect(() => { - if (!running || kind === 'clock') return; + if (!running && kind !== 'clock') return; intervalRef.current = setInterval(() => { setElapsed((e) => e + 1000); }, 1000); diff --git a/src/services/timerSync.ts b/src/services/timerSync.ts index 3b4921d..a9b9119 100644 --- a/src/services/timerSync.ts +++ b/src/services/timerSync.ts @@ -14,7 +14,10 @@ export function listenTimers(callback: (timers: TimerConfig[]) => void) { // Add or update a timer document export function saveTimer(timer: TimerConfig) { const docRef = doc(db, 'timers', timer.id); - return setDoc(docRef, timer); + const data = Object.fromEntries( + Object.entries(timer).filter(([, v]) => v !== undefined) + ) as TimerConfig; + return setDoc(docRef, data); } // Remove a timer by id