import { useEffect, useMemo, useRef, useState } from 'react'; import { useUIStore } from '@/stores/uiStore'; interface PhotoFrameProps { intervalMs?: number; transitionMs?: number; } function shuffle(arr: T[]): T[] { const out = arr.slice(); for (let i = out.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [out[i], out[j]] = [out[j], out[i]]; } return out; } function useClock() { const [now, setNow] = useState(() => new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 10_000); return () => clearInterval(id); }, []); return now; } export function PhotoFrame({ intervalMs = 15_000, transitionMs = 1_200 }: PhotoFrameProps) { const setIdle = useUIStore((s) => s.setIdle); const [files, setFiles] = useState([]); const [index, setIndex] = useState(0); const [currentUrl, setCurrentUrl] = useState(null); const [prevUrl, setPrevUrl] = useState(null); const [empty, setEmpty] = useState(false); const [emptyDir, setEmptyDir] = useState(''); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { const load = async () => { const api = window.electronAPI; if (!api?.photos) { setEmpty(true); return; } const list = await api.photos.list(); const dir = await api.photos.getDir(); if (!mountedRef.current) return; if (!list.length) { setEmpty(true); setEmptyDir(dir); return; } setFiles(shuffle(list)); setEmpty(false); }; load(); // Re-scan every 10 min in case photos are added while idle const id = setInterval(load, 10 * 60_000); return () => clearInterval(id); }, []); useEffect(() => { if (!files.length) return; const id = setInterval(() => { setIndex((i) => (i + 1) % files.length); }, intervalMs); return () => clearInterval(id); }, [files.length, intervalMs]); // Resolve file:// URLs via IPC when the index changes useEffect(() => { if (!files.length) return; const api = window.electronAPI; if (!api?.photos?.getUrl) return; let cancelled = false; (async () => { const url = await api.photos.getUrl(files[index]); if (cancelled || !mountedRef.current) return; setPrevUrl(currentUrl); setCurrentUrl(url); })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [index, files]); const handleExit = () => setIdle(false); const now = useClock(); const timeStr = useMemo( () => now.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }), [now], ); const dateStr = useMemo( () => now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', }), [now], ); const currentSrc = currentUrl; const prevSrc = prevUrl; return (
{empty ? (
{timeStr}
{dateStr}
No photos found{emptyDir ? ` in ${emptyDir}` : ''}.
Touch anywhere to exit
) : ( <> {prevSrc && ( )} {currentSrc && ( )} {/* Lower-right clock + date overlay */}
{timeStr}
{dateStr}
)}
); }