import { useEffect, useRef, useState } from 'react' import { FolderJobProgress, useGalleryStore } from '../store' // Dev-only screenshot/demo helper. Injects frozen UI states that are otherwise // too transient (or unreachable) to capture: the background-worker pipeline, // which drains in under a second, and the auto-updater flow, which never fires // locally because there's no remote latest.json to check against. // Gated by import.meta.env.DEV in App.tsx so the whole thing is tree-shaken out // of production builds. // // Toggle the controls with Ctrl+Shift+D. Injected state persists in the store // while the panel is hidden, so: open → inject → hide → screenshot. function emptyProgress(folderId: number): FolderJobProgress { return { folder_id: folderId, thumbnail_pending: 0, metadata_pending: 0, embedding_pending: 0, embedding_ready: 0, embedding_failed: 0, caption_pending: 0, caption_ready: 0, caption_failed: 0, tagging_pending: 0, tagging_ready: 0, tagging_failed: 0, } } // A believable multi-folder "busy pipeline" — three folders at different stages. const BUSY_PRESETS: Partial[] = [ // Embeddings actively running, tagging queued behind it. { embedding_ready: 34, embedding_pending: 16, tagging_pending: 50 }, // Just started: thumbnails draining, everything else queued. { thumbnail_pending: 11, embedding_pending: 50, tagging_pending: 50 }, // Late stage: embeddings done, tagging running. { embedding_ready: 40, tagging_ready: 18, tagging_pending: 22 }, ] const DEMO_UPDATE_VERSION = '0.2.0' export function DemoPanel() { const folders = useGalleryStore((state) => state.folders) const appVersion = useGalleryStore((state) => state.appVersion) const [open, setOpen] = useState(false) const downloadTimer = useRef(null) useEffect(() => { const onKey = (event: KeyboardEvent) => { if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'd') { event.preventDefault() setOpen((value) => !value) } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, []) const stopTimer = () => { if (downloadTimer.current !== null) { window.clearInterval(downloadTimer.current) downloadTimer.current = null } } // Stop any running download animation when the panel unmounts. useEffect(() => stopTimer, []) const injectBusy = () => { const progress: Record = {} folders.slice(0, BUSY_PRESETS.length).forEach((folder, index) => { progress[folder.id] = { ...emptyProgress(folder.id), ...BUSY_PRESETS[index] } }) useGalleryStore.setState({ mediaJobProgress: progress }) } const injectSingleEmbedding = () => { const folder = folders[0] if (!folder) return useGalleryStore.setState({ mediaJobProgress: { [folder.id]: { ...emptyProgress(folder.id), embedding_ready: 27, embedding_pending: 23 }, }, }) } const clear = () => useGalleryStore.setState({ mediaJobProgress: {} }) // --- Updater flow --------------------------------------------------------- // These drive the real UpdateToast + Settings "About" row. The Install button // stays inert (there's no real pendingUpdate), so this validates the // presentation only — see DemoPanel's header note for the real e2e path. const updateAvailable = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'available', updateVersion: DEMO_UPDATE_VERSION, updateProgress: null, updateError: null, updateDismissed: false, }) } // Indeterminate pulse, then climb 0 → 100%, then flip to "installing". const simulateDownload = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'downloading', updateVersion: DEMO_UPDATE_VERSION, updateProgress: null, updateError: null, updateDismissed: false, }) let progress = 0 window.setTimeout(() => { downloadTimer.current = window.setInterval(() => { progress = Math.min(progress + 0.07, 1) useGalleryStore.setState({ updateProgress: progress }) if (progress >= 1) { stopTimer() useGalleryStore.setState({ updateStatus: 'installing' }) } }, 200) }, 700) } const updateInstalling = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'installing', updateVersion: DEMO_UPDATE_VERSION, updateProgress: 1, updateDismissed: false, }) } const updateError = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'error', updateError: 'Could not reach the update server (connection timed out).', updateDismissed: false, }) } const updateUpToDate = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'upToDate', updateVersion: null, updateError: null }) } const resetUpdate = () => { stopTimer() useGalleryStore.setState({ updateStatus: 'idle', updateVersion: null, updateProgress: null, updateError: null, updateDismissed: false, }) } // --- What's New flow ------------------------------------------------------ // Drives the post-update greeting without needing a real version change. The // toast + modal pull their content from the bundled changelog for the running // app version, so the current release's notes are what render. const showWhatsNewToast = () => useGalleryStore.setState({ whatsNewToast: appVersion ?? DEMO_UPDATE_VERSION, whatsNewOpen: false, }) const openWhatsNewModal = () => useGalleryStore.setState({ whatsNewOpen: true, whatsNewToast: null }) const resetWhatsNew = () => useGalleryStore.setState({ whatsNewOpen: false, whatsNewToast: null }) if (!open) return null const injectBtn = 'rounded-md border border-amber-400/30 bg-amber-500/15 px-2 py-1.5 text-left hover:bg-amber-500/25' const neutralBtn = 'rounded-md border border-white/10 bg-white/[0.06] px-2 py-1.5 text-left text-gray-300 hover:bg-white/10' return (
Demo · Ctrl+Shift+D

Pipeline

Inject a frozen worker-bar state, hide this panel, then screenshot.

Update flow

Drives the real toast (bottom-right) + Settings → About row. Install is inert here.

What's new

Post-update greeting for v{appVersion ?? '—'}, sourced from the bundled changelog.

) }