From 7a18011b0f3888ec539f99042922db6fe58d2491 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Mon, 29 Jun 2026 00:04:48 +0100 Subject: [PATCH] feat: add Phokus UI Lab Add a dev-only Vite UI mode with Tauri API mocks, in-memory fixture scenarios, reusable media source handling, and documentation for browser-based visual testing. --- README.md | 6 + docs/ui-lab.md | 180 +++++++++++++ package.json | 1 + src/App.tsx | 10 +- src/components/DuplicateFinder.tsx | 4 +- src/components/Gallery.tsx | 4 +- src/components/Lightbox.tsx | 7 +- src/components/Sidebar.tsx | 4 +- src/components/TagCloud.tsx | 4 +- src/dev/applyMockScenario.ts | 17 ++ src/dev/mockBackend.ts | 403 +++++++++++++++++++++++++++++ src/dev/mockFixtures.ts | 320 +++++++++++++++++++++++ src/dev/mockScenarios.ts | 17 ++ src/dev/setupMockTauri.ts | 11 + src/lib/mediaSrc.ts | 17 ++ src/main.tsx | 21 +- 16 files changed, 1006 insertions(+), 20 deletions(-) create mode 100644 docs/ui-lab.md create mode 100644 src/dev/applyMockScenario.ts create mode 100644 src/dev/mockBackend.ts create mode 100644 src/dev/mockFixtures.ts create mode 100644 src/dev/mockScenarios.ts create mode 100644 src/dev/setupMockTauri.ts create mode 100644 src/lib/mediaSrc.ts diff --git a/README.md b/README.md index b559491..3e0fb69 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,9 @@ pnpm dev:app # Frontend only pnpm dev:vite +# Browser-only UI Lab with mocked Tauri APIs +pnpm dev:ui + # Production build (CPU) pnpm build:app:cpu @@ -119,6 +122,9 @@ pnpm build:app:cuda pnpm build:vite ``` +For visual frontend work without launching Tauri or the Rust backend, see +[Phokus UI Lab](docs/ui-lab.md). + ## How it works 1. Add a folder from the sidebar — the Rust indexer walks it recursively. diff --git a/docs/ui-lab.md b/docs/ui-lab.md new file mode 100644 index 0000000..8735c62 --- /dev/null +++ b/docs/ui-lab.md @@ -0,0 +1,180 @@ +# Phokus UI Lab + +Phokus UI Lab is a browser-only development mode for visual work on the real +Phokus frontend. It runs the same `App.tsx`, Zustand store, components, and CSS +as the Tauri app, but installs Tauri JavaScript mocks before the app imports. + +This gives UI agents and contributors a stable browser target without launching +the Rust backend or a Tauri window. + +## Run It + +```bash +pnpm dev:ui +``` + +Then open: + +```text +http://127.0.0.1:1422 +``` + +The script runs Vite in the custom `ui` mode: + +```bash +vite --mode ui --host 127.0.0.1 --port 1422 --strictPort --open +``` + +The normal app development commands are unchanged: + +```bash +pnpm dev:app # Tauri app + Rust backend +pnpm dev:vite # frontend dev server for Tauri dev +``` + +## Scenarios + +UI Lab reads `?scenario=` from the URL. If no scenario is provided, it uses +`rich`. + +| URL | Purpose | +| --- | --- | +| `/?scenario=rich` | Default realistic library with folders, albums, ratings, favorites, tags, images, and videos | +| `/?scenario=empty` | First-run state with no folders or media | +| `/?scenario=busy` | Background workers with pending thumbnail, metadata, embedding, caption, and tagging jobs | +| `/?scenario=duplicates` | Duplicate Finder opened with duplicate groups already available | +| `/?scenario=album` | Gallery opened directly into an album | +| `/?scenario=errors` | Broken thumbnails, folder scan errors, failed embeddings, failed tagging, and metadata issues | +| `/?scenario=huge` | Large mock library for virtualization and layout checks | + +Examples: + +```text +http://127.0.0.1:1422/?scenario=duplicates +http://127.0.0.1:1422/?scenario=errors +http://127.0.0.1:1422/?scenario=huge +``` + +## How It Works + +`src/main.tsx` bootstraps the app asynchronously. In `ui` mode it imports +`src/dev/setupMockTauri.ts` first, then imports the real `App`. + +That order matters because static imports are hoisted. The Tauri mocks must be +installed before `App`, the store, the title bar, or visual components import +Tauri APIs. + +The mock setup installs: + +- `mockWindows("main")` for window APIs used by the title bar +- `mockConvertFileSrc("windows")` as a baseline Tauri file URL mock +- `mockIPC(..., { shouldMockEvents: true })` for `invoke`, `listen`, and `emit` + +The in-memory backend lives in: + +```text +src/dev/mockBackend.ts +src/dev/mockFixtures.ts +src/dev/mockScenarios.ts +src/dev/applyMockScenario.ts +``` + +Happy-path fixture media is imported from the existing onboarding assets in: + +```text +src/assets/onboarding/ +``` + +Synthetic or deliberately broken fixture paths can still use the `mock://` +scheme described below. + +## Mock Media + +Visual components should use `mediaSrc(...)` instead of calling +`convertFileSrc(...)` directly: + +```ts +import { mediaSrc } from "../lib/mediaSrc"; + +const src = mediaSrc(image.thumbnail_path); +``` + +In normal Tauri modes, `mediaSrc` delegates to `convertFileSrc`. In UI Lab, +already-resolved Vite asset URLs are returned as-is, so fixtures can reuse +existing imported assets. Paths beginning with `mock://` are served from +`public/dev-media`: + +```text +mock://thumb-1.svg -> /dev-media/thumb-1.svg +``` + +Use `mediaSrc` when adding components that render thumbnails, album covers, +preview images, or video posters. + +## Adding A Mock Command + +If the browser console logs an unmocked command, add it to +`src/dev/mockBackend.ts`. + +Prefer returning realistic data for commands that affect visible UI. For actions +that would touch the machine, return a safe no-op: + +```ts +case "open_app_data_folder": + return null; +``` + +When adding fixture data, keep it shaped like the real store/Rust contracts. +Most frontend-facing types are exported from `src/store.ts`, so the mocks can +stay type-checked against the real UI. + +## Adding A Scenario + +1. Add the scenario name to `MockScenario` and `SCENARIOS` in + `src/dev/mockScenarios.ts`. +2. Adjust fixture creation in `src/dev/mockFixtures.ts`. +3. If the scenario should open a specific view, add that behavior in + `src/dev/applyMockScenario.ts`. +4. Visit `http://127.0.0.1:1422/?scenario=your-scenario` and check for console + errors. + +## What UI Lab Is For + +UI Lab is intended for: + +- visual iteration on the real app shell +- layout and responsive checks +- state-heavy views like Explore, Timeline, Duplicates, albums, and Settings +- AI-agent screenshot/browser inspection +- safe UI work without filesystem or Rust-side effects + +It is not a replacement for Tauri app testing. Validate native behavior with +`pnpm dev:app` when touching: + +- file or folder pickers +- filesystem permissions +- real thumbnail/video loading +- window drag, minimize, maximize, or close behavior +- Rust backend performance +- updater installation +- WebView2-specific behavior + +## Verification + +Useful checks after changing UI Lab: + +```bash +pnpm exec tsc --noEmit +pnpm build:vite +pnpm dev:ui +``` + +Then smoke-test at least: + +```text +http://127.0.0.1:1422/?scenario=rich +http://127.0.0.1:1422/?scenario=empty +http://127.0.0.1:1422/?scenario=duplicates +http://127.0.0.1:1422/?scenario=errors +http://127.0.0.1:1422/?scenario=huge +``` diff --git a/package.json b/package.json index fc358f8..d3d03e3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "clean:app": "cd src-tauri && cargo clean", "dev:app": "tauri dev", "dev:app:cpu": "tauri dev -- --no-default-features", + "dev:ui": "vite --mode ui --host 127.0.0.1 --port 1422 --strictPort --open", "dev:vite": "vite", "dev:web": "cd website && pnpm dev", "format:app": "cd src-tauri && cargo fmt", diff --git a/src/App.tsx b/src/App.tsx index 3dfe52f..75de078 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,12 +48,16 @@ export default function App() { if (import.meta.env.PROD) { void checkForUpdates({ quiet: true }); } - loadFolders().then(() => { + loadFolders().then(async () => { void loadBackgroundJobProgress(); void loadCaptionModelStatus(); void loadDuplicateScanCache(); - void loadAlbums(); - return loadImages(true); + await loadAlbums(); + await loadImages(true); + if (import.meta.env.MODE === "ui") { + const { applyMockScenario } = await import("./dev/applyMockScenario"); + applyMockScenario(); + } }); let unlisten: (() => void) | undefined; subscribeToProgress().then((fn) => { diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index f7fe36e..c0e006d 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -1,8 +1,8 @@ import { useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { convertFileSrc } from "@tauri-apps/api/core"; import { DuplicateGroup, useGalleryStore } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; +import { mediaSrc } from "../lib/mediaSrc"; function formatBytes(bytes: number): string { if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; @@ -68,7 +68,7 @@ function DuplicateGroupCard({ group }: { group: DuplicateGroup }) {
{group.images.map((image) => { const isSelected = selectedIds.has(image.id); - const src = image.thumbnail_path ? convertFileSrc(image.thumbnail_path) : null; + const src = mediaSrc(image.thumbnail_path); return (