The quick theme switcher's option list was a hand-maintained array that
could drift from the AppTheme union. Derive it from a
Record<AppTheme, string> instead, so adding a theme forces this
mapping to be updated too.
Unit tests for three previously-untested pure functions: mediaSrc's
UI Lab path handling, getChangelogForVersion's version normalization,
and groupImages' date bucketing/sorting.
Cache invalidation across affected folders after a bulk delete ran
sequentially through awaited invoke calls, duplicated between
gallerySlice and duplicateSlice. Extract a shared
invalidateDuplicateScanCaches helper that batches the calls with
Promise.allSettled, logging failures instead of throwing — a
best-effort cache refresh should never turn an already-successful
delete into a rejected promise for the caller.
Filename/tag search treated literal % and _ in query text as SQL
wildcards; escape them (with ESCAPE '\') before building LIKE patterns.
Also pass -- before the URL in curl invocations so a URL starting with
a dash can't be misread as an option.
Only require a worker to be paused if it currently has pending work for
that folder, instead of demanding all four worker flags be true. The
background tasks panel only lets you toggle stages it shows, so the old
check stayed out of sync after pausing everything visible.
Also grants start-dragging and start-resize-dragging window permissions.
Merge the Tauri test feature branch into main.
Adds Vitest coverage for frontend formatting, path, and store helper behavior, plus Rust unit coverage for database, vector, storage, indexer, thumbnail, and AI tag filtering logic. Also wires the new unit test scripts into the package commands and documents the test layers.
CLAUDE.md claimed the Playwright smoke tests were the only tests; add
the Vitest and cargo-test commands and describe the three test layers,
including the shared db::test_support fixture for DB-touching Rust
tests.
storage.rs: thumbnail worker clamping across parallelism levels, the
adaptive-profile EMA transitions, and UNC-path fallback detection.
indexer.rs: supported-media extension matching plus media-kind and MIME
mapping. thumbnail.rs: fit_dimensions aspect-ratio math with extreme
ratios and is_jpeg extension checks. ai_tag_filter.rs: padding and
mixed-separator normalization edge cases.
A shared test_support module in db.rs provides the fixture: sqlite-vec
registered via auto-extension, an in-memory connection with foreign keys
on, and both migrations applied - no refactoring of production code
needed since every query function already takes &Connection.
db.rs coverage: folder idempotency, upsert_image update semantics
(favorite/rating preserved, AI tag state invalidated), the get_images
filter matrix with pagination and count_images agreement, tag
merge/rename/delete, user-tag precedence over AI tags in update_ai_tags,
album CRUD with FK cascade, the embedding job queue (backfill, retry,
consistency repair), tag search, and delete_folder cascades.
vector.rs coverage: pack/unpack round-trip, embedding upsert/delete with
dimension validation, and find_similar_image_ids ranking on both the
global KNN and folder-scoped brute-force paths.
Wire Vitest into the existing Vite config (scoped to src/**/*.test.ts so it
never collides with the Playwright suite in tests/) and add test:unit,
test:unit:watch, and test:rust scripts.
Covers the pure logic layer: parseSearchValue prefix parsing, all twelve
sort orders through mergeImages, merge/dedup/window helpers, filter
matching, gallery request tokens, localStorage-backed initial settings,
folder-picker path utilities, and the duplicate/lightbox/gallery/video
formatters. Fixture factories live in src/test/factories.ts.
CLAUDE.md still claimed no test suites existed. Update it for the
current repo state:
- add pnpm test:e2e commands (full run, single file, single test) and
note the suite runs against UI Lab mocks, not the Tauri app
- new UI Lab section: src/dev mock layer, ?scenario=/?changelog= params,
mediaSrc rule, and the mockBackend requirement for new commands
- add dev:ui and format:all to the command list
- note website/ as a separate Vite project and document the
changelog:add script syntax
captioner.rs had grown into two things: the (currently disabled)
Florence-2 captioner plus generic infrastructure the live tagger
depends on. Split the neutral parts out:
- download.rs: resilient curl downloader (resume, stall detection)
and NuGet package extraction
- onnx_runtime.rs: shared ONNX Runtime/DirectML DLL manifest,
provisioning, and ort init, with runtime_dir() as the single
definition of the DLL location (kept inside the caption model dir
so existing installs do not re-download)
Also dedupes the tagger-side copies of the DLL list and four
hardcoded caption-model path literals, and rewords tagger runtime
errors that wrongly told users to download the caption model.
Spawn bare `curl` (which Windows still resolves to curl.exe via
CreateProcess, and macOS/Linux ship natively) and write the size-probe
output to the platform null device instead of the Windows-only NUL,
removing the hard Windows dependency from the model download path.
Spawn quirks (console-window suppression) are centralized in a new
curl_command() helper.
DemoPanel is dev-only tooling like the rest of the UI Lab mock code, so it
lives with it now instead of among the real UI components. Import path in
App.tsx updated; the DEV gating (and production tree-shaking) is unchanged.
Add separate UI Lab scenarios for true first-run onboarding and post-update What's New checks. Keep empty-library fixtures reusable across mock data, tagger readiness, worker pause defaults, and launch-time version state so browser verification can exercise the real flows.
Add a UI-Lab-only (mode === "ui", dead-code-eliminated from real builds)
URL override for the What's New modal: ?changelog=unreleased shows the
in-progress notes before they ship, ?changelog=small serves a synthetic
hotfix-sized entry to exercise the compact layout (including a no-lead
bullet), and ?changelog=<version> previews any released entry. Documented
in docs/ui-lab.md alongside the ?scenario= table.
Small releases keep the compact single-column list; releases with more than
20 bullets across 2+ sections switch to a wider two-pane layout with a
section nav rail, so no pane is a marathon scroll. The threshold lives in
RAIL_THRESHOLD; single-section entries always stay single-column.
Add the user-facing changes that had no entry: the Settings page reorg, the
AI-tag reset flow, the onboarding tagger-model choice + per-model confidence
thresholds, and the lightbox tagger-readiness fix. Fold two more papercuts
(search-field button alignment, selectable menu labels) into the papercuts
bullet.
Rework several Fixed entries to open with what the user used to suffer
rather than the fix, Discord-patch-notes style.
- Zustand store split from one god file into per-feature slices under
src/store/ (library, gallery, search, explore, albums, duplicates,
tagger, captions, settings, app) with shared helpers and event wiring.
- All large components modularized into per-feature subfolders:
Lightbox, SettingsModal, Sidebar, ExploreView, FolderPickerModal,
BackgroundTasks, Toolbar, Gallery, Timeline, DuplicateFinder,
VideoPlayer, and BulkActionBar.
- Context menus, dropdowns, and dismissal handling consolidated into a
shared menu system (src/components/menu/); dead MenuBar removed;
shared icons extracted into icons.tsx.
- New: add-to-album submenu on the image right-click menu.
- Fixes: menu labels no longer text-selectable, duplicate tile React
keys, mock IPC results cloned to match real invoke semantics, tile
tooltip and folder picker glitches.
- Tooling: prettier added with format/format:rust scripts, one-shot
format of the frontend, LF line endings enforced via .gitattributes.
Prettier enforces endOfLine: lf, but core.autocrlf=true was converting
working-copy files to CRLF on checkout, which would flip format:check
red after every branch switch. eol=lf keeps LF in the working tree;
binary assets and Windows scripts are exempted.
Prettier config (no semi, single quotes, 100 cols, tailwindcss plugin)
scoped via .prettierignore to frontend code only; markdown, docs, CI
workflows, website/, and src-tauri/ are excluded. Scripts follow the
toolchain split: format/format:check (prettier), format:rust(:check)
(cargo fmt), format:all for both.
Break down the monolithic BulkActionBar component into smaller, highly cohesive modules to improve maintainability and separation of concerns.
- Extract distinct UI regions and popovers into dedicated components (`BulkSelectionSummary`, `BulkAlbumPopover`, `BulkDeleteConfirm`, `BulkRatingPopover`, and `BulkTagPopover`).
- Abstract shared panel states and configurations into a dedicated `types.ts` file within the `bulk` directory.
- Simplify the main `BulkActionBar` component to act as a clean presentation orchestrator.
Break down the monolithic VideoPlayer component into smaller, highly cohesive modules to improve maintainability and separate concerns.
- Extract UI sub-components into dedicated files (`VideoControls` and `ControlButton`).
- Abstract complex playback state, event listeners, scrubbing, and keyboard shortcuts into a custom `useVideoPlayer` hook.
- Move time formatting utilities to a dedicated `format.ts` file.
- Relocate shared constants and types to dedicated files (`constants.ts` and `types.ts`).
- Simplify the main `VideoPlayer` component to act as a lightweight presentation orchestrator.
Break down the monolithic DuplicateFinder component into smaller, highly cohesive modules to improve maintainability and separation of concerns.
- Extract empty, loading, and intro states into dedicated components (`DuplicateFinderEmptyStates`).
- Extract the complex header and action logic into `DuplicateFinderHeader`.
- Move `DuplicateGroupCard` to its own dedicated component file.
- Relocate formatting utilities like byte conversion and relative time strings to a dedicated `format.ts` file.
Break down the monolithic Gallery and Timeline components into smaller, highly cohesive modules to improve maintainability and separation of concerns.
- Extract core UI elements into dedicated components (`ImageTile`, `TruncatedFilename`, `ScrubberYearBlock`).
- Separate loading and empty states into dedicated files (`GalleryEmptyState`, `TimelineEmptyState`).
- Move complex timeline grouping, row virtualization mapping, and duration formatting logic into utility files (`timelineModel.ts` and `format.ts`).
- Abstract shared data structures into `types.ts` to cleanly decouple domain logic from the presentation layer.
Break down the monolithic Toolbar component into smaller, highly cohesive modules to improve maintainability and separation of concerns.
- Extract distinct UI regions into dedicated components (`ToolbarTitle`, `ToolbarSearch`, `SortControl`, `ZoomControl`, `ToolbarFilters`, and `FilterPill`).
- Abstract complex search state, debouncing, event listeners, and autocomplete logic into a custom `useToolbarSearch` hook.
- Relocate search value formatting and sort configurations to dedicated utility files (`searchValue.ts` and `sortOptions.ts`).
- Simplify the main `Toolbar` component to act as a clean presentation orchestrator.
Break down the monolithic FolderPickerModal component into smaller, highly cohesive modules to improve maintainability and separate concerns.
- Extract UI sub-components into dedicated files (`FolderRow`, `StagedFoldersPanel`, and `StatusLine`).
- Move path manipulation, string formatting, and breadcrumb utilities into a dedicated `pathUtils.ts` file.
- Abstract complex local state, API calls, and event listeners into a custom `useFolderPicker` hook.
- Clean up `FolderPickerModal` to act as a lightweight presentation layer.
Break down the monolithic BackgroundTasks component into smaller, highly cohesive modules to improve maintainability and readability.
- Extract complex UI components into dedicated files (`BackgroundTaskSummary`, `ExpandedTaskPanel`, `TaskStagePill`, `TaskProgressBar`, `BackgroundTaskActions`, and `FailedWorkerItemRow`).
- Move task construction, duplicate scan formatting, and progress calculation logic into a dedicated `taskModel.ts` utility file.
- Relocate shared interfaces and constants to `types.ts` to cleanly decouple data structures from the presentation layer.
Break down the monolithic Sidebar component into smaller, maintainable modules to improve readability and separation of concerns.
- Extract `NavItem`, `FolderItem`, and `AlbumItem` into dedicated UI components.
- Modularize domain logic by introducing `LibrarySection` and `AlbumSection`.
- Abstract complex drag-and-drop and ordering state into custom `useFolderOrdering` and `useAlbumOrdering` hooks.
- Move shared constants and types to a dedicated `types.ts` file.
To de-godify \ExploreView.tsx\, the inline components \ClusterCloud\, \ExploreLoadingPanel\, \TagAtlas\, and \TagManageList\, along with shared constants in \layout.ts\, have been extracted into independent files. This improves modularity and maintainability.
Extracts the various settings sections (General, Media, Updates, Storage, AI Workspace) from the massive SettingsModal.tsx into distinct sub-components. Introduces a shared.tsx file for common settings UI primitives (SettingsGroup, SettingsItem, StatusPill, etc.) to improve code maintainability and separation of concerns.
Deconstruct the Lightbox.tsx god component into feature-specific hooks (slideshow, region selection, navigation, media details) and separate UI components (viewport, slideshow view, details panel). Extracted shared types and formatting logic into utility files.
Break the 3,244-line monolithic store into a Zustand slice-per-feature
layout (types, helpers, librarySlice, gallerySlice, searchSlice,
exploreSlice, albumSlice, duplicateSlice, taggerSlice, captionSlice,
settingsSlice, appSlice, events) combined in index.ts. Components keep
calling useGalleryStore(s => s.field) against the same flat state
object — no component changes required. Completes the de-godify effort
started with the menu/Dropdown/Sidebar extractions and icons.tsx.
The ten most-duplicated inline SVGs (Check, Close, ChevronDown/Right,
Plus, Photo, Folder, Warning, Star, Play) become shared components --
34+ copies across 15 files replaced. Stroke icons take a per-site
strokeWidth since weights legitimately differ by context; icons that
appear once or use variant path data stay inline.
The album popover body (album list + create-new-album form) moves to
its own component. The host supplies onPick, which both an existing
album row and a freshly created album route through -- BulkActionBar
adds the selection and closes the panel. Ready for reuse anywhere else
an album needs picking (e.g. the context menu submenu later).
FolderItem and AlbumItem each carried their own copy of the in-place
rename input (state, select-on-open effect, commit/cancel handling) and
the Confirm/Cancel pair for destructive actions. Both are now shared
components: InlineRename mounts fresh per rename (an external name
change mid-rename no longer clobbers typing), InlineConfirm is the
compact red/gray pair. The four copy-pasted nav rows collapse into a
local NavItem component.
Mock handlers return references straight into the in-memory db (e.g.
`return db.albums`), so store updates like set({ albums }) kept the same
array identity across loads and Zustand never notified subscribers --
the sidebar album list froze after creating an album from the bulk bar.
Real invoke() deserializes fresh JSON per call, so production was never
affected. structuredClone in the shim restores that fidelity for every
mock command at once.
MenuBar has had no importers since the Toolbar superseded it -- its
last real touch was the multi-folder picker work. Delete it and update
the CLAUDE.md component list (which also drifted: Timeline/ExploreView
existed but were missing, TagCloud never split out) plus a pointer to
the shared menu primitives.
ThemedDropdown, Toolbar''s local SortDropdown, and FolderScopeDropdown
were three hand-rolled implementations of the same select pattern. They
are replaced by a single generic Dropdown (src/components/menu/) built
on MenuPanel/MenuItem, with solid/ghost/compact trigger variants and
Object.is value comparison so number|null folder scopes work alongside
string unions. Call sites drop their `value as X` casts.
MenuItem gains an `active` state plus stable menu-panel/menu-item class
hooks, and the subtle-light CSS that previously dressed only the folder
scope dropdown (feature-scope-*) now themes every menu surface --
dropdowns, context menus, and submenus alike.
Replaces the hand-rolled outside-pointerdown listeners with the shared
hook. Both popovers now also close on Escape, and listeners only attach
while a panel is actually open.
Right-clicking an image in the Gallery or Timeline now offers an
"Add to Album" submenu listing all albums with their counts — the first
use of the new SubMenu primitive. Filing a single image away no longer
requires starting a multi-select.
Five hand-rolled context menu implementations (image tiles, sidebar
folders, sidebar albums, title bar theme switcher, plus duplicated
close-listener effects in Gallery/Timeline) are replaced by shared
primitives in src/components/menu/:
- useDismissable: one outside-pointerdown + Escape dismissal hook
- Menu.tsx: MenuPanel chrome, MenuItem (danger/disabled/checked/hint),
MenuSeparator, MenuLabel, and SubMenu with viewport edge-flipping
- ContextMenu: portal-rendered wrapper that measures and clamps to the
viewport, fixing menus rendering off-screen and the latent
fixed-inside-transform bug under framer-motion Reorder items
The image right-click menu moves to ImageContextMenu.tsx, shared by
Gallery and Timeline.
Debounce Explore tag refreshes while AI tagging is active so the tag cloud catches up after worker activity settles instead of continuously re-querying.
Optimize the tag cloud aggregate query by fetching representative thumbnails in one pass and adding a supporting tag/image index.
Split Settings into General, Media, Updates & Setup, Storage, and AI Workspace pages so update/setup and maintenance controls are easier to reach.
Move runtime check results beside the model runtime controls instead of under model location.