diff --git a/src/components/DemoPanel.tsx b/src/components/DemoPanel.tsx index e87ba12..b62c9a8 100644 --- a/src/components/DemoPanel.tsx +++ b/src/components/DemoPanel.tsx @@ -1,10 +1,12 @@ -import { useEffect, useState } from "react"; +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 to capture (e.g. the background-worker pipeline, which drains -// in under a second on a fast machine). Gated by import.meta.env.DEV in App.tsx -// so the whole thing is tree-shaken out of production builds. +// 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. @@ -36,9 +38,12 @@ const BUSY_PRESETS: Partial[] = [ { 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 [open, setOpen] = useState(false); + const downloadTimer = useRef(null); useEffect(() => { const onKey = (event: KeyboardEvent) => { @@ -51,6 +56,16 @@ export function DemoPanel() { 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) => { @@ -71,36 +86,135 @@ export function DemoPanel() { 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, + }); + }; + 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. +

+
+ + + + + + +
); } diff --git a/src/components/PhokusMark.tsx b/src/components/PhokusMark.tsx index 52e6039..627267a 100644 --- a/src/components/PhokusMark.tsx +++ b/src/components/PhokusMark.tsx @@ -3,9 +3,18 @@ // // The full app-icon tile (filled iris on a dark rounded square) lives in // branding/phokus-aperture.svg and feeds `pnpm tauri icon`. +// +// Pass dotClassName (e.g. "fill-amber-400") to light up the central focal point — +// used in the titlebar as the "update available" indicator. const BLADE = "M0,-4.18 A10,10 0 0 1 6.43,-7.66"; -export function PhokusMark({ className }: { className?: string }) { +export function PhokusMark({ + className, + dotClassName, +}: { + className?: string; + dotClassName?: string; +}) { return ( + {dotClassName ? : null} ); } diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 22f0cc5..a08499a 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -40,6 +40,9 @@ function CloseIcon() { export function TitleBar() { const [isMaximized, setIsMaximized] = useState(false); const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen); + const updateStatus = useGalleryStore((state) => state.updateStatus); + const updateVersion = useGalleryStore((state) => state.updateVersion); + const installUpdate = useGalleryStore((state) => state.installUpdate); const appWindow = getCurrentWindow(); useEffect(() => { @@ -60,6 +63,10 @@ export function TitleBar() { const handleMaximize = () => appWindow.toggleMaximize(); const handleClose = () => appWindow.close(); + // An update is waiting for the user to act. Covers the "clicked Later" case too, + // since dismissing the toast doesn't change updateStatus. + const updatePending = updateStatus === "available"; + return ( // data-tauri-drag-region is the recommended Tauri approach for drag regions. // WebkitAppRegion is kept as a CSS fallback for compatibility. @@ -68,11 +75,32 @@ export function TitleBar() { className="titlebar relative z-50 flex h-9 shrink-0 items-center bg-gray-950 select-none" style={{ WebkitAppRegion: "drag" } as React.CSSProperties} > - {/* App icon + name — left side */} + {/* App icon + name — left side. When an update is waiting, the iris lights + up its focal point and the chip becomes a button that re-opens the prompt. */}
-
- -
+ {updatePending ? ( +
+ + {/* Custom tooltip — fades in ~100ms instead of the native ~500ms delay. */} + + Click to update — v{updateVersion} + +
+ ) : ( +
+ +
+ )} Phokus