199 Commits

Author SHA1 Message Date
LyAhn a3d09fa943 docs(release): fill 0.2.0 installer checksums 2026-07-11 19:35:17 +01:00
LyAhn cad3b9c57d chore(release): 0.2.0
github/actions/release GitHub Actions release finished: success
github/actions/ci GitHub Actions CI finished: success
2026-07-11 19:13:58 +01:00
LyAhn e551b15aca ci: support rebuilding a release tag via workflow_dispatch
release.yml gains a required tag input so an existing tag can be rebuilt
without re-pushing it. The tag is checked out via refs/tags/, verified
against package.json and Cargo.toml versions before any toolchain setup,
and Gitea commit statuses are posted against the tag commit rather than
GITHUB_SHA (which is the dispatched branch head on workflow_dispatch).
2026-07-11 19:12:52 +01:00
LyAhn 0b4459365d ci: run unit and Rust tests, tighten triggers and permissions
Expand path triggers to all build inputs (configs, lockfile, index.html,
CHANGELOG.md which feeds the What's New modal), add pnpm test:unit and
cargo test steps, read-only permissions, a 60-minute timeout, and a
workflow-scoped concurrency group.
2026-07-11 19:12:41 +01:00
LyAhn 4aa74d535b fix(ui): fix alignment and sizing of the update pending icon
github/actions/ci GitHub Actions CI finished: success
2026-07-11 12:08:54 +01:00
LyAhn 48df4f2965 style: Format db.rs
github/actions/ci GitHub Actions CI finished: success
2026-07-06 20:53:14 +01:00
LyAhn 2bc4a98164 fix(frontend): derive THEME_OPTIONS from an exhaustive AppTheme record
github/actions/ci GitHub Actions CI finished: failure
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.
2026-07-06 20:42:06 +01:00
LyAhn 6923777345 test(frontend): cover mediaSrc, changelog, and timeline grouping
Unit tests for three previously-untested pure functions: mediaSrc's
UI Lab path handling, getChangelogForVersion's version normalization,
and groupImages' date bucketing/sorting.
2026-07-06 20:41:42 +01:00
LyAhn 96e62cb7c1 perf(frontend): invalidate duplicate-scan cache concurrently and safely
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.
2026-07-06 20:41:16 +01:00
LyAhn 42564a93e0 fix(backend): escape LIKE wildcards and harden curl arg passing
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.
2026-07-06 20:40:37 +01:00
LyAhn b8d009c973 fix(frontend): sync sidebar pause menu with per-worker background task state
github/actions/ci GitHub Actions CI finished: success
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.
2026-07-06 00:47:13 +01:00
LyAhn dcc1612802 merge: add automated test coverage
github/actions/ci GitHub Actions CI finished: success
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.
2026-07-05 21:26:32 +01:00
LyAhn 2c699a5aac docs(claude): document the new unit-test layers and commands
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.
2026-07-05 21:20:24 +01:00
LyAhn ca5c500e18 test(backend): cover storage, indexer, thumbnail, and tag-filter logic
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.
2026-07-05 21:19:50 +01:00
LyAhn 782cf0ea08 test(backend): add in-memory SQLite test harness with db and vector tests
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.
2026-07-05 21:19:42 +01:00
LyAhn 5004a2d01a test(frontend): add Vitest unit tests for store helpers and utilities
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.
2026-07-05 21:19:31 +01:00
LyAhn 9a282dda86 docs(claude): document UI Lab, Playwright e2e tests, and tooling scripts
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
2026-07-05 20:48:16 +01:00
LyAhn b23212ea1c refactor(backend): extract download and onnx_runtime modules from captioner
github/actions/ci GitHub Actions CI finished: success
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.
2026-07-05 20:21:47 +01:00
LyAhn a791f112f5 fix(downloads): invoke system curl portably instead of curl.exe
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.
2026-07-05 20:21:23 +01:00
LyAhn 7020a6b6cf style(whats-new): reformat WhatsNewModal via pnpm format
github/actions/ci GitHub Actions CI finished: success
2026-07-05 15:02:27 +01:00
LyAhn bf38fac30d test(e2e): wire Playwright to the UI Lab with scenario smoke tests 2026-07-05 15:02:26 +01:00
LyAhn 79e2e28979 refactor(dev): move DemoPanel into src/dev
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.
2026-07-05 13:38:52 +01:00
LyAhn aa3fe2062d feat(ui-lab): add first-run and update scenarios
github/actions/ci GitHub Actions CI finished: success
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.
2026-07-04 21:41:34 +01:00
LyAhn af1a443a64 feat(ui-lab): preview What's New entries via ?changelog=
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.
2026-07-04 21:15:17 +01:00
LyAhn 302a3151ef feat(whats-new): adapt modal layout to release size
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.
2026-07-04 21:14:45 +01:00
LyAhn d7c3cbd683 docs(changelog): fill unreleased gaps and give Fixed notes a pain-first pass
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.
2026-07-04 21:14:29 +01:00
LyAhn 2d567c0810 Merge refactor/modularize-components: modular components, store slices, shared menus, prettier
github/actions/ci GitHub Actions CI finished: success
- 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.
2026-07-04 20:31:47 +01:00
LyAhn 5ff3ff53a6 chore: enforce LF line endings 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.
2026-07-04 20:26:05 +01:00
LyAhn 827e1a8ecf style: format frontend with prettier
Mechanical one-shot pass of pnpm format over src/, tests/, tools/, and
root configs. No functional changes; build and type-check verified.
2026-07-04 20:23:32 +01:00
LyAhn 32c6ae09d6 chore: add prettier with format scripts
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.
2026-07-04 20:23:19 +01:00
LyAhn 44179c83a7 refactor(bulk-action-bar): modularize component
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.
2026-07-04 19:49:22 +01:00
LyAhn 4fcc42f356 refactor(video-player): modularize component
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.
2026-07-04 19:41:46 +01:00
LyAhn aa3d843a4b refactor(duplicate-finder): modularize component
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.
2026-07-04 19:36:07 +01:00
LyAhn 01faec9155 refactor(gallery): modularize gallery and timeline
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.
2026-07-04 19:30:54 +01:00
LyAhn 3242897a3b refactor(toolbar): modularize component
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.
2026-07-04 19:23:02 +01:00
LyAhn 8f424773d2 refactor(folder-picker): modularize component
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.
2026-07-04 19:14:32 +01:00
LyAhn 2149b4cad5 refactor(bg tasks): modularize component
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.
2026-07-04 19:04:06 +01:00
LyAhn 2901425f42 refactor(sidebar): modularize Sidebar
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.
2026-07-04 18:57:36 +01:00
LyAhn 1074c875a3 fix(menus): prevent menu label text from being selectable 2026-07-04 18:30:25 +01:00
LyAhn 58ecb03070 refactor(explore): extract components from ExploreView into src/components/explore/
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.
2026-07-04 18:09:41 +01:00
LyAhn 2c0b928bf5 refactor(ui): split SettingsModal into feature slices under src/components/settings/
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.
2026-07-04 16:46:41 +01:00
LyAhn fe312e7678 refactor(lightbox): split Lightbox into smaller hooks and components
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.
2026-07-04 16:33:54 +01:00
LyAhn 52ac2543ec refactor(store): split store.ts into feature slices under src/store/
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.
2026-07-04 15:53:52 +01:00
LyAhn fbf8a7878c refactor(ui): extract shared icons into 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.
2026-07-04 12:29:14 +01:00
LyAhn 4d41f3744f fix(duplicates): key duplicate tiles on the outermost mapped element
The key sat on the button nested inside the Tooltip wrapper, so React
warned about missing keys for every duplicate group tile.
2026-07-04 12:21:04 +01:00
LyAhn c27662dd74 refactor(ui): extract AlbumPicker from BulkActionBar
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).
2026-07-04 12:11:36 +01:00
LyAhn ee2a1b204e refactor(sidebar): extract InlineRename, InlineConfirm, and NavItem
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.
2026-07-04 12:11:26 +01:00
LyAhn 6806703363 fix(dev): clone mock IPC results to match real invoke semantics
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.
2026-07-04 12:11:00 +01:00
LyAhn 90fd6f4fed chore(ui): remove dead MenuBar component
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.
2026-07-04 07:22:32 +01:00
LyAhn 053a2bd846 refactor(ui): consolidate dropdowns into one shared Dropdown
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.
2026-07-04 07:22:22 +01:00
LyAhn 54016df830 refactor(ui): adopt useDismissable in ColorFilter and BulkActionBar
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.
2026-07-04 07:22:09 +01:00
LyAhn 5d46ee5b94 feat(gallery): add-to-album submenu on the image right-click menu
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.
2026-07-03 23:58:18 +01:00
LyAhn 83081928f6 refactor(ui): unify context menus into a shared menu system
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.
2026-07-03 23:54:28 +01:00
LyAhn e374ff6b02 fix(ui): fix tile tooltip + folder picker
- Tooltips now shows their correct px size on hovering

- Remove trailing `\` from drive letter in folder picker UI
2026-07-03 23:27:14 +01:00
LyAhn 3ab9357d6f perf(explore): reduce tag cloud refresh pressure
github/actions/ci GitHub Actions CI finished: success
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.
2026-07-03 22:43:41 +01:00
LyAhn fe65bc6f38 feat(settings): reorganize preferences pages
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.
2026-07-03 22:43:41 +01:00
LyAhn 68932b55c5 fix(tagger): separate JoyTag confidence threshold
Store confidence thresholds per tagger model so JoyTag no longer inherits WD tuning. Refresh the active threshold when switching models, guard stale threshold saves, and keep UI Lab mocks in sync.

Also tightens the onboarding model selector so the segmented control no longer stretches across the row.
2026-07-03 22:43:41 +01:00
LyAhn f1116c6c26 feat(onboarding): choose AI tagger model
Add WD and JoyTag selection to the Welcome Tour AI step so users can choose the model before downloading it.

Share tagger model metadata with Settings and keep the Settings close button anchored to the modal chrome.
2026-07-03 22:43:41 +01:00
LyAhn b92b850d02 Merge AI tagger readiness fixes
Refresh selected tagger readiness at startup so the lightbox AI tags action reflects installed model state without needing a Settings refresh.

Add UI Lab coverage for uninstalled WD and JoyTag scenarios, and include the toolbar clear-button alignment follow-up.
2026-07-02 20:59:48 +01:00
LyAhn bf04df7484 fix(toolbar): center search field overlay buttons
The clear button and command-prefix chip sat a few pixels high: their
inline-flex Tooltip wrappers created a line box with descender space
below, making the positioned div taller than the button it centers.
Making the wrappers flex containers collapses them to the button height
so the translate centering lands correctly.
2026-07-02 20:47:22 +01:00
LyAhn d29a779c13 test(ui-lab): add tagger readiness scenarios
Add UI Lab scenarios for uninstalled WD and JoyTag tagger states so the lightbox and AI Workspace readiness flows can be exercised directly.

Make the Settings title-bar button accessible by label to support reliable UI Lab automation.
2026-07-02 20:22:06 +01:00
LyAhn b7e82dbf91 fix(ai-tags): refresh tagger readiness for lightbox
Load the selected tagger model and model status during app startup so the lightbox AI tags action does not stay disabled until Settings refreshes the state.

Refresh readiness after tagger downloads complete and replace stale WD-specific unavailable copy with generic AI tagger wording.
2026-07-02 20:13:45 +01:00
LyAhn 749b23723a Merge lightbox slideshow mode
github/actions/ci GitHub Actions CI finished: success
Add fullscreen slideshow playback for image-only lightbox sessions, including sequential or random order, idle controls, and slideshow settings.

Include the gentle motion transition polish and no-repeat random navigation refinements from the feature branch.
2026-07-02 17:52:30 +01:00
LyAhn 68d19d219e feat(lightbox): add gentle slideshow motion
Add a selectable slideshow transition setting with a gentle motion option, keep the default soft fade, and make random slideshow navigation avoid repeats until the current image pool is exhausted.

Split the slideshow crossfade layer from the slow image drift so transitions overlap cleanly without black frames or lingering stale slide layers.
2026-07-02 11:39:19 +01:00
LyAhn 31b46327fd feat(lightbox): add slideshow mode
Adds a fullscreen image-only slideshow from the current lightbox collection, with pause, keyboard navigation, hidden idle controls, and polished image transitions.

Adds slideshow duration and playback order settings, including random order support.
2026-07-02 08:19:13 +01:00
LyAhn 29d9106039 Merge AI tag maintenance tools
github/actions/ci GitHub Actions CI finished: failure
Add an AI tag reset flow across Settings and Explore so AI-generated tags, AI tagging metadata, and stale tagging jobs can be cleared without affecting user tags or other media data.

Also add subtle AI source indicators in the tag manager and an Extreme UI Lab scenario for stress-testing large tag/library counts.
2026-07-01 11:38:51 +01:00
LyAhn a78111c8d4 test(ui-lab): add extreme mock scenario
Add an extreme UI Lab scenario with virtual-scale library, album, cluster, and tag counts while keeping the rendered media fixture set manageable.

Return the full extreme tag set from the mock backend so tag manager layouts can be stress-tested with 10k-100k tag counts.
2026-07-01 10:31:59 +01:00
LyAhn 4cdbc54d18 feat(ai-tags): add reset flow
Add a reset_ai_tags backend command that removes AI-generated tag rows, clears AI tagging metadata, cancels active tagging jobs, and drops queued or failed tagging jobs in the selected scope.

Expose reset actions in AI Workspace and the Explore tag manager, refresh gallery/progress/tag state after reset, and add subtle AI source indicators to tag manager rows.
2026-07-01 10:31:22 +01:00
LyAhn d5b93b2e21 feat(folder-picker): add editable address navigation
github/actions/ci GitHub Actions CI finished: success
Add a File Explorer-style dual-mode path bar to the folder picker so users can navigate with breadcrumbs or switch into an editable address field for pasted paths. Also allow staging the current or typed path directly and show friendlier feedback for missing folders.
2026-07-01 01:00:18 +01:00
LyAhn 257b2b54e7 chore: update changelog 2026-06-30 23:43:17 +01:00
LyAhn d619b01f2e refactor(ui): replace native app tooltips
github/actions/ci GitHub Actions CI finished: success
Wrap app controls with the shared Tooltip component and use cursor-positioned tooltip placement for icon buttons, chips, path labels, and media controls.

Leave native title attributes on the window chrome buttons so Minimize, Maximize, and Close keep platform-style behavior.
2026-06-30 23:25:43 +01:00
LyAhn 1a971899d1 feat(search): filter tag results by colour
Pass the active colour filter through tag searches and apply the existing palette match inside the tag query and count paths.

Update the UI Lab mock backend so colour filtering behaves the same way when testing tag search results.
2026-06-30 23:24:25 +01:00
LyAhn 8fe5daf25d Dev/codex cloud setup (#9)
## Summary
- Adds `scripts/codex-cloud-setup.sh` for Codex Cloud environment setup
- Installs Linux/Tauri native dependencies, Node/pnpm, Rust tooling, JS dependencies, and Rust crates
- Uses CPU-safe Rust checks with `--no-default-features` because Phokus enables CUDA by default
- Adds UI Lab/browser guidance for `pnpm dev:ui` on port `1422`

## Notes
- The script is intended to be pasted into the Codex Cloud setup field or run from the repo root.
- It avoids full release builds during setup to keep Codex cache warm without making environment creation too heavy.

Reviewed-on: #9
2026-06-30 14:35:20 +00:00
LyAhn 619bd0c9d2 Merge: rename 'tag cloud' to 'visual clusters' across the stack
github/actions/ci GitHub Actions CI finished: success
Disambiguate the Explore feature naming: the visual k-means cluster view was
named tag_cloud/tagCloud/TagCloud everywhere while the actual tag list is
explore_tags, which was easy to confuse. The cluster side is now
visual_cluster/visualCluster/VisualCluster (command, types, store, DB cache
table, and the ExploreView component); the tags side and the user-facing
"Tag Cloud" label are unchanged. Includes a mock-fixture fix so the dev
"huge" scenario surfaces a realistic field of clusters.
2026-06-30 10:01:25 +01:00
LyAhn 996bb71375 fix(mock): scale visual-cluster fixtures to the scenario
The mock Explore clusters were a fixed 10 regardless of scenario, so the "huge"
dev scenario looked sparse next to its large tag vocabulary. Mirror the backend's
k = (n / 20).clamp(5, 30): the huge scenario now surfaces ~30 clusters with a
long-tailed, big-library-sized count distribution, so Explore looks realistic.
2026-06-30 09:59:50 +01:00
LyAhn 0d9229635b refactor(explore): rename misnamed "tag cloud" to "visual clusters"
The visual k-means cluster feature was confusingly named tag_cloud / tagCloud /
TagCloud across the whole stack, while the actual tag list is explore_tags — the
two were trivially easy to mix up (and did cause confusion). Rename the cluster
side to visual_cluster / visualCluster / VisualCluster everywhere: command
get_tag_cloud -> get_visual_clusters (+ lib.rs registration and the invoke
string), VisualClusterEntry, the store fields/actions/tokens, and the mock
backend. Old names are retired rather than reused, so any missed reference fails
loudly instead of silently resolving to the wrong concept.

The tags side keeps its accurate explore_tags naming, and the user-facing
"Tag Cloud" UI label is unchanged.

Also rename the SQLite tag_cloud_cache table -> visual_cluster_cache (the old
table is dropped during schema setup — it is a disposable cache already
invalidated by the clustering version bump) and the TagCloud.tsx component file
-> ExploreView.tsx, since it is the Explore container hosting both the cluster
and tag views.
2026-06-30 09:48:38 +01:00
LyAhn cdb8aa20b9 Merge: faster Explore visual clustering on large libraries
Sampled, parallel k-means with density-aware k-means++ seeding makes first-time
visual clustering on large libraries fast, without a single cluster swallowing
tens of thousands of generic images. Together with the earlier cache-hit
optimization, the Explore tab no longer stalls for several seconds on 80k+
libraries.
2026-06-30 09:10:15 +01:00
LyAhn d2af84d9e8 perf(explore): sampled, parallel visual clustering for large libraries
Computing visual clusters was O(n·k·dim) per Lloyd iteration over the whole
library, single-threaded — several seconds on an 80k-image library on first
view. Find centroids on a deterministic, evenly-strided sample (<=3000
embeddings) and then assign every image to its nearest centroid in one parallel
rayon pass. Libraries at or below the sample cap are unchanged.

Replace the greedy farthest-point seeding (which seeds outliers, leaving the
dense core under-represented on a sample so one centroid absorbed tens of
thousands of generic images) with proper density-aware k-means++ D² seeding,
made deterministic via a small fixed-seed SplitMix64 PRNG. This keeps clusters
balanced on large libraries.

A CLUSTER_CACHE_VERSION is folded into the tag-cloud cache key so existing
caches computed by the old algorithm are invalidated and recomputed. The
clustering timing line and the cache-write failure now go through the `log`
facade (debug/warn) instead of eprintln.
2026-06-30 09:08:48 +01:00
LyAhn 68a9df5ab3 feat(lightbox): two-column metadata layout
github/actions/ci GitHub Actions CI finished: failure
Lay the lightbox info-panel metadata out in a two-column grid: paired fields
(Dimensions/Duration, Video codec/Audio codec, Type/File size) sit side by side
while Rating, Modified, and Embedding span the full width. More compact panel
with less scrolling. Tags and EXIF sections are unchanged.
2026-06-29 20:35:13 +01:00
LyAhn 79ce458fd5 feat(tags): open the tag manager from Settings
Add an "Open tag manager" button under a new Tag library group in Settings →
AI Workspace. It closes Settings and jumps to Explore's tag Manage mode.

To make manage mode reachable from outside the Explore view, lift its flag out
of TagCloud's local state into the store (tagManagerOpen / setTagManagerOpen)
behind an openTagManager() action. Manage mode is reset whenever Explore is
entered normally or the visual-cluster view is selected, so openTagManager()
stays the only path that opens it programmatically.
2026-06-29 20:26:49 +01:00
LyAhn a9a8f8422e feat(ui): quick theme switch from the settings cog
Right-clicking the settings cog in the title bar opens a small theme menu
(Phokus / Subtle Light / Conventional Dark) with the active theme checked,
anchored under the cog and dismissed on outside-click or Escape. Left-click
still opens Settings. Keeps theme switching one gesture away without cluttering
the title bar with another icon.
2026-06-29 20:26:08 +01:00
LyAhn ab7022e118 perf(explore): instant tag-cloud cache hits + fix stale-on-switch loading
The get_tag_cloud cache key was built by loading and hashing every embedding
blob for the scope *before* checking the cache, so even a cache hit re-read
hundreds of MB on large libraries and stalled Explore for several seconds.
Validate the cache from a lightweight image-ID-set signature plus the embedding
revision instead, so a hit never loads embeddings. The ID-set hash keeps the key
membership-sensitive (add/remove/move between folders) and the revision covers
an image being re-embedded in place. Cache write failures are now logged rather
than silently ignored.

On the frontend, switching folders (or re-entering Explore) no longer leaves the
previous folder's clusters/tags on screen with no loading indicator:
loadTagCloud/loadExploreTags clear stale entries on a real folder switch. The
displayed folder is tracked separately (exploreTagsShownFolderId) from the
cache-dirty marker so a same-folder invalidation (tag edits, new AI tags) does
not masquerade as a switch and wipe the visible list mid-refresh.
2026-06-29 20:25:38 +01:00
LyAhn 23e9850c7a Merge feat/tagging-ux: tagging model choice, related tags, tag manager, and UX polish
github/actions/ci GitHub Actions CI finished: failure
- JoyTag added as a second selectable tagger alongside WD; model switches on demand, tags attributed per-model

- Related tags atlas in Explore: clicking a tag shows co-occurring tags with animated connection lines and image counts

- Persist worker pauses across restarts via a Settings toggle

- Tag manager gains live filter, sort (most-used / least-used / A–Z / Z–A), and virtualisation for large libraries

- Tooltip portal with anchorToCursor mode for precise hover positioning

- Noisy AI tags (e.g. generic background descriptors) filtered at store time; existing tags cleaned on migration

- Fix: tag cloud hover glow and atlas gradient now adapt to Subtle Light theme

- Fix: AI Workspace 'Selected Folders' scope no longer pre-selects the first folder

- Fix: changelog version lookup strips build suffixes so What's New works in UI Lab
2026-06-29 18:36:01 +01:00
LyAhn c111032d99 fix(changelog): strip build suffixes before version lookup
UI-lab builds append a suffix to the version string (e.g.
"0.1.1-ui") which caused getChangelogForVersion to find no match and
fall back to the "not available in-app" message. A regex now strips any
hyphen-and-letter suffix before the lookup so the What's New modal
renders correctly in all build modes.
2026-06-29 18:06:45 +01:00
LyAhn c13f78c68b docs(changelog): add unreleased entries for feat/tagging-ux
Adds the user-facing changes introduced on this branch that were missing
from the [Unreleased] section:

Added:
- Related tags in Explore (tag atlas connection lines + co-occurrence counts)
- Persist worker pauses across restarts (Settings toggle)

Changed:
- Tag manager search/sort/virtualisation

Fixed:
- Noisy AI tags filtered automatically (removal list applied at store time)
- Explore Tag Cloud hover glow and atlas gradient in Subtle Light theme
- AI Workspace "Selected Folders" scope no longer pre-selects a folder
2026-06-29 17:23:20 +01:00
LyAhn f2939d70ab fix: atlas glow and AI Workspace scope pre-selection in light theme
Two targeted fixes:

- Tag Cloud atlas SVG radial gradient now switches its inner stop from white
  to a warm dark tone (rgba 60 50 30) when the Subtle Light theme is active,
  matching the explore-tag hover glow style used elsewhere in light mode.
- Switching the AI Workspace tagging queue scope to "Selected Folders" no
  longer auto-selects the first folder in the list; the selection now starts
  empty so the user can choose exactly which folders to target.
2026-06-29 17:23:03 +01:00
LyAhn 8dbabc2d9e fix: match tag cloud hover glow to dark mode in light theme
Replace the flat background hover override with a dark warm radial
gradient on the ::before pseudo-element, mirroring the same elliptical
glow effect used in dark mode.
2026-06-29 17:06:50 +01:00
LyAhn 5bc397af01 chore: add Playwright test scaffolding
Adds @playwright/test and @types/node as dev dependencies, playwright.config.ts
with a localhost:1420 base URL targeting the Vite dev server, an example
spec, and the standard Playwright output directories to .gitignore.
2026-06-29 16:30:05 +01:00
LyAhn af3c8418ee feat: virtualized tag manager with filter, sort, and light-theme support
Replaces the flat TagManageRow list with a virtualized grid of
TagManageTile cards using @tanstack/react-virtual and dynamic measured
heights (46px idle, 82px when editing/confirming). Adds a filter input
and a sort dropdown (most-used / least-used / A-Z / Z-A). renameTag and
deleteTag no longer clear exploreTagEntries on invalidation so the
manager keeps its filter/sort state during the background refresh.
Light-theme overrides cover all new tag-manager class names.
2026-06-29 16:29:12 +01:00
LyAhn 9144be2518 feat: Tooltip portal with anchorToCursor mode
Tooltip now portals cursor-anchored variants into document.body via a
`mounted` guard, preventing transformed parents and scroll-container
overflow from distorting coordinates. New `anchorToCursor` prop locks
position at hover entry without tracking; `followCursor` retains the
spring-animated tracking behaviour. Color swatches (ColorFilter),
timeline scrubber dots/labels (Timeline), and toolbar dropdowns (Toolbar)
are updated to use the appropriate cursor mode. Toolbar z-index bumped
z-20→z-40 (dropdowns z-30→z-50) to layer above portaled content; tag
autocomplete result guard added (Array.isArray).
2026-06-29 16:28:35 +01:00
LyAhn d81624573d feat: persist worker-pause state across restarts
Worker pause states can optionally survive app restarts. A new toggle in
Settings saves the current pause map to settings/worker_pauses.json; on
startup lib.rs restores it before workers are spawned. Backend: new
snapshot/replace helpers in indexer.rs, persist functions in commands.rs
(get/set_worker_pauses_persist). Frontend: workerPausesPersist store
field, load/setWorkerPausesPersist actions, toggle in SettingsModal.
2026-06-29 13:21:53 +01:00
LyAhn 949382f28c feat: related-tags atlas in Explore view
Hovering a tag in Explore now loads and displays co-occurring tags as a
weighted cloud. New `get_related_tags` SQL self-join (db.rs/commands.rs),
`loadRelatedTags` store action with per-folder keyed cache, and TagCloud
atlas UI with ResizeObserver-driven layout and RAF animation. Explore tag
limit raised to 180; tag cloud auto-refreshes 700ms after new AI tagging
completes.
2026-06-29 13:21:43 +01:00
LyAhn e4a63c8bb0 fix: filter noisy AI tags
Add an editable AI tag removal list and apply it to WD/JoyTag output before tags are stored. Clean existing generated AI tags during database migration while leaving manual user tags untouched.
2026-06-29 11:24:21 +01:00
LyAhn 1685134116 fix(tagger): model-neutral session log; changelog for JoyTag
create_tagger_session is shared by both models but logged "WD tagger: using
CPU execution provider" even when loading JoyTag. Make the CPU/DirectML
session logs model-neutral. Add the JoyTag tagging-model picker to the
changelog now that it's runtime-verified.
2026-06-29 10:26:51 +01:00
LyAhn 705f8c2e56 feat(tagger): Settings picker to switch tagging model (WD / JoyTag)
Adds a "Tagging model" control to Settings → AI Workspace that calls
get/set_tagger_model, alongside the existing acceleration/threshold/batch
controls. Switching refreshes the model status so the download/ready row and
the model name/description reflect the selected model, and the threshold hint
shows the model-appropriate default (0.4 for JoyTag, 0.35 for WD).

Frontend only; mirrors the existing acceleration toggle pattern. End-to-end
JoyTag tagging still needs the model files on disk to verify.
2026-06-29 09:51:30 +01:00
LyAhn 52d54d2404 feat(tagger): add JoyTag provider behind a tagger-model abstraction
Introduces a selectable tagging model so Phokus isn't locked to the
anime-leaning WD tagger. JoyTag uses the Danbooru schema but generalizes to
photographic content and is strong on NSFW concepts — a better fit for a photo
manager while keeping the explicitness range.

- `TaggerModel` enum (wd | joytag) persisted as a `tagger_model` setting;
  `model_dir`, status, and download now follow the active model (per-model
  repo/dir/file list). WD stays the default.
- `Tagger` trait implemented by `WdTagger` and the new `JoyTagger`;
  `create_active_tagger` builds the selected one. The worker holds
  `Box<dyn Tagger>` and rebuilds on model change (TAGGER_SESSION_DIRTY).
- Shared `assemble_batch` skeleton (pack -> one forward pass -> per-image
  fallback, results in input order); both providers and the shared
  decode/pad/resize are de-duped onto common helpers.
- JoyTag specifics: NCHW + RGB + CLIP-normalized input (vs WD's NHWC/BGR/raw),
  flat top_tags.txt labels, logits -> sigmoid -> threshold (default 0.4). It has
  no native rating, so the explicitness rating is derived from its NSFW tags.
- Tags are attributed to the model that produced them (ai_tagger_model),
  via Tagger::model_name, instead of a hardcoded WD constant.
- New get/set_tagger_model commands.

Backend only; the Settings model picker and end-to-end testing against the
JoyTag model files come next.
2026-06-29 09:40:12 +01:00
LyAhn 3a2b134103 Merge perf/tagging: faster, non-freezing AI tagging
github/actions/ci GitHub Actions CI finished: success
Tagging inference no longer monopolises the GPU. It now runs in small
chunked forward passes with a brief yield between them, so a tagging run
keeps the UI responsive instead of freezing the whole app for seconds at a
time — worst on first launch with a cold batch. Throughput is also steadier
(the old wide batches caused periodic slowdowns).

CPU-provider tagging is multi-threaded instead of pinned to a single core
(~2.7x faster on a representative machine), leaving headroom so the rest of
the app stays responsive. DirectML/GPU tagging behaviour is unchanged.
2026-06-29 09:19:40 +01:00
LyAhn 71ad7bf762 perf(tagger): multi-thread CPU inference instead of pinning to one core
The ONNX session was built with intra_threads(1) unconditionally. That's
correct for DirectML (compute is on the GPU), but on the CPU execution
provider it pinned all matmul/conv work to a single core — ~1.78s/image,
~14s for an 8-image batch.

Derive the intra-op thread count from available_parallelism() for the CPU EP
(leaving 2 logical cores free for the UI and a possible concurrent scan;
tagging is the lowest-priority worker, so heavier workers are idle when it
runs). DirectML/Auto keep a single thread. Measured ~2.7x on a representative
machine (14s -> 5.3s per 8-image batch); sublinear because swinv2 inference is
memory-bandwidth-bound. The selected thread count is logged at load.
2026-06-29 09:09:29 +01:00
LyAhn 992417710f perf(tagger): chunked, yielding inference so tagging stops freezing the UI
The tagging worker claimed a batch from the DB but ran the model one image
at a time, so `tagger_batch_size` had no effect on inference. Batching the
whole claim into a single DirectML forward pass fixed that but introduced a
worse problem: on a shared GPU each wide dispatch locks the device (and the
WebView2 compositor with it) for 1-3.7s, freezing the entire app while
tagging runs — worst of all on first launch with a cold graph compile.

The WD model is compute-bound here (~50-230ms/image of actual GPU work), so
a wide batch buys almost no throughput; it only lengthens each uninterruptible
GPU lock. So decouple DB claim size from GPU granularity: claim
`tagger_batch_size` for DB efficiency, but feed the GPU in TAGGER_INFER_CHUNK
(4) images per forward pass with a brief yield between chunks. Each dispatch
now lasts ~200-900ms, the UI gets windows to paint, peak decode memory is
bounded, and the cold compile is for a smaller shape. As a bonus the wide-batch
throughput spikes disappear — steady ~1.6-1.8s/16 vs the old 0.8-3.7s swings.

- run_batch: parallel (rayon) decode + single forward pass per chunk, with
  per-image fallback and decode failures kept attached to their input slot.
- Worker iterates source_paths.chunks(TAGGER_INFER_CHUNK), yielding 40ms
  between chunks; outputs zip back to jobs 1:1, write tx unchanged.
- Remove now-dead WdTagger::run() (fallback uses infer_one directly).
2026-06-29 01:09:47 +01:00
LyAhn 4c6da99507 Merge feat/ui-lab: Add browser-only UI Lab
github/actions/ci GitHub Actions CI finished: success
- Adds a Vite UI Lab mode that boots the real Phokus frontend with mocked
  Tauri APIs.
- Introduces deterministic mock backend scenarios for rich, empty, busy,
  duplicate, album, error, and large-library states.
- Adds fixture media plus a mediaSrc helper so browser previews and Tauri file
  URLs share one rendering path.
- Documents the UI Lab workflow and exposes it through pnpm dev:ui.
2026-06-29 00:12:51 +01:00
LyAhn 24d4e82950 feat: expand UI Lab media fixtures
Add a derived dev-media fixture pack and wire UI Lab scenarios to use the broader image set for more useful browser screenshots.
2026-06-29 00:06:58 +01:00
LyAhn 7a18011b0f 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.
2026-06-29 00:04:48 +01:00
LyAhn cebd709391 fix: anchor cluster Open pill to corner so it stays on small cards
github/actions/ci GitHub Actions CI finished: success
2026-06-28 21:19:32 +01:00
LyAhn d55e4c7502 docs: changelog for unreleased fixes
github/actions/ci GitHub Actions CI finished: success
2026-06-28 21:13:12 +01:00
LyAhn a40a2e8d12 chore: split production build into CPU and CUDA scripts 2026-06-28 21:13:12 +01:00
LyAhn 2ce1547844 fix: improve Explore cluster layout and light-theme readability 2026-06-28 21:12:26 +01:00
LyAhn 623aabbb51 fix: surface actively-processing folder over paused one in background tasks bar 2026-06-28 21:12:24 +01:00
LyAhn f65fd350cc fix: make toolbar, sidebar, and lightbox responsive on small screens 2026-06-28 21:12:22 +01:00
LyAhn 86a1a53289 fix: clamp window to monitor work area so it fits small screens on first launch 2026-06-28 21:12:21 +01:00
LyAhn ebf16e8cb9 fix: skip Phokus's own app-data dir when indexing to break self-indexing loop 2026-06-28 21:12:21 +01:00
LyAhn e7d9c39fd1 docs: regroup unreleased changelog Added entries
github/actions/ci GitHub Actions CI finished: success
2026-06-28 14:56:00 +01:00
LyAhn 136d74a81b Merge feat/smart-albums-multiselect: albums, multi-select, EXIF, tag management, color search
- Manual albums (create/rename/delete/reorder; multi-select bulk add/remove) in a distinct sidebar section
- Gallery multi-select + bulk actions (tag, rate, favorite, add-to-album, delete with disk-delete confirmation)
- Lightbox EXIF/camera panel (on-demand) with a GPS map link
- Tag management in Explore (rename/merge/delete, library-wide)
- Album-scoped similar-image search
- Color search — filter the gallery by dominant color (swatches + custom picker), with background backfill
- Reusable tooltip component, duplicate-finder delete confirmation, CPU/CUDA build badge
- Post-review security/accessibility hardening (validated URL-open commands, etc.)
2026-06-28 14:45:31 +01:00
LyAhn bb0038e0a1 chore: post-review hardening + changelog link tooltip
Security / robustness / a11y polish on top of the color-search + tooltips work:

- URL opening: route through validated backend commands (open_map_location
  with lat/lon bounds-checking, open_changelog_url with a fixed URL) instead
  of the frontend opener:allow-open-url capability, which is now removed.
- EXIF GPS parsing: validate coordinate ranges and require the correct
  N/S/E/W hemisphere ref byte, then clamp.
- Guard double-submit on album create / add-to-album (Lightbox, BulkActionBar,
  Sidebar) and discard stale autocomplete responses in the bulk tag editor.
- Gallery tile: stop nesting <button>s — non-interactive tile div with an
  overlay button for open/toggle; checkbox and Similar promoted with z-index
  + focus rings.
- Accessibility: keyboard handlers, focus-visible rings, and aria on album
  rows, tag-manage actions, and tile controls; Tooltip uses a block <div>
  wrapper in block mode and aria-hidden when hidden.
- Show the destination URL in a tooltip on the "Full changelog" link so the
  user can see where it goes before clicking.
- Toolbar "All" filter also clears the color filter; color-filtered views no
  longer get unfiltered newly-indexed images injected.
- Sidebar reorder: bail if the album set changed mid-drag.
- Tooling: add cargo fmt scripts; alphabetize package.json scripts.
2026-06-28 14:39:21 +01:00
LyAhn 90dec3b212 feat: add color search and reusable tooltips
Add dominant-color palette extraction, storage, filtering, and startup backfill so the gallery and Timeline can be filtered from the toolbar color picker.

Introduce a reusable tooltip component and migrate the color filter, update indicator, and gallery filename hover affordances to it.
2026-06-28 11:04:33 +01:00
LyAhn e3fde46e91 feat: add album scope for similar image search
Similar search scoping:
- Add current_album as a similar-scope option and remember the source album when similar or region searches are launched from an album.
- Route gallery and lightbox similar actions through scope-aware store helpers so Album/Folder/All choices are applied consistently.
- Keep pagination and scope toggles working for both whole-image and region-search result sets.

Backend filtering:
- Extend find_similar_images and find_similar_by_region params with album_id, giving album scope precedence over folder scope.
- Add album_membership filtering for HNSW whole-image search and brute-force crop embedding search.
2026-06-28 01:21:31 +01:00
LyAhn a12e81d8bd feat: lightbox EXIF panel, tag management, reorderable albums
EXIF info panel:
- New on-demand get_image_exif command (kamadak-exif) returning camera/lens/
  aperture/shutter/ISO/focal-length and decimal GPS; read from the file when the
  lightbox opens (no DB schema change, works on already-indexed images).
- Lightbox shows a Camera panel; GPS opens the location in the browser via
  OpenStreetMap (adds opener:allow-open-url capability).

Tag management:
- Backend rename_tag (rename, or merge when the target exists) and delete_tag
  (library-wide), both clearing the tag-cloud cache; store actions invalidate
  tag caches, refresh Explore, and re-point/refresh an active tag-search.
- Explore -> Tag Cloud gains a Manage mode: a flat list with per-tag rename/
  merge/delete.

Albums:
- Drag-to-reorder in the sidebar via framer-motion Reorder with a hover handle;
  order persists through the existing reorder_albums command.

Reads live store order on drag end and robustly derives GPS hemisphere from raw
EXIF ref bytes (review follow-ups). CHANGELOG updated.
2026-06-27 23:50:44 +01:00
LyAhn 6bef90b7fb feat: manual albums + gallery multi-select with bulk actions
Albums (manual collections):
- New albums/album_images tables with FK cascades; DB functions and Tauri
  commands for create/rename/delete/delete-many/reorder/list, add/remove
  images, and paginated get_album_images.
- Distinct sidebar "ALBUMS" section with cover thumbnails, create/rename/
  delete, and a Manage multi-select mode for bulk album deletion.
- Album view reuses the gallery grid (activeView "album" + selectedAlbumId);
  spans folders; add from the bulk bar or the lightbox, remove from within.

Gallery multi-select + bulk actions:
- gallerySelectedIds selection model with a top-left corner checkbox that
  reveals on corner hover; click-to-toggle in selection mode, double-click
  to open.
- Floating BulkActionBar: tag (inline autocomplete popover), rating,
  favorite, add-to-album, and a delete with an explicit "from disk"
  confirmation. Batch commands bulk_update_details/bulk_add_tags/
  bulk_remove_tag.

Also:
- Duplicate Finder delete now requires confirmation with clear "from disk"
  wording (was single-click fire-and-forget).
- CPU/CUDA build-variant badge in Settings (get_build_variant).
- Rating/favorite no longer re-sorts derived collections (similar/region/
  semantic/tag/album results); single and bulk paths replace in place there.
- Album-aware indexed-images/media-updated handlers so thumbnails paint and
  newly-indexed files don't leak into an album view.
- CHANGELOG updated.
2026-06-27 15:23:54 +01:00
LyAhn 55cd3b5aa7 Merge feat/whats-new-updater-ux: What's New screen + updater UX + light-theme fixes
github/actions/ci GitHub Actions CI finished: success
Post-0.1.1 polish queued for the next release (0.1.2):

- What's New: post-update toast that opens an in-app, offline release-notes
  screen (collapsible Added/Changed/Fixed sections) sourced from the bundled
  CHANGELOG.md, reopenable from Settings -> Updates. Backed by a
  last_seen_version settings file that tells upgrades from fresh installs.
- Updater: the progress toast reappears when an update is started from the
  title-bar indicator or Settings after the prompt was dismissed; Settings
  gains a real download progress bar with a percentage.
- Light theme: fix the recurring subtle-light breakage — neutral surfaces rely
  on the CSS-variable remap instead of light-theme:bg-white, and the green
  action buttons drop the broken light-theme:hover:bg-emerald-200 for an
  override-free emerald tint that auto-themes (updater, What's New, onboarding).
- Debug panel: What's New triggers.
2026-06-23 21:36:35 +01:00
LyAhn c878970180 feat: What's New screen + updater progress, plus light-theme fixes
Post-update UX for the next release (0.1.2):

- What's New: after a version change, greet the user with a toast that opens
  an in-app release-notes screen (collapsible Added/Changed/Fixed sections)
  sourced from the bundled CHANGELOG.md, reopenable from Settings -> Updates.
  Backed by a last_seen_version settings file (get/set_last_seen_version
  commands) that distinguishes upgrades from fresh installs.
- Updater: the download/install progress toast now reappears when an update is
  started from the title-bar indicator or Settings after the prompt was
  dismissed; Settings -> Updates gains a real progress bar with a percentage.
- Light theme: fix the recurring subtle-light breakage. Neutral surfaces now
  rely on the CSS-variable remap instead of light-theme:bg-white (which forced
  surfaces dark because --color-white is remapped dark); the green action
  buttons drop the broken light-theme:hover:bg-emerald-200 (remapped dark,
  unreadable on hover) for an override-free emerald-500 tint that auto-themes,
  across the updater, What's New, and onboarding.
- Debug panel: add What's New triggers (toast / modal / reset).
2026-06-23 21:33:08 +01:00
LyAhn 1a95e31f78 docs: fill in 0.1.1 release checksums
Add the SHA-256s for the published CPU and CUDA installers to the
0.1.1 release-notes draft.
2026-06-23 20:26:25 +01:00
LyAhn e1e89b0f87 chore(release): 0.1.1
github/actions/ci GitHub Actions CI finished: success
github/actions/release GitHub Actions release finished: success
Bump version to 0.1.1 (package.json, Cargo.toml, Cargo.lock), date the
changelog section (2026-06-23), and add the GitHub release-notes draft.

QoL release on top of 0.1.0: custom multi-folder picker, theme system,
timeline scrubber, folder reordering, gallery/duplicate-finder
virtualisation, video playback settings, rebuild-semantic-index action,
plus AVIF thumbnail, video-embedding, and Subtle Light theme fixes.
2026-06-23 20:00:08 +01:00
LyAhn 0909b58110 docs: update changelog with QoL-02 and AVIF fixes 2026-06-23 08:55:08 +01:00
LyAhn 4f9ab0b821 fix: support AVIF thumbnail processing
github/actions/ci GitHub Actions CI finished: success
Route AVIF thumbnail generation through the bundled FFmpeg path instead of the Rust image decoder, avoiding unsupported-format failures without requiring system dav1d dependencies.

Requeue existing AVIF jobs that previously failed with unsupported-format errors and feed generated JPEG derivatives to embedding/tagging preprocessing while leaving lightbox display on the original AVIF file.
2026-06-22 20:43:46 +01:00
LyAhn a06e76c7a7 fix: resolve Rust Clippy CI failures
github/actions/ci GitHub Actions CI finished: success
Derive default implementations for captioner and tagger option enums, simplify sorting and progress multiple checks, and remove redundant iterator conversions.
2026-06-21 21:00:46 +01:00
LyAhn 1e008244ae fix(db): suppress too_many_arguments clippy lint on count_images
github/actions/ci GitHub Actions CI finished: failure
2026-06-21 19:40:35 +01:00
LyAhn ebed194f17 Merge feat/qol-02: QoL polish — folder picker, settings, duplicate finder
github/actions/ci GitHub Actions CI finished: failure
- Custom multi-folder picker replaces the native OS dialog: browse the
  filesystem in-app, stage multiple folders at once, and add them in one
  shot. Virtualised list handles large directory trees without jank.
- Duplicate Finder group list is now virtualised, keeping the UI
  responsive even with hundreds of duplicate groups.
- Settings panel reordered: General is now the first (default) section.
- Lightbox video playback toggles (autoplay / auto-mute) added to
  General settings, wired to the store with ARIA switch roles.
- "Rebuild Semantic Index" maintenance action added to Settings.
- Folder picker QoL fixes: chevron tooltip, Unix breadcrumb root label,
  and partial-failure staging panel cleanup.
2026-06-21 19:30:35 +01:00
LyAhn 3684b98d55 fix(folder-picker): address QoL issues from PR review
- Fix chevron tooltip: was "Open folder" in both branches; now shows
  "No subfolders" when the entry has no children (consistent with the
  existing opacity-45 visual cue on the same chevron icon)
- Fix Unix breadcrumb root label: was always "Home" even for non-home
  paths like /mnt/data — now labelled "/" which is always accurate
- Fix partial-failure staging: on a mixed add result, successfully-added
  entries are now removed from the staging panel so only genuinely failed
  folders remain for the user to retry (index-pairing is safe because the
  backend returns results in input order via a preserved .map())
2026-06-21 19:21:00 +01:00
LyAhn 74a4134f2f feat: add custom multi-folder picker
Replace native add-folder dialogs with an in-app folder picker that supports collecting folders from multiple locations before adding them together.

Add backend directory listing and batch add commands with duplicate skipping, plus store actions and a themed picker UI with a dedicated folders-to-add panel.
2026-06-21 17:38:01 +01:00
LyAhn f66fbe7931 feat(settings): add "Rebuild semantic index" maintenance action
Drops and recreates the sqlite-vec tables at the current model dimension, then
re-queues every image for embedding. Fixes "dimension mismatch" search errors
that occur when the vector table was built for a different model/dimension (e.g.
after experimenting with 768-dim models against this 512-dim build).

The rebuild runs under the embedding worker's DB write lock and resets the job
queue inside a single transaction, so it can't interleave with an in-flight
embedding batch (per code review).
2026-06-21 15:17:34 +01:00
LyAhn 5870205047 feat(settings): General-first layout + lightbox video playback toggles
Reorder the Settings sections so General is the top and default section instead
of AI Workspace. Add two persisted settings under a new "Video playback" group:
- Autoplay in lightbox (default on)
- Start muted (default off)

VideoPlayer reads these when a video opens — autoplaying only when enabled and
starting muted when enabled, otherwise falling back to the session's last-used
mute state. Settings apply to the next opened video, not the current one.
2026-06-21 14:39:41 +01:00
LyAhn 3db95a4489 perf: virtualize the Duplicate Finder group list
The duplicate view rendered every group and every thumbnail at once — a 5,000-pair
result mounted ~10K <img> elements, making scroll lag heavily (same class of bug
as the old per-month Timeline). Virtualize the group list so only on-screen cards
mount; heights are measured dynamically since each group wraps a variable number
of copies.
2026-06-21 13:45:11 +01:00
LyAhn c1ab651131 Merge: drop dead settings-modal theme CSS
github/actions/ci GitHub Actions CI finished: failure
Removes subtle-light .settings-modal overrides that targeted classes no component
on main applies (dead rules), keeping the Settings modal flat and themed via the
global accent variables.
2026-06-21 12:58:21 +01:00
LyAhn 166ffdb189 chore: remove dead .settings-modal theme overrides
These subtle-light overrides target classes (settings-modal, settings-nav-active,
settings-model-card) that no component on main applies, so the rules never took
effect. They also pulled the Settings modal toward a card look we don't want. The
accent-text readability is now handled globally by the theme's accent variables,
so the modal themes correctly (flat) without this block.
2026-06-21 12:58:19 +01:00
LyAhn 58750b169a Merge Subtle Light accent-text readability fix
Coloured text/icons (warnings, errors, status) used pastel accent shades tuned
for dark UIs and washed out on the light theme — e.g. "Update check failed" in
Settings was near-invisible. Darken those accent variables in the subtle-light
theme while resetting them to the bright originals inside .media-dark-surface,
so light chrome reads clearly and on-photo overlays keep their signal colours.
2026-06-21 12:53:44 +01:00
LyAhn 1e148bdf18 fix: make accent text readable in Subtle Light theme
Pastel accent shades (amber/red/rose/emerald/sky/violet/blue -200..-400) are
tuned for dark UIs and washed out on the light chrome — e.g. "Update check
failed" in the Settings updates row was near-invisible. Darken those accent
variables in the subtle-light theme so coloured text/icons stay readable, and
reset them to Tailwind's bright defaults inside .media-dark-surface so on-photo
overlays (ratings, failed badges, the lightbox region tool) keep their signal
colours. Remapping the variable rather than the utility also covers opacity
variants like text-amber-300/90.
2026-06-21 12:34:04 +01:00
LyAhn 7367845f8b Merge 0.1.1 QoL fixes
github/actions/ci GitHub Actions CI finished: failure
Post-release quality-of-life work for 0.1.1:

- Timeline: right-edge year/month scrubber, full-library load so the scrubber
  spans everything, and per-row virtualization so dense months scroll smoothly.
- Gallery grid row-virtualization; folder reordering (drag + keyboard) with
  persisted custom order and A-Z/Z-A/Custom sort.
- Theme system (Phokus, Subtle Light, Conventional Dark) with an onboarding
  theme picker, plus subtle-light parity fixes for the lightbox panel, media
  overlays, duplicate finder, and window controls.
- Failed AI-tagging locate/filter from the background worker prompt.
- Perf: background media-updated batches no longer re-sort the whole image set.
- Video embedding jobs no longer churn through false failures before their
  thumbnail exists.
- Tooling: changelog helper + notes.
2026-06-21 08:48:53 +01:00
LyAhn 50e8bc8e4d docs: record 0.1.1 QoL fixes in changelog 2026-06-21 08:48:20 +01:00
LyAhn 779a18f56e fix: use valid end-of-input anchor in changelog-add
JS regex has no \z anchor — it matched a literal 'z', so appending to the last
section under [Unreleased] silently failed and duplicated the heading. Use
$(?![\s\S]) to anchor to true end-of-input.
2026-06-21 08:48:19 +01:00
LyAhn d027de675b fix: debounce folder keyboard-reorder persistence
Holding Up/Down on a folder's drag handle fired a reorder_folders DB write per
keystroke. Update the local order immediately for responsiveness, but debounce
the persist (400ms) with cleanup on unmount.
2026-06-21 08:48:18 +01:00
LyAhn b7cfc9177e fix: subtle-light theme parity for lightbox panel and media surfaces
In subtle-light, the lightbox dragged its whole surface (including the metadata
panel) dark via media-dark-surface, while conventional-dark themed the panel
normally. Scope the dark surface to the media canvas and re-light the panel by
remapping --color-* variables on a .lightbox-panel wrapper (Tailwind v4 resolves
every colour utility through these vars, so this re-themes the subtree —
including accents — with no !important). Mark gallery/duplicate media tiles as
media-dark-surface so their on-image overlays stay light-on-dark, and theme the
window restore icon via var(--color-gray-950) instead of a hardcoded hex.
2026-06-21 08:48:05 +01:00
LyAhn 479de76ebb perf: stop full re-sort on media-updated batches
replaceExistingImages re-sorted the entire loaded image array on every
media-updated event. Harmless for the ~200-item gallery window, but in Timeline
(which loads the whole library) it was an O(n log n) pass many times per second
during background indexing — severe lag, occasional crashes. Thumbnail/metadata
fills don't change list position (Timeline re-buckets by taken_at separately), so
replace records in place and skip the sort; return the same array reference when
nothing matched to avoid a wasted re-render.
2026-06-21 08:48:05 +01:00
LyAhn 21f6c30d25 perf: row-virtualize Timeline instead of per-month
Each month was a single virtual item that rendered all of its tiles, so
scrolling into a busy month mounted thousands of ImageTiles at once. Flatten
months into a fixed-height row list (header rows + tile rows of `cols` images),
mirroring the Gallery grid, so only on-screen rows render and thumbnails stream
in as you scroll. Active-month tracking and scrubber jump-to-month are remapped
to the flat row model.
2026-06-21 08:47:48 +01:00
LyAhn 1df75fd490 Refine explore and settings theming 2026-06-20 18:08:39 +01:00
LyAhn a4c928345c chore: add changelog helper and unreleased notes 2026-06-18 00:49:30 +01:00
LyAhn ca58c2ddd4 fix: add failed tag locate and filter controls
Add a failed-tag discovery flow for background worker failures.

Changes:
- Add a Failed Tags toolbar filter that appears when tag failures exist.
- Add Locate buttons for failed tag tasks in the background worker prompt.
- Route Locate to the affected folder and filter the gallery to images with tagger errors.
- Fetch and display failed tag filenames/errors in the expanded worker details.
- Add a backend query and gallery filter flag for images with failed AI tagging.
- Improve subtle-light contrast for failed worker chips, filenames, and Locate/Retry buttons.
- Also slightly increases the title bar update indicator pulse size for better visibility.
2026-06-18 00:36:02 +01:00
LyAhn c97fec2eb3 fix: improve light theme onboarding controls
Make the onboarding tour theme-aware across the app themes, add the first-run theme picker, and keep fake media previews on the dark media surface. Update light-mode secondary controls in onboarding, settings, dropdowns, toolbar controls, and duplicate actions so they no longer render as dark buttons on subtle-light.
2026-06-17 22:07:35 +01:00
LyAhn 9047c8053a feat: 0.1.1 — timeline scrubber, gallery virtualisation, folder reorder, QoL polish
Timeline:
- Add a right-edge scrubber (year labels + month dots) that jumps to any
  period; runs in the same direction as the scrolled content
- Load the full filtered set in Timeline view so the scrubber spans the whole
  library instead of just the first page

Gallery & UI:
- Virtualise the gallery grid
- Folder reordering in the sidebar (drag + persisted sort_order) and a themed
  dropdown component
- Subtle Light theme contrast fixes for toggles, secondary buttons and the
  update toast

Embedding workers now defer video jobs without a thumbnail at claim time and
requeue any previously-failed deferred jobs on startup, so videos no longer
churn through failed embeddings.

QoL polish across BackgroundTasks, DuplicateFinder, Lightbox and VideoPlayer.
2026-06-17 18:37:37 +01:00
LyAhn f049f8c997 Update ci.yml
flip paths-ignore to paths and point to src + src-tauri
2026-06-15 21:44:34 +01:00
LyAhn 3e0f59300e feat(website): launch phokus.jezz.wtf product site
github/actions/ci GitHub Actions CI finished: cancelled
Single-page marketing site built with React 19, Vite 7, and Tailwind v4.
Covers the full product story: local-first privacy, semantic search, explore/timeline,
curation, deduplication, and download. Screenshots transcoded to AVIF/WebP at build
(~5.3 MB masters → ~0.5 MB served). Self-hosted Inter and Space Grotesk variable fonts.
Mobile-first redesign with phone-optimised hero, swipeable feature cards, and touch-friendly
navigation. Includes the standalone Phokus aperture SVG asset.

CI: exclude website/ and docs/ from the Rust/Tauri check workflow.
2026-06-15 21:25:55 +01:00
LyAhn 00bf7da344 feat(website): redesign the mobile experience
Replace the stacked desktop layout with a phone-first product flow featuring an image-led hero, inline privacy proof, compact semantic search, swipeable feature cards with live pagination, expandable technical details, and touch-friendly navigation and calls to action. Add the standalone Phokus aperture SVG asset.
2026-06-15 21:15:18 +01:00
LyAhn e14dbda41d feat(website): finish responsive polish
Self-host the Inter and Space Grotesk variable fonts, tighten the mobile layout, hide oversized edge marks on narrow screens, and add keyboard focus plus reduced-motion safeguards. Exclude website and documentation-only changes from the desktop CI workflow.
2026-06-15 19:55:01 +01:00
LyAhn 072c3887cf feat(website): add Phokus product site with optimized media pipeline
Single-page marketing site for phokus.jezz.wtf in the website/ pnpm workspace
(React 19 + Vite 7 + Tailwind v4). Sections: Hero, Local-first, Search, Explore
& timeline, Curate, Cleanup, Tech facts, Download. Product-first composition with
a recurring aperture motif (EdgeMark) bleeding off alternating edges.

Screenshots are transcoded to AVIF/WebP at build via vite-imagetools (sharp);
~5.3MB of masters -> ~0.5MB served. Commits only the 7 in-use captures.
2026-06-15 19:39:56 +01:00
LyAhn 584a92b7cd ci: report GitHub Actions status to Gitea
github/actions/ci GitHub Actions CI finished: failure
2026-06-15 00:36:25 +01:00
LyAhn 6a5cf0afe3 docs(changelog): date the 0.1.0 release (2026-06-14)
CI / check (push) Has been cancelled
2026-06-14 20:28:22 +01:00
LyAhn ce804f5aa5 chore(gitignore): drop blanket *.json ignore, track configs directly
CI / check (push) Has been cancelled
The global *.json rule (added for transient ComfyUI workflow dumps) forced
git add -f on real configs and hid tauri.conf.json from tauri-action's
gitignore-aware globbing. Removed it and the negation workarounds; local-only
ignores (e.g. skills-lock.json) live in .git/info/exclude instead.
2026-06-14 20:10:18 +01:00
LyAhn 40fcd1b469 ci(release): un-ignore tauri.conf.json + fix updater-json input
CI / check (push) Has been cancelled
Release / release (push) Has been cancelled
- tauri-action globs for tauri.conf.json honoring .gitignore; the global
  *.json rule hid it (force-added but still ignore-matched), so detection
  failed with 'Failed to resolve Tauri path' and it tried to re-init.
  Negate it like the cuda overlay already is.
- rename uploadUpdaterJson -> includeUpdaterJson (valid v0.6.2 input).
2026-06-14 19:58:54 +01:00
LyAhn 6b504aaae1 ci(release): pin tauri-action to v0.6.2 (the v1 ref does not exist)
Release / release (push) Has been cancelled
CI / check (push) Has been cancelled
2026-06-14 19:31:12 +01:00
LyAhn 0bd99e2c7a release: Phokus 0.1.0 release preparation
CI / check (push) Has been cancelled
Release / release (push) Has been cancelled
Merge chore/release-prep into main for the first public release.

- Release pipeline: CI (fmt, clippy -D warnings, tsc) + tag-triggered
  GitHub Actions building the NSIS installer as a draft release.
- Self-updater via GitHub Releases — launch check, Settings UI, update
  toast, and a title-bar focal-point indicator that lights up when a new
  version is ready.
- Production hardening: real CSP, scoped asset protocol, rotating file
  logging, single-instance, window-state.
- Guided first-run onboarding with background FFmpeg provisioning and
  resilient (curl-based, resumable) model/DLL downloads.
- Branding: Phokus aperture mark across the icon set + title bar, with a
  reusable PhokusMark component.
- CUDA installer variant with bundled runtime DLLs (built locally).
- MIT license, bundle metadata, NSIS-only target, and docs (README
  install/privacy, CHANGELOG, 0.1.0 release notes).
2026-06-14 18:27:48 +01:00
LyAhn 0144526a3d feat(onboarding): add a closing 'Staying current' step on updates + the app mark
- new StepUpdates: explains the title-bar update indicator with a mini
  app-window mockup, and introduces the aperture mark as the app's identity
- move the tour-closing copy off the AI features step onto this final step
- changelog: note the title-bar update indicator + one-click install, and the
  new onboarding step
2026-06-14 18:26:35 +01:00
LyAhn 2785b7d5e6 feat(updates): titlebar update indicator + dev injectors for the update flow
- light up the Phokus iris's focal point (pulsing amber) in the titlebar when
  an update is pending; click goes straight to install, with a fast custom
  tooltip (native title delay was too slow)
- PhokusMark gains an optional dotClassName to render the central focal point
- DemoPanel: inject every updater state (available/downloading/installing/
  error/up-to-date), which is otherwise unreachable without a remote latest.json
2026-06-14 17:11:00 +01:00
LyAhn 797247e900 feat(branding): replace default Tauri icon with the Phokus aperture mark
- regenerate src-tauri/icons from the new branding/phokus-aperture.svg master
- add reusable PhokusMark component (inline SVG, inherits currentColor)
- use it for the titlebar app mark, retiring the placeholder
2026-06-14 15:15:28 +01:00
LyAhn e4373195fe chore(dev): add a dev-only demo panel for screenshotting transient UI
The background-worker pipeline drains in under a second on a fast machine,
making it impossible to screenshot live. This adds a dev-only panel
(Ctrl+Shift+D) that injects a frozen mid-pipeline state into mediaJobProgress
so the worker bar can be captured at leisure, then cleared. Gated by
import.meta.env.DEV and verified absent from the production bundle, so it
never ships. A first rough cut of the broader screenshot/demo mode.
2026-06-14 09:39:29 +01:00
LyAhn 72a1a886a7 fix(sidebar): refresh UI immediately when removing a library
Removing a folder only reloaded the gallery when that exact folder was the
active selection, so on All Media (or another view) its images stayed on
screen until a manual refresh. Now the folder is dropped from the sidebar
optimistically (instant feedback while the backend deletes its images), the
gallery is always reloaded, and a failed delete resyncs the folder list.
2026-06-13 23:10:09 +01:00
LyAhn c15eed6655 fix(onboarding): no image flash when switching search demos
Switching search modes briefly showed the next demo's result images because
the results container stayed mounted and faded its opacity down from the
previous demo's visible state. Key it by demoIndex so it remounts fresh at
opacity-0 and only fades in once the new query finishes typing.
2026-06-13 21:48:41 +01:00
LyAhn 602c271531 feat(tagger): resilient model and runtime-DLL downloads via curl
ureq's read timeout doesn't fire on a stalled large transfer on some Windows
TLS stacks (schannel), so a stall hangs the download forever with no recovery.
Download the ONNX runtime DLLs and the tagger model through the system
curl.exe (Win10 1803+/Win11), which has a real inactivity timeout
(--speed-time) plus resume (-C -). The size probe uses curl too, so nothing in
the download path depends on ureq.

A stalled download discards its partial when it gives up (no more deleting the
model folder by hand to restart) and fails to a retryable error in a couple of
minutes rather than locking the UI on 'preparing'. Adds progress, extraction,
and runtime-init logging. Verified downloading both models on real Windows 11
hardware.
2026-06-13 21:48:41 +01:00
LyAhn 54fa8ab117 build(cuda): ship a CUDA installer variant with bundled runtime DLLs
The default build (default = candle-cuda) load-time-imports the CUDA runtime,
so it crashes on any machine without the toolkit. Add a CUDA release variant
that bundles the redistributable runtime DLLs (cudart/cublas/cublasLt/curand,
verified via dumpbin) so it runs on any NVIDIA machine with just a driver.

- tauri.cuda.conf.json: a 'tauri build --config' overlay (CPU release
  untouched) that points the updater at latest-cuda.json so a CUDA install
  never self-updates into the CPU build
- windows/cuda-hooks.nsh: NSIS hook embeds the DLLs via File and extracts them
  next to phokus.exe, where the Windows loader searches
- build:app:cuda script; cuda-redist/ gitignored (DLLs copied from the toolkit
  locally), with a negation so the overlay stays tracked
2026-06-13 21:48:40 +01:00
LyAhn 23095a6d05 docs: add installation, privacy, changelog, and 0.1.0 release notes
Phase 6 release collateral:
- README: Installation (unsigned-build/SmartScreen note, requirements,
  first-run downloads) and Privacy & local-first sections
- CHANGELOG.md seeded with the 0.1.0 feature set
- docs/release-notes-0.1.0.md draft for the GitHub Release body

Also corrects the CLIP model download size from ~330 MB to ~580 MB (the
actual model.safetensors is 577 MB) across the docs and the onboarding UI.
2026-06-13 10:34:15 +01:00
LyAhn 075c7e4cfb feat(sidebar): pause/resume all background work from folder context menu
The onboarding tour tells users they can right-click a folder to pause its
background work, but only the per-worker buttons in the BackgroundTasks bar
existed. Add a 'Pause/Resume background work' context-menu item that toggles
all four workers (thumbnail/metadata/embedding/tagging) for the folder.

To keep the bar and the menu in sync, worker-pause state moves from
BackgroundTasks' local state into the store (the canonical owner of app
state), so pausing from either surface reflects in both. Reuses the existing
set_worker_paused / get_worker_states commands; no backend change.
2026-06-13 09:42:51 +01:00
LyAhn 2f66b0bdb8 feat(onboarding): fuller, varied semantic search demo results
Replace the two near-identical pale ocean sunsets with three distinct
'golden sunset over water' stills — a vivid coastal sunset, a soft golden
ocean, and a golden alpine lake — so the /s demo shows a convincing,
varied result set. Drops the redundant sunset2 asset.
2026-06-13 09:29:40 +01:00
LyAhn ed9c061ac1 fix(onboarding): hide search results until typed, trim gallery grid to 2 rows
- search demo results were greyed-but-visible while the query was still
  typing, as if the app pre-knew the search; they're now invisible (space
  reserved, no layout jump) and fade in only once typing completes
- gallery preview dropped from 3 rows to 2 so the 'Click any tile' explainer
  text is visible without scrolling
2026-06-13 09:23:30 +01:00
LyAhn 09810cb868 refactor(onboarding): play animations once with replay, fix search demo results
Two refinements from testing:
- the gallery-reveal, pipeline-drain, and search-typing animations looped
  forever; they now play once and stop, with a Replay button to re-run
- the search demo's result tiles didn't match the queries. Now: a filename
  search returns one exact file, '/s golden sunset over water' shows only
  ocean-sunset stills (added a second), and '/t landscape' shows the three
  actual landscapes (valleys + alpine lake), not a foggy forest
2026-06-13 09:19:38 +01:00
LyAhn f8e981c6f6 feat(tagger): real byte progress for model download instead of a spinner
The 1.3 GB model.onnx downloaded via hf-hub's repo.get() and the ONNX
runtime DLLs via a read_to_end — neither reported bytes, so the user saw
only an indeterminate spinner on a multi-minute download.

- tagger model files now download via repo.download_with_progress with a
  HubProgress adapter over hf-hub's Progress trait (throttled 200ms events
  carrying downloaded/total bytes)
- captioner's nuget DLL download streams in 64 KB chunks, parsing
  content-length, and reports per-chunk byte progress
- TaggerModelProgress carries downloaded_bytes/total_bytes; onboarding and
  Settings show a determinate bar plus 'X MB / Y MB' for the current file
2026-06-13 08:56:49 +01:00
LyAhn 9c135179a3 feat(onboarding): real demo stills in place of gradient placeholders
Swap the fake gallery tiles' gradient stand-ins for 13 rights-clean
generated WebP stills (512px, ~25 KB each). Search demo now returns
thematically matched results — beach/dunes for the filename query, sunset
and water for /s, landscapes for /t — so the show-don't-tell steps look
like a real library. tileGradient retained for the abstract Explore and
Timeline mini-previews.
2026-06-13 08:36:50 +01:00
LyAhn b72f140737 fix(tagger): provision ONNX runtime DLLs during tagger model prep
On a clean install the WD tagger download failed instantly: only the
(UI-removed) caption model prep ever downloaded the shared ONNX Runtime
DLLs, while ensure_onnx_runtime — despite its comment — only initializes
and errors when the DLL is absent. Worse, the OnceLock cached that error
for the whole session, and the onboarding step swallowed the message.

- new captioner::provision_onnx_runtime downloads missing DLLs; tagger
  prep calls it before init
- ensure_onnx_runtime no longer caches failure (Mutex<bool>, success-only
  latch) so a later download can recover in-session
- onboarding AI step now displays taggerModelError
2026-06-13 00:00:35 +01:00
LyAhn 3b7190d353 fix(onboarding): honest framing of AI features step
Semantic search is part of the standard pipeline (CLIP auto-downloads on
first embedding batch), not opt-in — only AI tagging is. Reword the step
so 'optional' applies to what is actually optional. A true embedding
opt-out toggle is tracked as a follow-up.
2026-06-12 23:56:04 +01:00
LyAhn 7403f0cfeb feat(onboarding): guided first-run tour with background FFmpeg provisioning
Phase 5 of the 0.1.0 release prep:
- FFmpeg no longer blocks startup (or crashes the app offline): provisioning
  runs in a background thread emitting throttled ffmpeg-progress events,
  with installed/downloading/failed state queryable via get_ffmpeg_status
  and a retry command
- video jobs are invisible to claiming and tier-priority checks until FFmpeg
  is ready, so they wait as pending without failing — and without starving
  image embeddings/tagging (include_videos gating in db.rs)
- 7-step show-don't-tell onboarding wizard: welcome + live FFmpeg progress,
  real first-folder picker, faked animating pipeline bar, placeholder
  gallery tiles, cycling search-syntax demo, views overview, and an AI
  features step with a real opt-in tagger download
- skippable at every point (Escape included), persisted via
  settings/onboarding_completed.txt, re-runnable from Settings > General,
  which also gains an FFmpeg status/retry row
2026-06-12 23:09:05 +01:00
LyAhn 1640e30330 refactor(backend): zero clippy warnings, gate CI with -D warnings
Auto-fix pass (uninlined format args, needless borrows, to_vec,
size_of_val, map_err->inspect_err) plus hand fixes: fully annotated the
sqlite-vec transmute (c_char for cross-platform correctness), type alias
for the duplicate-scan tuple, slice signatures over &mut Vec/&PathBuf,
and #[allow(dead_code)] documenting the intentionally dormant caption
worker. The CI clippy step now fails on any new warning.
2026-06-12 20:59:40 +01:00
LyAhn 178754f6c3 feat: add Pan and Drag to lightbox
Users can now click and drag to pan the image when zoomed in. Previously it would just zoom into the centre of the image without any way of panning
2026-06-12 20:45:50 +01:00
LyAhn 3fb4a9685f feat(hardening): production logging, single-instance, CSP, and scoped asset protocol
Phase 4 of the 0.1.0 release prep:
- tauri-plugin-log: rotating file log (5 MB, Info) in the app log dir plus
  stdout; all 54 backend println!/eprintln! sites migrated to log macros so
  release builds finally produce diagnostics
- tauri-plugin-single-instance (registered first): second launches focus the
  existing window instead of racing the same SQLite DB with a second worker
  fleet; tauri-plugin-window-state persists window geometry
- real CSP replacing null (scripts self-only, media/images via the asset
  protocol, IPC endpoints whitelisted, object-src none) with a devCsp
  variant for Vite HMR
- asset protocol scope narrowed from '**' to thumbnails statically plus
  per-folder runtime grants (setup, add_folder, update_folder_path)

Panic audit (plan item) concluded with no changes: worker loops already
catch and log all Result errors; remaining unwraps are startup fail-fast,
poisoned-lock convention, infallible-by-construction, or test code.
2026-06-12 20:12:03 +01:00
LyAhn 890c23bdce feat(updater): self-update via GitHub Releases with launch check and settings UI
Phase 3 of the 0.1.0 release prep:
- tauri-plugin-updater + tauri-plugin-process wired into the builder, with
  updater:default / process:allow-restart capabilities
- bundle.createUpdaterArtifacts: true; endpoint points at the GitHub
  releases latest.json, pubkey baked into tauri.conf.json
- store: update status state machine (check / download progress / install)
  with a quiet launch check (prod only) that stays silent on network errors
- UI: dismissible update toast with download progress, plus an Updates group
  in Settings > General showing current version and manual check/install
2026-06-12 19:00:12 +01:00
LyAhn 34ced67fe1 ci: add CI checks and tag-triggered release workflow
Phase 2 of the 0.1.0 release prep, running on the GitHub mirror:
- ci.yml: frontend type-check/build, rustfmt gate, clippy (CPU-only) on
  windows-latest with pnpm + rust caching
- release.yml: v* tags build the NSIS installer via tauri-action and attach
  it to a draft GitHub Release with updater JSON; updater signing secrets
  referenced ahead of Phase 3
- both build --no-default-features: runners have no CUDA toolkit, and the
  shipped artifact must be the CPU/DirectML build
2026-06-12 18:41:32 +01:00
LyAhn 1629339569 build(deps): gate CUDA behind default feature, add CPU build scripts
Applies the stashed 'deps and cpu mode' work: cuda was hardcoded on the
candle dependency lines, making the candle-cuda feature flag dead and any
non-CUDA build impossible. Now default = [candle-cuda] for the main dev
machine, with pnpm dev:app:cpu / build:app:cpu (--no-default-features) for
CPU-only machines and CI. Also trims unused deps (log, uuid, tokio-full,
chrono-serde). Verified: cargo check --locked --no-default-features passes.
2026-06-12 18:41:32 +01:00
LyAhn 1d782a6d57 style(backend): apply rustfmt across the backend
Mechanical cargo fmt pass so the CI rustfmt gate starts green; no semantic
changes (verified token-level + cargo check).
2026-06-12 18:36:31 +01:00
LyAhn 69e53ed62a chore(release): add MIT license, bundle metadata, and single NSIS target
Phase 1 of the 0.1.0 release prep:
- LICENSE (MIT) plus license fields in Cargo.toml, package.json, and bundle config
- tauri.conf.json: publisher/copyright/descriptions/homepage so the NSIS
  installer no longer derives 'jezz' from the identifier as manufacturer
- version: null in tauri.conf.json — Cargo.toml is now the version source
- bundle.targets narrowed from 'all' to nsis (updater-friendly, faster builds)
- track pnpm-workspace.yaml (esbuild build-script approval, needed by CI)
2026-06-12 18:20:27 +01:00
LyAhn d84c74e241 Merge feat/settings-ui-refinement: settings redesign, in-view scoping, custom video player
UI/UX pass delivering:

- feat(settings): flat desktop redesign — VS Code-style form rows
  replace the card-based layout, dialog sizes proportionally with the
  app window (85% capped at 1400x900); fixes thumbnail-cleanup stats
  blanking mid-clean and the Compact now button staying clickable
  after compaction
- feat(views): in-view folder scope switching — Timeline, Explore, and
  Duplicates get a header scope dropdown so changing folders no longer
  bounces through the sidebar
- feat(lightbox): custom immersive video player — edge-to-edge
  playback with auto-hiding controls, drag scrubbing, volume, speed,
  loop, fullscreen, and full keyboard support, replacing the native
  browser player
- docs(readme): feature list and architecture brought up to date
2026-06-12 13:01:49 +01:00
LyAhn 4cd3bbd4fd docs(readme): bring feature list and architecture up to date
Adds everything shipped since the last README pass: live file tracking
with rename-aware watching, Timeline view, batched notifications with
mute/pause, the custom lightbox video player, in-view folder scoping
for Explore/Timeline/Duplicates, three-phase duplicate scanning,
maintenance tools (database compaction, thumbnail cleanup), per-folder
worker pausing, and the strict-priority background worker pipeline.
Features are now grouped by area, and the stack/how-it-works sections
mention mozjpeg scaled decoding, EXIF capture dates, and the watcher.
2026-06-12 12:49:30 +01:00
LyAhn 86ce7bc8e2 feat(lightbox): custom immersive video player
Replace the native <video controls> element with a custom player.
Videos now fill the lightbox media area edge-to-edge on black instead
of sitting in a padded, rounded box, with an auto-hiding control bar
over a bottom gradient (2.5s idle, always visible while paused):
play/pause, drag scrubber with buffered ranges, time readout, volume
slider with session-persisted volume/mute, playback speed (0.25-2x),
loop, and fullscreen.

Click toggles playback, double-click toggles fullscreen. Keyboard:
Space play/pause, M mute, F fullscreen, L loop, Shift+Left/Right seek
5s, Up/Down volume (unmutes) — plain arrows still navigate between
media, and all player keys are ignored while typing in the info-panel
inputs. Videos autoplay on open, falling back to paused-with-controls
if the webview blocks it.
2026-06-12 12:39:04 +01:00
LyAhn b02bf1da2b feat(views): in-view folder scope switching
Timeline, Explore, and Duplicates get a folder-scope dropdown in their
headers (Timeline via the shared Toolbar), so changing scope no longer
means leaving the feature and bouncing through the sidebar. The new
setViewFolderScope store action updates selectedFolderId while
preserving activeView: Timeline/Gallery reload images, Explore reloads
via its existing selectedFolderId effect, and Duplicates loads the
cached results for the new scope (fresh scans remain an explicit
Rescan). Sidebar behavior is unchanged — clicking a library still
opens its gallery.
2026-06-12 12:39:04 +01:00
LyAhn cd7dd89f00 feat(settings): flat desktop redesign with proportional sizing
Complete presentation rework of the Settings modal away from the
card-based tablet look:

- Dialog now sizes proportionally (85vw x 85vh, capped at 1400x900)
  and resizes with the app window instead of a fixed centred square.
- Content rebuilt as flat form rows in the VS Code preferences style:
  uppercase group headings with hairline-divided label/control rows —
  no nested cards, panels, or boxed strips. Sidebar retained.
- Duplicated content header removed; close button floats top-right.
- Folder selection restyled from bordered chips to flat divider rows;
  model path and runtime check render as plain monospace text.

Also fixes two maintenance-section bugs: thumbnail-cache stats no
longer blank to em-dashes while a cleanup is running, and the Compact
now button disables immediately after compaction instead of allowing
pointless re-runs until the section remounted.
2026-06-12 12:07:52 +01:00
LyAhn b1290268a7 Merge feat/smart-file-tracking: file tracking, notifications, and worker performance
Delivers rename-aware file tracking plus a major background-worker
performance series:

- feat: smart thumbnail cleanup and rename-aware file tracking —
  watcher handles atomic renames in place (preserving thumbnails and
  embeddings); thumbnails deleted alongside DB rows in all removal paths
- feat: notification batching, per-folder mute, and global pause
- feat(duplicates): phased scan progress with skipped-file reporting
- fix(settings): thumbnail cleanup UX with live state and time warning
- perf(thumbnails): scaled JPEG decode (mozjpeg), RGB8 pipeline, direct
  video frame grabs, continuous worker batching
- perf(workers): scaled parallel embedding preprocessing, idle-only
  backoff, EXIF orientation applied before embedding
- fix(indexer): embedding/tagging no longer claim folders mid-scan
- perf(metadata): idle-only backoff and per-item commits
- perf(workers): strict priority pipeline (thumbnails -> metadata ->
  embeddings -> tagging)
2026-06-12 11:21:25 +01:00
LyAhn d30fe47876 perf(workers): strict priority pipeline across background workers
Workers now form a priority pipeline (thumbnails -> metadata ->
embeddings -> tagging): a worker only claims jobs when no
higher-priority tier has claimable work, so stages drain one at a time
at full speed instead of all workers contending for CPU (shared rayon
pool), disk, and the DB writer simultaneously.

Each tier gate is a single read-only EXISTS query per idle tick using
the same exclusion set the corresponding claim would (that worker''s
paused folders plus actively-indexing folders), so paused or mid-scan
work never blocks lower tiers, and failed jobs stop gating once they
leave pending. Deferred workers wake within one backoff interval
(250-750ms) of the higher tier draining.

A watcher-driven trickle of new files briefly pauses lower tiers by
design. A user-initiated tag run during a large embedding backlog
waits for it; pausing embeddings per folder overrides if tags are
wanted first.
2026-06-12 10:45:12 +01:00
LyAhn cbfcbea96a perf(metadata): idle-only backoff and per-item commits
The metadata worker now sleeps its 250ms backoff only when the queue
is empty or a batch errored, claiming the next batch immediately while
work is pending. Each ffprobe result is committed and emitted
individually (in its own transaction) instead of after the whole
batch, so video metadata progress moves steadily rather than stalling
for up to 16 sequential probes.
2026-06-12 10:36:05 +01:00
LyAhn 948a489a8a fix(indexer): keep embedding and tagging off folders mid-scan
Embedding and tagging claims now exclude actively-indexing folders,
matching the thumbnail and metadata workers. Previously the embedding
worker started processing a folder while it was still being scanned,
competing with the scanner for CPU (shared rayon pool), disk, and the
DB writer — slowing scans and skewing the adaptive storage profile
toward Conservative. Video embedding jobs claimed mid-scan also
fail-fasted pointlessly since their thumbnails are deferred until
indexing completes; the existing backfill at scan end covers requeue.
2026-06-12 10:36:05 +01:00
LyAhn 334ac54e00 perf(workers): scaled embedding preprocessing and idle-only backoff
- Embedding and tagging workers now sleep their backoff interval
  (500ms/750ms) only when the queue is empty, the model is not ready,
  or a batch errored — previously every batch paid the sleep even with
  work pending.
- CLIP preprocessing no longer decodes originals at full resolution:
  decode_for_thumbnail is generalized into decode_image_scaled with a
  cover mode (shortest side >= target) and the embedder decodes JPEGs
  at the smallest DCT scale covering its 224px fill-crop, in parallel
  via rayon. The forward pass, not image decode, is now the dominant
  embedding cost.
- EXIF orientation is now applied before embedding, so rotated photos
  embed the way they are displayed (previously embedded sideways).
  Existing stored embeddings are unaffected.
2026-06-12 09:00:25 +01:00
LyAhn a4486547e8 perf(thumbnails): scaled JPEG decode and continuous worker batching
Major speedups for large-folder thumbnail generation:

- JPEGs decode through mozjpeg at the smallest DCT scale covering
  320px instead of full resolution, with catch_unwind and a fallback
  to the image crate for anything mozjpeg rejects. EXIF orientation
  preserved in both paths.
- RGB8 pipeline end to end (no RGBA round-trip) and CatmullRom resize
  in place of Lanczos3.
- Video posters grab the seeked frame directly instead of running the
  ffmpeg thumbnail filter (which decoded ~100 frames), with decode
  threads capped at 2 per process.
- Workers only sleep when the queue is empty rather than 250ms after
  every batch. Image batches commit before videos start, and videos
  run sequentially off the rayon pool (blocking ffmpeg waits were
  starving image decode) with per-item commits so progress keeps
  moving through slow stretches.
2026-06-12 08:13:12 +01:00
LyAhn 665c315f56 feat(duplicates): phased scan progress with skipped-file reporting
Replace the [scanned, total] tuple progress event with a structured
{phase, processed, total, skipped} payload covering all three scan
phases (checking, hashing, confirming). find_duplicates now returns a
DuplicateScanResult with scanned/candidate/skipped counts, and the UI
surfaces phase labels plus a warning when unreadable files were
skipped. Also adds a clean:app script for cargo clean.
2026-06-12 08:12:49 +01:00
LyAhn a34d38d9d3 feat: notification batching, per-folder mute, and global pause
Debounce embedding/tagging completion notifications per folder (6s window)
so rapid file additions from downloaders fire one notification instead of
one per file. Add per-folder mute via the sidebar context menu and a global
pause toggle in Settings > General, both persisted across restarts.
2026-06-11 06:28:16 +01:00
LyAhn b89e7406e9 fix(settings): improve thumbnail cleanup UX with live state and time warning
Clear stale stats during cleanup, show zero when done, and warn when the
file count is large enough to take a few minutes.
2026-06-09 06:45:24 +01:00
LyAhn ceb51f8fad feat: smart thumbnail cleanup and rename-aware file tracking
Delete thumbnails alongside DB rows in all three removal paths (watcher
detect, delete-from-disk command, remove-folder command) so orphans no
longer accumulate passively.

Intercept RenameMode::Both watcher events and handle them as in-place
path updates: renames the thumbnail file to the new hash and updates the
DB row without touching the embedding, avoiding a full rebuild on move.
Falls back to thumbnail regeneration if the file rename fails.
2026-06-09 01:16:12 +01:00
307 changed files with 30411 additions and 6991 deletions
+22
View File
@@ -0,0 +1,22 @@
# Keep LF in the working copy for all text files (prettier enforces LF)
* text=auto eol=lf
# Windows scripts that genuinely need CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Binary assets — never touch line endings
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.gif binary
*.ico binary
*.icns binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.mp4 binary
*.onnx binary
+20
View File
@@ -0,0 +1,20 @@
# External CI
CI and release builds run on the GitHub mirror because they require Windows
runner capacity. The GitHub workflows report their state back to this Gitea
repository through the commit status API.
Keep this directory in the repository. Gitea checks `.gitea/workflows` before
falling back to `.github/workflows`; an existing directory with no workflow
files prevents the GitHub-only workflows from being queued by Gitea Actions.
## Setup
1. In Gitea, create an access token with `write:repository` permission for an
account that can update `JezzWTF/phokus`.
2. In the GitHub repository, add that token as the Actions repository secret
`GITEA_STATUS_TOKEN`.
The status steps are non-blocking. If Gitea is temporarily unavailable, the
GitHub build result is preserved and the failed status update remains visible
in the workflow log.
+149
View File
@@ -0,0 +1,149 @@
name: CI
on:
push:
branches: [main]
paths:
- '.github/workflows/ci.yml'
# CHANGELOG.md is a build input: src/changelog.ts imports it ?raw for
# the What's New modal, and changelog.test.ts asserts on its content.
- 'CHANGELOG.md'
- 'index.html'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'src/**'
- 'src-tauri/**'
- 'tsconfig*.json'
- 'vite.config.ts'
pull_request:
paths:
- '.github/workflows/ci.yml'
# CHANGELOG.md is a build input: src/changelog.ts imports it ?raw for
# the What's New modal, and changelog.test.ts asserts on its content.
- 'CHANGELOG.md'
- 'index.html'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'src/**'
- 'src-tauri/**'
- 'tsconfig*.json'
- 'vite.config.ts'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
# windows-latest to match the release target — the ML crates (ort, candle)
# and the NSIS bundle only ever ship from Windows.
runs-on: windows-latest
timeout-minutes: 60
env:
CARGO_INCREMENTAL: 0
GITEA_STATUS_TOKEN: ${{ secrets.GITEA_STATUS_TOKEN }}
steps:
- name: Report pending status to Gitea
if: github.event_name != 'pull_request' && env.GITEA_STATUS_TOKEN != ''
continue-on-error: true
shell: pwsh
env:
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
$headers = @{
Authorization = "token $env:GITEA_STATUS_TOKEN"
Accept = "application/json"
}
$body = @{
state = "pending"
context = "github/actions/ci"
description = "GitHub Actions CI is running"
target_url = $env:GITHUB_RUN_URL
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "https://git.jezz.wtf/api/v1/repos/JezzWTF/phokus/statuses/$env:GITHUB_SHA" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Unit tests
run: pnpm test:unit
# tsc + vite build; also produces dist/ which tauri's build script expects
- name: Type-check and build frontend
run: pnpm build:vite
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Rustfmt
working-directory: src-tauri
run: cargo fmt --check
# --no-default-features: default enables candle-cuda for the main dev
# machine; CI runners have no CUDA toolkit and must build CPU-only.
- name: Clippy
working-directory: src-tauri
run: cargo clippy --all-targets --locked --no-default-features -- -D warnings
- name: Rust tests
working-directory: src-tauri
run: cargo test --locked --no-default-features
- name: Report final status to Gitea
if: always() && github.event_name != 'pull_request' && env.GITEA_STATUS_TOKEN != ''
continue-on-error: true
shell: pwsh
env:
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
JOB_STATUS: ${{ job.status }}
run: |
$state = switch ($env:JOB_STATUS) {
"success" { "success" }
"failure" { "failure" }
default { "error" }
}
$headers = @{
Authorization = "token $env:GITEA_STATUS_TOKEN"
Accept = "application/json"
}
$body = @{
state = $state
context = "github/actions/ci"
description = "GitHub Actions CI finished: $env:JOB_STATUS"
target_url = $env:GITHUB_RUN_URL
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "https://git.jezz.wtf/api/v1/repos/JezzWTF/phokus/statuses/$env:GITHUB_SHA" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
+158
View File
@@ -0,0 +1,158 @@
name: Release
# Tag pushes mirrored from Gitea (git.jezz.wtf) trigger this on the GitHub
# mirror. Builds the NSIS installer and attaches it to a DRAFT GitHub Release —
# review and publish manually.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to build, for example v0.1.1'
required: true
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
release:
permissions:
contents: write
runs-on: windows-latest
timeout-minutes: 120
env:
CARGO_INCREMENTAL: 0
GITEA_STATUS_TOKEN: ${{ secrets.GITEA_STATUS_TOKEN }}
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
steps:
# refs/tags/ qualification: a branch with a tag-like name must never win
# ref resolution. Works for tag pushes too — RELEASE_TAG is the tag name.
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ env.RELEASE_TAG }}
# On workflow_dispatch GITHUB_SHA is the dispatched branch head, not the
# tag's commit — resolve the real one for Gitea commit statuses.
- name: Resolve release commit
shell: pwsh
run: |
$sha = git rev-parse HEAD
"RELEASE_SHA=$sha" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Verify release version
shell: pwsh
run: |
if ($env:RELEASE_TAG -notmatch '^v\d+\.\d+\.\d+(-.+)?$') {
throw "Release tag '$env:RELEASE_TAG' must look like v0.1.1"
}
$packageVersion = (Get-Content package.json -Raw | ConvertFrom-Json).version
$cargoVersion = (Select-String -Path src-tauri/Cargo.toml -Pattern '^version\s*=\s*"(.+)"').Matches[0].Groups[1].Value
if ($packageVersion -ne $cargoVersion) {
throw "package.json version ($packageVersion) does not match Cargo.toml version ($cargoVersion)"
}
if ($env:RELEASE_TAG -ne "v$packageVersion") {
throw "Release tag '$env:RELEASE_TAG' does not match project version v$packageVersion"
}
- name: Report pending status to Gitea
if: env.GITEA_STATUS_TOKEN != ''
continue-on-error: true
shell: pwsh
env:
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
$headers = @{
Authorization = "token $env:GITEA_STATUS_TOKEN"
Accept = "application/json"
}
$body = @{
state = "pending"
context = "github/actions/release"
description = "GitHub Actions release is running"
target_url = $env:GITHUB_RUN_URL
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "https://git.jezz.wtf/api/v1/repos/JezzWTF/phokus/statuses/$env:RELEASE_SHA" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Build and create draft release
uses: tauri-apps/tauri-action@v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Updater artifact signing (Phase 3) — set these repo secrets once
# the updater keypair exists; empty values are ignored until then.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ env.RELEASE_TAG }}
releaseName: 'Phokus v__VERSION__'
releaseBody: 'See the assets below to download and install this version.'
releaseDraft: true
prerelease: false
includeUpdaterJson: true
updaterJsonPreferNsis: true
# Cargo args after `--`: ship the CPU/DirectML build — default
# features enable candle-cuda, which runners (and most users) lack.
args: '-- --no-default-features'
- name: Report final status to Gitea
if: always() && env.GITEA_STATUS_TOKEN != ''
continue-on-error: true
shell: pwsh
env:
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
JOB_STATUS: ${{ job.status }}
run: |
$state = switch ($env:JOB_STATUS) {
"success" { "success" }
"failure" { "failure" }
default { "error" }
}
$headers = @{
Authorization = "token $env:GITEA_STATUS_TOKEN"
Accept = "application/json"
}
$body = @{
state = $state
context = "github/actions/release"
description = "GitHub Actions release finished: $env:JOB_STATUS"
target_url = $env:GITHUB_RUN_URL
} | ConvertTo-Json
# RELEASE_SHA is unset if the job died before checkout; fall back.
$sha = if ($env:RELEASE_SHA) { $env:RELEASE_SHA } else { $env:GITHUB_SHA }
Invoke-RestMethod `
-Method Post `
-Uri "https://git.jezz.wtf/api/v1/repos/JezzWTF/phokus/statuses/$sha" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
+11 -2
View File
@@ -32,6 +32,15 @@ dist-ssr
# Misc
*.py
*.json
*.pyc
# Bundled CUDA runtime DLLs for the CUDA build (copied from the toolkit
# locally; ~600 MB, never committed).
src-tauri/cuda-redist/
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
+30
View File
@@ -0,0 +1,30 @@
# Build outputs
dist/
build/
# Tauri / Rust (handled by cargo fmt)
src-tauri/
# Lock files & generated
pnpm-lock.yaml
*.lock
# Generated
src/vite-env.d.ts
# Public assets
public/
# Website subpackage (has its own config if needed)
website/
# Markdown & docs (hand-written or tool-generated, keep diffs quiet)
*.md
docs/
# CI workflows
.github/
.gitea/
# Tool-managed config
.claude/
+14
View File
@@ -0,0 +1,14 @@
{
"semi": false,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
+269
View File
@@ -0,0 +1,269 @@
# Changelog
All notable changes to Phokus are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
(0.x: anything may change between minor versions).
## [0.2.0] — 2026-07-11
### Added
- **What's New** — after updating, Phokus now greets you with a "What's new"
toast that opens a tidy in-app tour of the new version. Added, Changed, and
Fixed notes are grouped into collapsible sections, so you can skim the good
bits without playing "spot the difference".
- **Quick theme switch** — right-click the settings cog in the title bar to
swap between Phokus, Subtle Light, and Conventional Dark instantly. No Settings
detour required.
- **Albums** — make your own cross-folder collections without moving a single
file. Albums live in their own sidebar section with cover thumbnails, can be
created, renamed, reordered, opened, and cleaned up in Manage mode, and deleting
one only removes the grouping.
- **Gallery multi-select** — hover a thumbnail's top-left corner to start
selecting, then use the floating action bar to tag, rate, favorite, add to an
album, or delete a whole batch at once. It also works in similar-image, region,
and album views, because bulk work should not disappear the moment you need it.
- **Colour search** — narrow the Gallery, Timeline, or tag results by dominant
colour using toolbar swatches or a custom picker. Great for those "I know it
was mostly blue" moments.
- **Album-aware similar search** — similar-image and region searches started from
an album can now stay inside that album, jump back to the source folder, or
search everything.
- **Tag manager** — Explore's Tag Cloud now has a Manage mode for renaming,
merging, and deleting tags across the whole library.
- **Camera info in the lightbox** — the info panel now shows EXIF details like
camera, lens, aperture, shutter speed, ISO, and focal length. Geotagged photos
also get a browser link for their GPS coordinates, and already-indexed images
do not need a re-index.
- **Build badge in Settings** — Settings -> Updates now shows whether you are
running the CPU build or the CUDA build.
- **Choose your tagging model** — Settings -> AI Workspace now lets you pick
between the anime-focused WD tagger and JoyTag, which is better suited to photo
libraries and stronger on NSFW concepts (if that's your thing, we don't judge).
New users get the same choice during the welcome tour, and each model keeps its
own confidence threshold instead of sharing one.
- **Reset AI tags** — a new reset action in Settings -> AI Workspace and the Tag
manager wipes AI-generated tags for a folder or the whole library, cancelling
any tagging still in flight. Tag manager rows now show which tags came from the
AI, so you know what you are about to lose before you lose it. Manually-added
tags are never touched.
- **Related tags in Explore** — Hover over a tag in the Tag Cloud to see the tags
that most often appear with it, complete with connection lines and image counts.
Handy for finding little clusters you did not know were there.
- **Pause workers for longer** — Settings -> General can now remember per-folder
worker pauses across app restarts, useful for folders you want to keep in the
library but leave out of background processing for now.
- **Editable folder path** — the folder picker now has an address bar, so you can
paste a path directly while still using breadcrumbs for quick jumps.
- **Slideshow mode** — turn the lightbox into a fullscreen, image-only slideshow
from whatever collection you are already browsing.
- **Add to album from the right-click menu** — right-click any image in the
Gallery or Timeline and file it straight into an album from the new "Add to
Album" submenu. One image, one click, zero ceremony.
### Changed
- **Settings got a spring clean** — preferences are now organised into General,
Media, Updates & Setup, Storage, and AI Workspace pages, so update and
maintenance controls no longer hide at the bottom of unrelated sections.
- **Menus got their act together** — right-click menus (images, folders,
albums, the theme switcher) and every dropdown (sort, folder scope, settings,
sidebar) now share one style with one set of manners: they stay on screen
instead of wandering off the edge, all close on Escape, and right-click menus
can do proper submenus now. Subtle Light dresses them all the same way too,
instead of saving the nice outfit for one dropdown.
- **Neater lightbox details** — image and video metadata now sits in two columns,
so the info panel shows more at a glance with less scrolling.
- **Faster Explore revisits** — returning to a folder's visual clusters should
feel much faster now, even in big libraries.
- **Calmer Tag Cloud during AI tagging** — Explore no longer keeps hammering the
tag list while a folder is actively being tagged, so tagging stays smoother and
the cloud catches up once the work settles.
- **Faster first-time clustering** — large libraries build their first visual
clusters much more quickly, while still keeping the groups nicely balanced.
- **Better tag browsing** — the Tag manager now has live search, sorting
(most-used, least-used, A-Z, and Z-A), smooth scrolling for huge tag lists, and
it keeps your filter/sort in place while you edit.
- **Safer deletion** — deleting media now asks for confirmation and clearly says
the file is being removed from disk. This covers gallery bulk delete and the
Duplicate Finder.
- **Clearer update progress** — Settings -> Updates now shows a real download
progress bar with a percentage instead of the old lonely "Downloading" label.
- **Better narrow-window layout** — the toolbar, filters, search box, colour
picker, sidebar, and lightbox info panel now adapt more gracefully when the
window is short on space.
- **Tidier Explore clusters** — busier clusters get more room, dense groups
overlap less, and everything should stay easier to read and click.
- **Faster CPU tagging** — CPU-only AI tagging can now use multiple cores while
leaving some breathing room for the rest of the app. GPU tagging is unchanged.
- **Smoother tooltips** — Phokus now uses its custom tooltip style across more of
the app instead of falling back to the native browser tooltip.
### Fixed
- **Explore no longer flashes the last folder** — switching folders now clears
the old clusters/tags and shows a loading state while the new folder catches
up.
- **Ratings keep your search order** — if you ever rated an image mid-search and
watched your results reshuffle themselves, that's over. Similar-image, region,
semantic, tag, and album results now stay put.
- **Update progress comes back when you need it** — if you dismiss the update
prompt and later start the update from the title bar or Settings, the progress
toast now reappears instead of hiding away in Settings.
- **Subtle Light cleanup** — fixed dark or hard-to-read surfaces, hover states,
dialogs, updater buttons, onboarding controls, and green action buttons in the
light theme.
- **No more self-indexing loops** — adding a broad folder like your whole user
profile used to send Phokus off indexing its own thumbnail cache, generating
thumbnails of thumbnails until the end of time. It now skips its own app-data
directory.
- **Background tasks show the active work first** — when one folder is paused and
another is processing, the active folder gets the main spot in the background
tasks bar.
- **First launch fits smaller screens** — on 1366x768-style displays, fresh
installs used to open with part of the app tucked below the taskbar. The window
now clamps itself to the usable monitor area.
- **Explore is clearer in Subtle Light** — cluster captions, buttons, cloud
words, hover glows, and the new connection lines now use stronger light-theme
colours.
- **Explore got a few sharp edges sanded down** — Cluster Cloud uses the in-app
tooltip, singular counts now say "1 image", and the folder-scope dropdown no
longer hides behind cluster cards.
- **AI tagging stays responsive** — starting a big tagging job used to turn the
rest of the app into a slideshow. GPU tagging now works in smaller bursts with
brief pauses between them, so the UI keeps moving and the first results land
sooner.
- **Lightbox AI tags wake up on time** — the lightbox's AI tags action no longer
stays disabled until you happen to open Settings, and it switches on as soon as
a model download finishes.
- **Noisy AI tags get cleaned up** — generic low-signal tags from WD and JoyTag
are filtered before they are saved, and matching older generated tags are
cleaned up on startup. Your manually-added tags are left alone.
- **Selected Folders starts empty** — choosing "Selected Folders" for AI tagging
no longer pre-selects the first folder. You decide exactly what gets queued.
- **A handful of tiny UI papercuts are gone** — the zoom buttons now show the
right tile size when you hover them, the folder picker no longer adds an odd
trailing slash to Windows drive breadcrumbs, the search field's clear button
no longer sits a few pixels too high, and menu labels no longer highlight like
text when you drag across them.
## [0.1.1] — 2026-06-23
### Added
- **Custom multi-folder picker** — replaces the native OS dialog with an
in-app folder browser that lets you navigate, stage folders from multiple
locations, and add them all in one go. Duplicate roots are skipped
automatically; partially-failed batches remove successfully-added entries
from the staging panel so only failed folders remain to retry.
- **Rebuild semantic index** maintenance action in Settings — drops and
recreates the vector tables at the current model dimension, then re-queues
every image for embedding. Fixes "dimension mismatch" search errors that
occur after switching between CLIP models with different output sizes.
- **Video playback settings** — new Video Playback group in Settings with two
persisted toggles: "Autoplay in lightbox" (default on) and "Start muted"
(default off). Settings apply to the next opened video rather than the
current one.
- **Timeline scrubber** — a year/month rail on the Timeline view that jumps to
any period in the library. Timeline now loads the full filtered set so the
scrubber spans the whole library instead of just the first page.
- **Folder reordering** in the sidebar — drag-and-drop (with edge auto-scroll)
or keyboard (↑/↓ on the drag handle), with the custom order persisted across
sessions; the Libraries list also gains AZ / ZA / Custom sort.
- Failed AI-tagging jobs can now be located from the background worker prompt,
including a gallery filter for images with failed tags and an expanded list
of failed filenames/errors.
- A new theme system adds Phokus, Subtle Light, and Conventional Dark chrome
options across the app.
- First-run onboarding now includes an inline theme picker so new users can
choose their preferred app chrome before continuing the tour.
### Changed
- Settings sections are reordered — General is now the first and default
section instead of AI Workspace.
- The Duplicate Finder group list is now virtualised — only on-screen cards
mount, so large result sets (e.g. 5,000+ pairs) scroll without lag rather
than mounting every thumbnail at once.
- The gallery grid is now row-virtualised, so very large libraries scroll
smoothly and only on-screen thumbnails are rendered.
- Polished the new theme surfaces before release, including readable
subtle-light secondary buttons, failed-worker action buttons, and onboarding
controls.
- Onboarding preview media keeps the dark gallery/media surface regardless of
the active chrome theme.
### Fixed
- **AVIF thumbnails** — AVIF files are now processed correctly by routing
thumbnail generation through the bundled FFmpeg path instead of the Rust
image decoder (which has no dav1d dependency). Previously-failed AVIF jobs
are requeued on startup; JPEG derivatives are fed to the embedding and
tagging pipeline while the lightbox continues to display the original file.
- Accent text is now readable in the Subtle Light theme.
- Folder picker chevron tooltip now correctly shows "No subfolders" for leaf
entries instead of "Open folder" in both branches.
- Folder picker Unix breadcrumb root now shows "/" instead of always "Home"
for non-home paths such as `/mnt/data`.
- Video embedding jobs are no longer claimed before their thumbnail exists, and
any that previously failed for that reason are requeued on startup — videos no
longer churn through failed embeddings.
- Subtle Light theme consistency — the lightbox metadata panel now follows the
light chrome while the image canvas stays dark (matching Conventional Dark),
and gallery/timeline media badges, duplicate-finder thumbnails, and the window
restore icon now theme correctly instead of staying Phokus-dark.
- Timeline scrolling is now smooth on large libraries — it virtualizes per row
of tiles instead of per month, so a month with thousands of photos no longer
mounts every tile at once (thumbnails now load in incrementally as you scroll,
matching the All Media grid).
- Background worker updates (thumbnails, metadata, embeddings, tags) no longer
re-sort the entire loaded image set on every batch. In Timeline, which loads
the whole library, this re-sort caused severe lag and could crash the app
during background indexing.
## [0.1.0] — 2026-06-14
First public release. Windows desktop, distributed as an unsigned NSIS
installer with a built-in updater.
### Added
- **Local media library** — add folders, recursive background indexing with
live progress, and a filesystem watcher that keeps the library in sync as
files are added, edited, moved, renamed, or removed (thumbnails and
embeddings are preserved across renames).
- **Gallery** — virtualized grid (handles very large libraries), favorites,
star ratings, video durations; filter by folder, type, favorites, or
rating; sort by date added, date taken (EXIF), name, size, rating, or
duration; compact / comfortable / detail density.
- **Search** — filename, semantic (`/s`, via CLIP visual embeddings), and tag
(`/t`) search from one prefix-aware search bar.
- **Discovery** — similar-image search (by image or selected region), an
Explore view with a visual cluster map and tag cloud, and a Timeline grouped
by EXIF capture date. Explore, Timeline, and Duplicates are folder-scopable
from their headers.
- **Lightbox** — keyboard navigation, zoom, pan, inline tag editing, and
rating controls, plus a custom edge-to-edge video player (scrubbing, volume,
speed, loop, fullscreen, keyboard support).
- **AI tagging** — WD tagger (ONNX, CPU/DirectML) with adjustable confidence
threshold, batch size, and per-folder queue targeting. Optional.
- **Duplicate finder** — three-phase exact-duplicate scan
(size → sample hash → full hash) with live progress and bulk delete.
- **Background pipeline** — strict-priority workers (thumbnails → metadata →
embeddings → tags) with per-folder pausing from the sidebar context menu or
the background-tasks bar.
- **Guided first-run onboarding** — background FFmpeg provisioning with live
progress and retry, a walkthrough of the library, pipeline, search modes,
views, and updates, plus an optional AI-tagger download. Re-runnable from
Settings.
- **Updater** — checks GitHub Releases on launch and from Settings; a title-bar
indicator lights up when a new version is ready, and one click downloads,
installs the signed update, and relaunches.
- **Maintenance** — database compaction and orphaned-thumbnail cleanup from
Settings, with live size/reclaimable stats.
- **Window state** persistence and single-instance handling.
[0.2.0]: https://github.com/JezzWTF/phokus/releases/tag/v0.2.0
[0.1.1]: https://github.com/JezzWTF/phokus/releases/tag/v0.1.1
[0.1.0]: https://github.com/JezzWTF/phokus/releases/tag/v0.1.0
+47 -8
View File
@@ -15,32 +15,67 @@ pnpm dev:app
# Frontend only (no Tauri window)
pnpm dev:vite
# Production build
pnpm build:app
# UI Lab — browser-only frontend with mocked Tauri backend (http://127.0.0.1:1422)
pnpm dev:ui
# Production build (CPU)
pnpm build:app:cpu
# Production build (CUDA / GPU-accelerated)
pnpm build:app:cuda
# Type-check frontend
pnpm build:vite
# Frontend unit tests (Vitest; only picks up src/**/*.test.ts)
pnpm test:unit
pnpm test:unit:watch
# Rust unit tests (in-memory SQLite; --no-default-features skips CUDA)
pnpm test:rust
# E2E tests (Playwright against the UI Lab; auto-starts the server)
pnpm test:e2e
pnpm exec playwright test tests/ui-lab.spec.ts # single file
pnpm exec playwright test -g "filename search" # single test by name
# Formatting (Prettier + prettier-plugin-tailwindcss; cargo fmt for Rust)
pnpm format:all
```
Use **pnpm** — never npm.
There are no test suites configured.
Three test layers, none of which exercise the real Tauri window:
- **Vitest unit tests** — co-located `*.test.ts` next to the pure logic they cover (`src/store/helpers.ts`, formatters, path utils); fixture factories in `src/test/factories.ts`.
- **Rust unit tests** — inline `#[cfg(test)]` modules; DB tests run against in-memory SQLite via the shared `db::test_support` fixture (`test_conn()` + `test_image()`), which registers sqlite-vec and applies both migrations. Reuse it for any new DB-touching tests.
- **Playwright e2e smoke tests** in `tests/` — run against the UI Lab (browser mocks).
## Architecture
### Frontend (`src/`)
- **`store.ts`** — single Zustand store (`useGalleryStore`) that owns all app state and all `invoke()` calls to the Tauri backend. Every feature (folders, images, search, similar images, tags, captions, tagger, duplicates) is implemented as store actions here. React components are thin consumers.
- **`src/store/`** — single Zustand store (`useGalleryStore`) that owns all app state and all `invoke()` calls to the Tauri backend, split into per-feature slices combined in `index.ts` (which exports `useGalleryStore` and the `GalleryStore` type). `types.ts` holds all interfaces/type unions (including `ImageRecord`); `helpers.ts` holds pure functions and cross-slice module state (search parsing, image sort/merge, request-token guards). Slices: `librarySlice` (folders), `gallerySlice` (image paging/filters/bulk actions), `searchSlice` (search + similar-images), `exploreSlice` (visual clusters, tag cloud, tags), `albumSlice`, `duplicateSlice`, `taggerSlice`, `captionSlice`, `settingsSlice`, `appSlice` (updates, onboarding, ffmpeg, worker pauses); `events.ts` wires the Tauri event listeners (`subscribeToProgress`). Components still call `useGalleryStore(s => s.field)` against one flat state object — the slice split is internal. React components are thin consumers.
- **`App.tsx`** — sets up Tauri event listeners (`subscribeToProgress`) and renders the top-level layout (sidebar + active view).
- **`src/components/`** — UI components: `Gallery`, `Lightbox`, `Sidebar`, `Toolbar`, `TagCloud`, `DuplicateFinder`, `BackgroundTasks`, `SettingsModal`, `MenuBar`, `TitleBar`.
- **`src/components/`** — UI components: `Gallery`, `Lightbox`, `Sidebar`, `Toolbar`, `Timeline`, `ExploreView`, `DuplicateFinder`, `BackgroundTasks`, `SettingsModal`, `TitleBar`.
- **`src/components/menu/`** — shared floating-UI primitives: `useDismissable`, `MenuPanel`/`MenuItem`/`SubMenu`, the portal-based `ContextMenu`, and the app-wide `Dropdown`. Build menus, dropdowns, and popovers on these instead of hand-rolling.
- State management: Zustand v5, no selectors library — components call `useGalleryStore(s => s.field)` directly.
- Styling: Tailwind CSS v4 (Vite plugin, no config file).
- Virtualized gallery grid: `@tanstack/react-virtual`.
- Animation: `framer-motion`.
### UI Lab (`src/dev/`)
`pnpm dev:ui` runs the real frontend (same `App.tsx`, store, components, CSS) in a plain browser with Tauri fully mocked — no Rust backend or Tauri window. `src/main.tsx` imports `src/dev/setupMockTauri.ts` before `App` in `ui` mode; `mockBackend.ts` implements an in-memory command backend, with fixtures in `mockFixtures.ts`/`mockScenarios.ts`. Pick a seeded state via `?scenario=` (`rich` default; also `empty`, `new-user`, `just-updated`, `busy`, `duplicates`, `album`, `errors`, `huge`) and a What's New entry via `?changelog=`. `Ctrl+Shift+D` opens the demo panel. Full guide: `docs/ui-lab.md`.
Rules that keep UI Lab working:
- Components that render thumbnails/covers/posters must use `mediaSrc(...)` from `src/lib/mediaSrc.ts`, never `convertFileSrc(...)` directly.
- New Tauri commands invoked from the frontend need a mock in `src/dev/mockBackend.ts` (unmocked commands log console errors, which fail the e2e tests).
UI Lab is for visual/layout work and agent browser inspection. Native behavior (file pickers, real thumbnails, window controls, updater) must still be validated in `pnpm dev:app`.
### Search modes
The search bar supports prefix syntax parsed by `parseSearchValue` in `store.ts`:
The search bar supports prefix syntax parsed by `parseSearchValue` in `src/store/helpers.ts`:
- No prefix / `f:` — filename search (paginated, DB-backed)
- `/s <query>` or `s: <query>` — semantic (embedding) search
- `/t <tag>` or `t: <tag>` — tag search
@@ -64,6 +99,8 @@ Key modules:
| `hnsw_index.rs` | In-memory HNSW index wrapper (hnsw_rs) |
| `tagger.rs` | WD tagger: ONNX model download, inference, CSV tag loading |
| `captioner.rs` | AI captioning (ONNX, disabled in workers but code intact) |
| `download.rs` | Resilient file downloads via the system `curl` (resume, stall detection) |
| `onnx_runtime.rs` | Shared ONNX Runtime DLL provisioning + `ort` init (used by tagger and captioner; DLLs live in the caption model dir for legacy reasons) |
| `thumbnail.rs` | Thumbnail generation (image crate + fast_image_resize, FFmpeg for video) |
| `media.rs` | FFmpeg sidecar provisioning and probing |
| `storage.rs` | `StorageProfile` for tuning worker counts |
@@ -80,11 +117,11 @@ Database: SQLite with WAL mode, stored in the Tauri app data directory as `galle
| `media-updated` | `ThumbnailBatch` |
| `caption-model-progress` | `CaptionModelProgress` |
| `tagger-model-progress` | `TaggerModelProgress` |
| `duplicate_scan_progress` | `[scanned, total]` |
| `duplicate_scan_progress` | `{ phase, processed, total, skipped }` |
### Key types
`ImageRecord` (mirrored in `store.ts` and `db.rs`) is the central data type. It carries embedding status, tagging status, caption data, and media metadata. The frontend type must stay in sync with the Rust struct serialization.
`ImageRecord` (mirrored in `src/store/types.ts` and `db.rs`) is the central data type. It carries embedding status, tagging status, caption data, and media metadata. The frontend type must stay in sync with the Rust struct serialization.
## Development notes
@@ -92,3 +129,5 @@ Database: SQLite with WAL mode, stored in the Tauri app data directory as `galle
- ML inference crates (`candle-*`, `ort`, `image`, `rayon`, `tokenizers`, `xxhash-rust`, `rusqlite`) use `opt-level = 3` in dev profile to keep inference performance acceptable.
- The caption worker is intentionally disabled (`lib.rs:73`) — the backend code is intact for future re-enabling.
- **Never use `any` type** in TypeScript — look up correct types.
- `website/` is a separate Vite project for the marketing site (phokus.jezz.wtf): `pnpm dev:web` / `pnpm build:web`. It is not part of the app build.
- `pnpm changelog:add -- --type fixed --message "..."` appends an entry to the `[Unreleased]` section of `CHANGELOG.md`, which also feeds the in-app What's New modal (types: added, changed, deprecated, removed, fixed, security).
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 JezzWTF
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+92 -15
View File
@@ -4,31 +4,95 @@ A local-first desktop media library for browsing, filtering, and curating image
## Features
### Library
- Add and remove media folders; background indexing with live progress
- **Live file tracking** — a filesystem watcher keeps the library in sync as files are added, changed, or removed; renames and moves are handled in place, preserving thumbnails and embeddings
- Browse all media or filter by folder, type (image/video), favorites, or star rating
- **Filename search**, **semantic search** (`/s query`), and **tag search** (`/t tag`)
- **Similar image search** — find visually similar media by image or selected region
- **AI tagging** via WD tagger (ONNX, CPU/DirectML) with confidence threshold control
- **Explore view** — visual cluster map and tag cloud for browsing by theme
- **Duplicate finder** — scan for exact duplicates by file hash with bulk delete
- Lightbox preview with keyboard navigation, inline tag editing, and rating controls
- Sort by date, name, size, rating, or duration
- Sort by date added, date taken (EXIF), name, size, rating, or duration
- Grid density controls (compact / comfortable / detail)
- Desktop notifications, batched per folder, with per-folder mute and a global pause
### Search & discovery
- **Filename search**, **semantic search** (`/s query`), and **tag search** (`/t tag`)
- **Similar image search** — find visually similar media by image or a selected region
- **Explore view** — visual cluster map and tag cloud for browsing by theme
- **Timeline view** — media grouped chronologically by capture date (EXIF)
- **Duplicate finder** — three-phase exact-duplicate scan (size → sample hash → full hash) with live progress and bulk delete
- Explore, Timeline, and Duplicates are folder-scopable directly from their headers — no need to bounce through the sidebar
### Viewing & curation
- Lightbox with keyboard navigation, zoom, inline tag editing, and rating controls
- **Custom video player** — immersive edge-to-edge playback with scrubbing, volume, playback speed, loop, and fullscreen; auto-hiding controls and full keyboard support
- **AI tagging** via WD tagger (ONNX, CPU/DirectML) with confidence threshold, batch size, and per-folder queue targeting
### Maintenance
- Database compaction and orphaned-thumbnail cleanup from Settings, with live size/reclaimable stats
- Per-folder pausing of background work (thumbnails, metadata, embeddings, tagging)
## Supported formats
| Images | Videos |
|--------|--------|
| jpg, jpeg, png, gif, bmp | mp4, mov, m4v |
| tiff, tif, webp, avif, heic, heif | webm |
| tiff, tif, webp, avif | webm |
## Installation
Phokus is a **Windows desktop app**. Download the latest installer from the
[Releases page](https://github.com/JezzWTF/phokus/releases/latest) and run it.
**Requirements:** Windows 10 or 11. The installer will fetch the WebView2
runtime automatically if it isn't already present (it ships with Windows 11).
### A note on the unsigned build
Phokus 0.1.0 installers are **not code-signed**. Code-signing certificates are
an ongoing expense that's hard to justify for an unfunded open-source project,
so signing is on the roadmap rather than in place today.
In practice this means Windows SmartScreen will show a blue
**"Windows protected your PC"** warning the first time you run the installer.
To proceed: click **More info → Run anyway**. If a release publishes a SHA-256
checksum, you can verify the download against it first.
### First run
On first launch Phokus downloads a few tools and models — all one-time, and all
processed on your machine:
- **FFmpeg** (~tens of MB) for video thumbnails and metadata — downloaded in the
background; the guided first-run tour shows progress.
- **WD tagger model** (~1.3 GB) — **optional**, only if you enable AI tagging.
- **CLIP embedding model** (~580 MB) — downloaded automatically the first time
visual embeddings run (powers semantic search and similar images).
## Privacy & local-first
Phokus is local-first by design. **Your media and everything derived from it —
thumbnails, embeddings, tags, ratings — never leave your machine.** There is no
account, no telemetry, and no cloud sync.
The only network activity is:
- **One-time tool/model downloads** on first use (FFmpeg, the CLIP and WD
models, and the ONNX runtime), fetched from their official sources
(FFmpeg builds, Hugging Face, NuGet). These pull *tools to your machine*
none of your images are uploaded.
- **Update checks** against the GitHub Releases page, so the app can offer new
versions. This can be ignored if you never update.
Everything Phokus stores lives in its app-data directory (`gallery.db`,
`thumbnails/`, `models/`, `settings/`). Removing that directory resets the app
completely; your original media folders are never modified.
## Stack
- Tauri 2 + Rust backend
- React 19 + TypeScript + Zustand
- SQLite + `sqlite-vec` (vector search)
- SQLite + `sqlite-vec` (vector search) + HNSW index
- ONNX Runtime (`ort`) for AI tagging
- Candle (Rust ML) for visual embeddings
- Candle (Rust ML) for CLIP visual embeddings
- mozjpeg for fast scaled JPEG decoding
- FFmpeg sidecar for video thumbnails and metadata
- Vite + Tailwind CSS v4
@@ -45,14 +109,27 @@ pnpm dev:app
# Frontend only
pnpm dev:vite
# Production build
pnpm build:app
# Browser-only UI Lab with mocked Tauri APIs
pnpm dev:ui
# Production build (CPU)
pnpm build:app:cpu
# Production build (CUDA / GPU-accelerated)
pnpm build:app:cuda
# Type-check the frontend
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.
2. Supported files are written to SQLite with metadata (path, dimensions, media type, etc.).
3. Background workers generate thumbnails, compute visual embeddings, and run AI tagging.
2. Supported files are written to SQLite with metadata (path, dimensions, media type, EXIF capture date, etc.).
3. Background workers process the queue as a strict priority pipeline — thumbnails first, then video metadata, then visual embeddings, then AI tags — so each stage runs at full speed instead of competing for CPU, disk, and the database.
4. Progress events stream back to the UI while the gallery updates incrementally.
5. Embeddings power semantic search and the similar images feature via an HNSW index.
5. A filesystem watcher picks up later changes (new files, edits, deletions, renames) and keeps the index current without rescans.
6. Embeddings power semantic search and the similar-images feature via an HNSW index.
+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Phokus">
<title>Phokus</title>
<!--
Phokus app mark — a camera iris.
The hexagon in the middle is the lens OPENING; each blade edge sweeps from a
corner of the opening out to the rim, all leaning the same way (the pinwheel).
Tuning knobs (edit and re-open in any browser/Figma):
* opening roundness -> the "52" radius in the opening <path> arcs.
smaller (toward 30) = rounder/softer hole; larger (toward 90) = flatter, sharper.
* blade curve -> the "110" radius in each blade <path>.
smaller = more pronounced curve; larger = straighter blade.
* blade sweep amount -> the +40deg baked into the blade endpoint (70.71,-84.27).
* curve DIRECTION -> the final flag in each "A r r 0 0 X" command (0<->1 flips the bow).
* weight -> stroke-width on the <g>.
* color -> stroke on the <g>; swap "#e9e9ec" for currentColor to inherit.
A brand-accent focal dot is provided at the bottom (commented out).
-->
<rect x="8" y="8" width="240" height="240" rx="52" fill="#0e0f14"/>
<g transform="translate(128 128)" fill="none" stroke="#e9e9ec" stroke-width="9"
stroke-linecap="round" stroke-linejoin="round">
<!-- outer rim -->
<circle r="110"/>
<!-- lens opening: soft, arc-rounded hexagon -->
<path d="M0,-46 A52,52 0 0 0 39.84,-23 A52,52 0 0 0 39.84,23 A52,52 0 0 0 0,46 A52,52 0 0 0 -39.84,23 A52,52 0 0 0 -39.84,-23 A52,52 0 0 0 0,-46 Z"/>
<!-- blades: one curved edge, swept six times -->
<g transform="rotate(0)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(60)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(120)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(180)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(240)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(300)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
</g>
<!-- optional brand focal point -->
<!-- <circle cx="128" cy="128" r="9" fill="#e6a23c"/> -->
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+59
View File
@@ -0,0 +1,59 @@
# Phokus v0.1.0
> Draft for the GitHub Release body. Paste into the release, fill in the
> checksum, and trim as needed.
**Phokus is a local-first desktop media library for Windows** — point it at
your image and video folders and it builds a fast, searchable gallery with
thumbnails, semantic search, visual discovery, AI tagging, and duplicate
cleanup. Everything is processed on your machine; nothing is uploaded.
This is the **first public release**. Expect rough edges, and please file
issues.
## Install
1. Download `Phokus_0.1.0_x64-setup.exe` below.
2. Run it. **Windows SmartScreen will warn** that the publisher is
unrecognized — this build is **not code-signed** (cost; signing is on the
roadmap). Click **More info → Run anyway** to proceed.
3. Requires **Windows 10/11**. The installer fetches the WebView2 runtime
automatically if needed.
On first launch, Phokus runs a short guided tour and downloads FFmpeg in the
background (one-time, ~tens of MB). AI tagging (~1.3 GB) is optional; the CLIP
model for semantic search (~580 MB) downloads automatically the first time
embeddings run. **All of this stays on your machine.**
## Highlights
- Virtualized gallery for very large libraries; favorites, ratings, filters, sorts
- Filename, semantic (`/s`), and tag (`/t`) search
- Similar-image search, Explore clusters + tag cloud, EXIF Timeline
- Lightbox with zoom/pan and a custom video player
- Optional on-device AI tagging (WD tagger)
- Exact-duplicate finder with bulk delete
- Built-in updater
See the [changelog](https://github.com/JezzWTF/phokus/blob/main/CHANGELOG.md)
for the full list.
## Privacy
Your media and all derived data (thumbnails, embeddings, tags, ratings) never
leave your machine. No account, no telemetry, no cloud. The only network
activity is the one-time tool/model downloads above and update checks against
this Releases page.
## Known limitations
- **Unsigned installer** — SmartScreen warning as described above.
- **Windows only** for now.
- CPU/DirectML inference in the shipped build; very large libraries take time
to embed on first index.
## Verify your download (optional)
```
SHA-256 (Phokus_0.1.0_x64-setup.exe) = <fill in at publish time>
```
+64
View File
@@ -0,0 +1,64 @@
# Phokus v0.1.1
> Draft for the GitHub Release body. Fill in the checksums and trim as needed.
**Phokus is a local-first desktop media library for Windows** — point it at
your image and video folders and it builds a fast, searchable gallery with
thumbnails, semantic search, visual discovery, AI tagging, and duplicate
cleanup. Everything is processed on your machine; nothing is uploaded.
This is a quality-of-life release on top of 0.1.0: a new in-app folder
picker, themes, smoother scrolling on large libraries, and a batch of fixes.
If you're updating from 0.1.0, the built-in updater will fetch this for you.
## Install / update
- **Updating from 0.1.0:** the in-app updater will offer 0.1.1 on launch — one
click downloads, installs, and relaunches.
- **Fresh install:** download `Phokus_0.1.1_x64-setup.exe` below and run it.
**Windows SmartScreen will warn** that the publisher is unrecognized — this
build is **not code-signed**. Click **More info → Run anyway**.
- Requires **Windows 10/11**. WebView2 is fetched automatically if missing.
NVIDIA users wanting GPU embedding speed can use the
`Phokus_0.1.1_x64-cuda-setup.exe` variant instead (larger download; bundles the
CUDA runtime DLLs).
## Highlights
### Added
- **Custom multi-folder picker** — navigate and stage folders from multiple
locations and add them in one go, replacing the native OS dialog.
- **Themes** — Phokus, Subtle Light, and Conventional Dark chrome, with an
inline theme picker in first-run onboarding.
- **Timeline scrubber** — a year/month rail to jump anywhere in the library.
- **Folder reordering** in the sidebar (drag-and-drop or keyboard), persisted,
plus AZ / ZA / Custom sort for the Libraries list.
- **Video playback settings** — autoplay-in-lightbox and start-muted toggles.
- **Rebuild semantic index** maintenance action — fixes "dimension mismatch"
search errors after switching CLIP models.
- Locate and filter images with failed AI-tagging jobs.
### Changed
- Gallery grid and Duplicate Finder list are now row-virtualised — large
libraries and large result sets (5,000+ pairs) scroll without lag.
- Settings now open on General by default.
### Fixed
- **AVIF thumbnails** now generate correctly (routed through bundled FFmpeg);
previously-failed AVIF jobs are requeued on startup.
- Video embedding jobs no longer churn through failed states before their
thumbnail exists; previously-failed ones are requeued.
- Timeline scrolling is smooth on large libraries (per-row virtualisation);
background batches no longer re-sort the whole loaded set.
- Numerous Subtle Light theme readability/parity fixes.
See the [changelog](https://github.com/JezzWTF/phokus/blob/main/CHANGELOG.md)
for the full list.
## Checksums
```
SHA-256 (Phokus_0.1.1_x64-setup.exe) = 1c19cbeb77f38a44149380c42c76b633add65777d317e1f3ff7e45d96d12d287
SHA-256 (Phokus_0.1.1_x64-cuda-setup.exe) = a7337ef5ee0478a785b48acc8012e8fc5c957341161d6103409213ad78eb845f
```
+85
View File
@@ -0,0 +1,85 @@
# Phokus v0.2.0
> Draft for the GitHub Release body. Fill in the checksums and trim as needed.
**Phokus is a local-first desktop media library for Windows** — point it at
your image and video folders and it builds a fast, searchable gallery with
thumbnails, semantic search, visual discovery, AI tagging, and duplicate
cleanup. Everything is processed on your machine; nothing is uploaded.
0.2.0 is the biggest update since launch: albums, gallery multi-select with
bulk actions, colour search, a second AI tagging model (JoyTag), a full tag
manager, camera EXIF details in the lightbox, a slideshow mode, and a large
batch of performance and theme fixes. Updating from 0.1.x? The built-in
updater will fetch this for you — and afterwards, a new in-app "What's New"
tour shows you around.
## Install / update
- **Updating from 0.1.x:** the in-app updater will offer 0.2.0 on launch — one
click downloads, installs, and relaunches.
- **Fresh install:** download `Phokus_0.2.0_x64-setup.exe` below and run it.
**Windows SmartScreen will warn** that the publisher is unrecognized — this
build is **not code-signed**. Click **More info → Run anyway**.
- Requires **Windows 10/11**. WebView2 is fetched automatically if missing.
NVIDIA users wanting GPU embedding/tagging speed can use the
`Phokus_0.2.0_x64-cuda-setup.exe` variant instead (larger download; bundles the
CUDA runtime DLLs). Settings → Updates now shows which build you are running.
## Highlights
### Added
- **Albums** — cross-folder collections without moving files: their own
sidebar section with cover thumbnails, create/rename/reorder/manage, and
album-aware similar-image search.
- **Gallery multi-select** — hover a thumbnail's corner to start selecting,
then bulk tag, rate, favorite, add to an album, or delete from a floating
action bar. Works in similar-image, region, and album views too.
- **Colour search** — filter the Gallery, Timeline, or tag results by dominant
colour via toolbar swatches or a custom picker.
- **Choose your tagging model** — pick between the anime-focused WD tagger and
**JoyTag** (better for photo libraries), each with its own confidence
threshold, plus a **Reset AI tags** action that never touches manual tags.
- **Tag manager** — rename, merge, and delete tags across the library, with
live search and sorting; Explore's Tag Cloud also shows **related tags** on
hover.
- **Camera info in the lightbox** — EXIF camera, lens, aperture, shutter
speed, ISO, focal length, and a map link for geotagged photos.
- **Slideshow mode**, **What's New tour after updates**, **quick theme switch**
from the title bar, an editable path bar in the folder picker, persistent
per-folder worker pauses, and add-to-album from the right-click menu.
### Changed
- **Settings reorganised** into General, Media, Updates & Setup, Storage, and
AI Workspace pages.
- **Menus unified** — all context menus and dropdowns share one style, stay on
screen, close on Escape, and support submenus.
- **Faster Explore** — quicker cluster revisits, much faster first-time
clustering on large libraries, and a calmer Tag Cloud during active tagging.
- **Faster CPU tagging** (multi-core with headroom) and smoother GPU tagging.
- **Safer deletion** — deleting media now asks for confirmation and says
clearly that files are removed from disk.
- Better narrow-window layouts, a real update download progress bar, and a
two-column lightbox info panel.
### Fixed
- **No more self-indexing loops** — Phokus now skips its own thumbnail cache
when you add a broad folder like your user profile.
- **Ratings keep your search order** — rating an image mid-search no longer
reshuffles similar-image, region, semantic, tag, or album results.
- **AI tagging stays responsive** — big GPU tagging jobs no longer freeze the
UI, and noisy low-signal tags are filtered (older ones cleaned up on
startup).
- First launch now fits smaller screens, Explore no longer flashes the
previous folder, and a long list of Subtle Light readability fixes.
See the [changelog](https://github.com/JezzWTF/phokus/blob/main/CHANGELOG.md)
for the full list.
## Checksums
```
SHA-256 (Phokus_0.2.0_x64-setup.exe) = 9d52b485ef0d614620b0df4ba235fbdf285e0792e4a38841777e0f0a05f38770
SHA-256 (Phokus_0.2.0_x64-cuda-setup.exe) = b3a24ea5fb9f00f64fe5b4040a69347b91341c75eb83fd7bb2409cc74c5eb052
```
+201
View File
@@ -0,0 +1,201 @@
# 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
```
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` | Empty library with no folders or media (onboarding already completed) |
| `/?scenario=new-user` | True first run: onboarding tour open, empty library, no tagger model downloaded |
| `/?scenario=just-updated` | Rich library that has just been updated — the "What's new" toast fires on launch |
| `/?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
```
## Changelog Previews
The What's New modal adapts its layout to release size (compact single column
for small releases, a two-pane section rail for large ones). UI Lab reads
`?changelog=` to override which entry the modal shows:
| URL | Purpose |
| --- | --- |
| `/?changelog=unreleased` | The in-progress `[Unreleased]` notes — large release, rail layout |
| `/?changelog=small` | Synthetic hotfix-sized entry — compact single-column layout |
| `/?changelog=0.1.1` | Any specific released version |
Open the modal via the demo panel: `Ctrl+Shift+D` → Open "What's new" modal.
Combine with the scenario above to walk the whole post-update greeting for the
next release: `/?scenario=just-updated&changelog=unreleased`.
## 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 generated from existing onboarding images and
website screenshots into:
```text
public/dev-media/fixture-*.webp
```
The pack is deliberately small and deterministic, but varied enough for browser
screenshots: square, portrait, landscape, monochrome, tinted, framed, and
interface-like crops. 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,
root-relative asset URLs such as `/dev-media/fixture-01.webp` are returned
as-is. Paths beginning with `mock://` are also 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
```
+28 -4
View File
@@ -1,15 +1,32 @@
{
"name": "phokus",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"type": "module",
"scripts": {
"build:app": "tauri build",
"build:app:cpu": "tauri build -- --no-default-features",
"build:app:cuda": "tauri build --config src-tauri/tauri.cuda.conf.json",
"build:vite": "tsc && vite build",
"build:web": "cd website && tsc && vite build",
"changelog:add": "node tools/changelog-add.mjs",
"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",
"dev:vite": "vite",
"dev:web": "cd website && pnpm dev",
"format": "prettier --write .",
"format:all": "pnpm format && pnpm format:rust",
"format:check": "prettier --check .",
"format:rust": "cd src-tauri && cargo fmt",
"format:rust:check": "cd src-tauri && cargo fmt --check",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"test:e2e": "playwright test",
"test:rust": "cd src-tauri && cargo test --no-default-features",
"test:unit": "vitest run",
"test:unit:watch": "vitest"
},
"dependencies": {
"@tanstack/react-virtual": "^3.13.23",
@@ -18,6 +35,8 @@
"@tauri-apps/plugin-fs": "^2.5.0",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"d3-force": "^3.0.0",
"framer-motion": "^12.38.0",
"react": "^19.1.0",
@@ -25,14 +44,19 @@
"zustand": "^5.0.12"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/vite": "^4.2.2",
"@tauri-apps/cli": "^2",
"@types/d3-force": "^3.0.10",
"@types/node": "^26.0.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"prettier": "^3.9.4",
"prettier-plugin-tailwindcss": "^0.8.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.8.3",
"vite": "^7.0.4"
"vite": "^7.0.4",
"vitest": "^4.1.9"
}
}
+75
View File
@@ -0,0 +1,75 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: 'http://127.0.0.1:1422',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run the Phokus UI Lab (browser-only dev mode, see docs/ui-lab.md) before starting the tests */
webServer: {
command: 'pnpm exec vite --mode ui --host 127.0.0.1 --port 1422 --strictPort',
url: 'http://127.0.0.1:1422',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
+791 -8
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
packages:
- website
allowBuilds:
esbuild: true
sharp: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Phokus">
<title>Phokus</title>
<!--
Phokus app mark — a camera iris.
The hexagon in the middle is the lens OPENING; each blade edge sweeps from a
corner of the opening out to the rim, all leaning the same way (the pinwheel).
Tuning knobs (edit and re-open in any browser/Figma):
* opening roundness -> the "52" radius in the opening <path> arcs.
smaller (toward 30) = rounder/softer hole; larger (toward 90) = flatter, sharper.
* blade curve -> the "110" radius in each blade <path>.
smaller = more pronounced curve; larger = straighter blade.
* blade sweep amount -> the +40deg baked into the blade endpoint (70.71,-84.27).
* curve DIRECTION -> the final flag in each "A r r 0 0 X" command (0<->1 flips the bow).
* weight -> stroke-width on the <g>.
* color -> stroke on the <g>; swap "#e9e9ec" for currentColor to inherit.
A brand-accent focal dot is provided at the bottom (commented out).
-->
<rect x="8" y="8" width="240" height="240" rx="52" fill="#0e0f14"/>
<g transform="translate(128 128)" fill="none" stroke="#e9e9ec" stroke-width="9"
stroke-linecap="round" stroke-linejoin="round">
<!-- outer rim -->
<circle r="110"/>
<!-- lens opening: soft, arc-rounded hexagon -->
<path d="M0,-46 A52,52 0 0 0 39.84,-23 A52,52 0 0 0 39.84,23 A52,52 0 0 0 0,46 A52,52 0 0 0 -39.84,23 A52,52 0 0 0 -39.84,-23 A52,52 0 0 0 0,-46 Z"/>
<!-- blades: one curved edge, swept six times -->
<g transform="rotate(0)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(60)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(120)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(180)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(240)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
<g transform="rotate(300)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
</g>
<!-- optional brand focal point -->
<!-- <circle cx="128" cy="128" r="9" fill="#e6a23c"/> -->
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Codex Cloud setup for Phokus.
# Paste this script into the Codex Cloud environment setup field, or run it from
# the repo root with: bash scripts/codex-cloud-setup.sh
#
# Goals:
# - install Linux packages needed by Tauri/WebKit and native Rust crates
# - install JS dependencies with pnpm using the lockfile
# - pre-fetch Rust dependencies while setup still has internet access
# - keep the environment CPU-safe by avoiding Phokus' default CUDA feature set
# - leave Codex with clear verification commands for UI and Rust work
log() {
printf '\n\033[1;36m[phokus-codex]\033[0m %s\n' "$*"
}
warn() {
printf '\n\033[1;33m[phokus-codex warning]\033[0m %s\n' "$*" >&2
}
repo_root="$(pwd)"
if [[ ! -f "package.json" || ! -d "src-tauri" ]]; then
warn "This script should be run from the Phokus repository root. Current directory: ${repo_root}"
exit 1
fi
export CI=1
export PNPM_HOME="${PNPM_HOME:-$HOME/.local/share/pnpm}"
export PATH="$PNPM_HOME:$HOME/.cargo/bin:$PATH"
# Persist useful shell defaults for the later Codex agent phase. Codex setup runs
# in a separate Bash session, so exports here alone would not survive.
if ! grep -q "# Phokus Codex Cloud" "$HOME/.bashrc" 2>/dev/null; then
cat >> "$HOME/.bashrc" <<'BASHRC'
# Phokus Codex Cloud
export PNPM_HOME="${PNPM_HOME:-$HOME/.local/share/pnpm}"
export PATH="$PNPM_HOME:$HOME/.cargo/bin:$PATH"
export CI=1
BASHRC
fi
install_apt_packages() {
if ! command -v apt-get >/dev/null 2>&1; then
warn "apt-get not found; skipping system package installation."
return 0
fi
log "Installing Linux system dependencies for Tauri, WebKit, SQLite/native crates, and browser tooling"
sudo apt-get update
# Tauri v2 Linux builds need the WebKitGTK/AppIndicator/Rsvg stack. Some base
# images expose either the 4.1 or 4.0 WebKit development package, so try the
# modern package first and gracefully fall back.
local common_packages=(
build-essential
curl
wget
file
pkg-config
libssl-dev
libgtk-3-dev
libayatana-appindicator3-dev
librsvg2-dev
patchelf
ca-certificates
)
if apt-cache show libwebkit2gtk-4.1-dev >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends "${common_packages[@]}" libwebkit2gtk-4.1-dev
else
sudo apt-get install -y --no-install-recommends "${common_packages[@]}" libwebkit2gtk-4.0-dev
fi
}
ensure_node_and_pnpm() {
log "Preparing Node/pnpm"
if ! command -v node >/dev/null 2>&1; then
warn "Node.js is not available in this image. Pin Node.js 20+ in the Codex environment settings or use a Codex universal image with Node installed."
exit 1
fi
node_major="$(node -p "process.versions.node.split('.')[0]")"
if [[ "$node_major" -lt 20 ]]; then
warn "Phokus expects Node.js 20+. Current version: $(node --version). Pin Node.js 20+ in Codex environment settings."
exit 1
fi
mkdir -p "$PNPM_HOME"
if command -v corepack >/dev/null 2>&1; then
corepack enable
corepack prepare pnpm@latest --activate
fi
if ! command -v pnpm >/dev/null 2>&1; then
npm install -g pnpm
fi
log "Node: $(node --version)"
log "pnpm: $(pnpm --version)"
}
ensure_rust() {
log "Preparing Rust toolchain"
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
# shellcheck source=/dev/null
source "$HOME/.cargo/env"
fi
rustup toolchain install stable --profile minimal
rustup default stable
rustup component add rustfmt clippy
log "rustc: $(rustc --version)"
log "cargo: $(cargo --version)"
}
install_js_dependencies() {
log "Installing frontend dependencies"
pnpm install --frozen-lockfile
}
prefetch_rust_dependencies() {
log "Pre-fetching Rust dependencies for CPU-safe Tauri checks"
# Phokus enables candle-cuda by default in Cargo.toml. Codex Cloud usually runs
# in a CPU Linux container, so use --no-default-features for checks/builds
# unless you intentionally configure a CUDA-capable environment.
cargo fetch --manifest-path src-tauri/Cargo.toml --locked
# This is intentionally a check, not a full release build. It warms the Cargo
# cache and catches missing native packages without making setup painfully slow.
cargo check --manifest-path src-tauri/Cargo.toml --locked --no-default-features
}
install_playwright_browsers() {
log "Installing Playwright Chromium dependencies for UI Lab/browser screenshots"
# Safe even if no Playwright tests are present yet. Useful for Codex browser
# inspection against `pnpm dev:ui`.
pnpm exec playwright install --with-deps chromium || warn "Playwright browser install failed; UI work may still run, but browser automation/screenshots may need manual setup."
}
print_next_steps() {
cat <<'EOF'
[phokus-codex] Setup complete.
Recommended Codex verification commands:
pnpm exec tsc --noEmit
pnpm build:vite
cargo check --manifest-path src-tauri/Cargo.toml --locked --no-default-features
For visual UI work in Codex/browser environments:
pnpm dev:ui
open http://127.0.0.1:1422/?scenario=rich
Other useful UI Lab scenarios:
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
Avoid the below in standard CPU-only Codex Cloud unless you have configured CUDA:
pnpm dev:app
pnpm build:app:cuda
cargo check --manifest-path src-tauri/Cargo.toml
EOF
}
main() {
install_apt_packages
ensure_node_and_pnpm
ensure_rust
install_js_dependencies
prefetch_rust_dependencies
install_playwright_browsers
print_next_steps
}
main "$@"
+518 -19
View File
@@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -52,6 +63,23 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_log-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
dependencies = [
"android_log-sys",
"env_filter 0.1.4",
"log",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -143,6 +171,12 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -372,6 +406,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -403,6 +449,30 @@ dependencies = [
"piper",
]
[[package]]
name = "borsh"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
dependencies = [
"once_cell",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -430,6 +500,40 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byte-unit"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
dependencies = [
"rust_decimal",
"schemars 1.2.1",
"serde",
"utf8-width",
]
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
@@ -635,6 +739,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -1450,6 +1556,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "env_filter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_filter"
version = "1.0.1"
@@ -1474,7 +1590,7 @@ checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"env_filter 1.0.1",
"jiff",
"log",
]
@@ -1608,6 +1724,15 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fern"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
[[package]]
name = "ffmpeg-sidecar"
version = "2.5.0"
@@ -1759,6 +1884,12 @@ dependencies = [
"libc",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futf"
version = "0.1.5"
@@ -2454,6 +2585,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
@@ -2461,7 +2595,7 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"ahash 0.8.12",
]
[[package]]
@@ -3108,6 +3242,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.94"
@@ -3327,6 +3471,9 @@ name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
dependencies = [
"value-bag",
]
[[package]]
name = "lzma-rust2"
@@ -3477,6 +3624,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -3559,6 +3712,31 @@ dependencies = [
"pxfm",
]
[[package]]
name = "mozjpeg"
version = "0.10.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9"
dependencies = [
"arrayvec",
"bytemuck",
"libc",
"mozjpeg-sys",
"rgb",
]
[[package]]
name = "mozjpeg-sys"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1"
dependencies = [
"cc",
"dunce",
"libc",
"nasm-rs",
]
[[package]]
name = "muda"
version = "0.17.2"
@@ -3586,6 +3764,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af"
[[package]]
name = "nasm-rs"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149"
dependencies = [
"jobserver",
"log",
]
[[package]]
name = "native-tls"
version = "0.2.18"
@@ -3837,6 +4025,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -3933,6 +4130,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -4102,6 +4311,20 @@ dependencies = [
"ureq",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -4372,7 +4595,7 @@ dependencies = [
[[package]]
name = "phokus"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"candle-core",
@@ -4388,6 +4611,7 @@ dependencies = [
"kamadak-exif",
"log",
"memmap2",
"mozjpeg",
"notify",
"ort",
"r2d2",
@@ -4402,12 +4626,16 @@ dependencies = [
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-log",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tokenizers",
"tokio",
"ureq",
"uuid",
"walkdir",
"xxhash-rust",
"zip 4.6.1",
@@ -4618,6 +4846,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pulp"
version = "0.21.5"
@@ -4728,6 +4976,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
@@ -5001,6 +5255,15 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.12.28"
@@ -5058,15 +5321,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -5102,6 +5370,15 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -5116,6 +5393,35 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.7.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta",
"rend",
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
@@ -5130,6 +5436,23 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust_decimal"
version = "1.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a"
dependencies = [
"arrayvec",
"borsh",
"bytes",
"num-traits",
"rand 0.8.5",
"rkyv",
"serde",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -5173,6 +5496,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
@@ -5182,6 +5517,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.10"
@@ -5310,6 +5672,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "3.7.0"
@@ -5607,6 +5975,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -5958,6 +6332,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.45"
@@ -6149,6 +6529,28 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-log"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93"
dependencies = [
"android_logger",
"byte-unit",
"fern",
"log",
"objc2",
"objc2-foundation",
"serde",
"serde_json",
"serde_repr",
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"time",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.3.3"
@@ -6190,6 +6592,79 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.18",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
dependencies = [
"bitflags 2.11.0",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]]
name = "tauri-runtime"
version = "2.10.1"
@@ -6398,7 +6873,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
@@ -6431,13 +6908,28 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokenizers"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223"
dependencies = [
"ahash",
"ahash 0.8.12",
"aho-corasick",
"compact_str",
"dary_heap",
@@ -6474,25 +6966,11 @@ dependencies = [
"bytes",
"libc",
"mio 1.2.0",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
@@ -6957,6 +7435,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
[[package]]
name = "utf8-zero"
version = "0.8.1"
@@ -6988,6 +7472,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -8096,6 +8586,15 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x11"
version = "2.21.0"
+22 -10
View File
@@ -1,8 +1,9 @@
[package]
name = "phokus"
version = "0.1.0"
description = "A performant image gallery application"
version = "0.2.0"
description = "Local-first desktop media library"
authors = ["JezzWTF"]
license = "MIT"
edition = "2021"
[lib]
@@ -13,7 +14,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[features]
default = []
# CUDA is on by default for the main dev machine; build with
# `--no-default-features` (pnpm dev:app:cpu) on machines without the toolkit.
default = ["candle-cuda"]
candle-cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"]
[dependencies]
@@ -32,18 +35,16 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
fast_image_resize = { version = "6.0.0", features = ["image"] }
walkdir = "2"
rayon = "1"
tokio = { version = "1", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
tokio = { version = "1", features = ["rt-multi-thread"] }
chrono = "0.4"
anyhow = "1"
log = "0.4"
ffmpeg-sidecar = "2.5.0"
xxhash-rust = { version = "0.8", features = ["xxh3"] }
memmap2 = "0.9"
sysinfo = "0.38.4"
candle-core = { version = "0.10.2", features = ["cuda"] }
candle-nn = { version = "0.10.2", features = ["cuda"] }
candle-transformers = { version = "0.10.2", features = ["cuda"] }
candle-core = "0.10.2"
candle-nn = "0.10.2"
candle-transformers = "0.10.2"
hf-hub = { version = "0.5.0", default-features = false, features = ["ureq", "native-tls"] }
tokenizers = "0.22.1"
ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "download-binaries", "copy-dylibs", "load-dynamic", "directml", "api-24", "tls-native"] }
@@ -53,6 +54,13 @@ csv = "1"
kamadak-exif = "0.5"
notify = "6"
tauri-plugin-notification = "2"
mozjpeg = "0.10.13"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-log = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-window-state = "2"
log = "0.4"
# ── Dev-mode performance ────────────────────────────────────────────────────
# opt-level=1 on the main crate keeps incremental compile short.
@@ -81,6 +89,10 @@ opt-level = 3
opt-level = 3
[profile.dev.package.fast_image_resize]
opt-level = 3
[profile.dev.package.mozjpeg]
opt-level = 3
[profile.dev.package.mozjpeg-sys]
opt-level = 3
# Parallel work scheduler
[profile.dev.package.rayon]
+6 -2
View File
@@ -15,13 +15,17 @@ fn main() {
if let Ok(path) = &cuda_path {
println!("cargo:warning=CUDA Toolkit found at {path} — GPU acceleration enabled.");
} else {
println!("cargo:warning=CUDA Toolkit (nvcc) found in PATH — GPU acceleration enabled.");
println!(
"cargo:warning=CUDA Toolkit (nvcc) found in PATH — GPU acceleration enabled."
);
}
} else {
println!("cargo:warning=━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("cargo:warning= candle-cuda feature is enabled but no CUDA Toolkit found.");
println!("cargo:warning= Install CUDA Toolkit from https://developer.nvidia.com/cuda-downloads");
println!("cargo:warning= Or build without GPU support: cargo build --no-default-features");
println!(
"cargo:warning= Or build without GPU support: cargo build --no-default-features"
);
println!("cargo:warning=━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
+9 -3
View File
@@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default",
@@ -14,9 +16,13 @@
"fs:read-files",
"fs:read-dirs",
"notification:default",
"updater:default",
"process:allow-restart",
"core:window:allow-minimize",
"core:window:allow-close",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized"
"core:window:allow-is-maximized",
"core:window:allow-start-dragging",
"core:window:allow-start-resize-dragging"
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+54
View File
@@ -0,0 +1,54 @@
/// AI-generated tags that are too broad/noisy to be useful in this gallery.
/// Edit this list to change what the tagger removes. Manual user tags are not
/// affected.
pub const AI_TAG_REMOVAL_LIST: &[&str] = &["1girl", "1boy", "no humans", "2girls", "2boys"];
fn normalize_tag_for_removal(tag: &str) -> String {
tag.trim()
.chars()
.filter(|c| !matches!(c, ' ' | '_' | '-'))
.flat_map(char::to_lowercase)
.collect()
}
pub fn is_removed_ai_tag(tag: &str) -> bool {
let normalized = normalize_tag_for_removal(tag);
AI_TAG_REMOVAL_LIST
.iter()
.any(|blocked| normalize_tag_for_removal(blocked) == normalized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn removed_ai_tags_match_common_spellings() {
for tag in [
"1girl",
"1 girl",
"1_girl",
"1-girl",
"NO_HUMANS",
"no humans",
] {
assert!(is_removed_ai_tag(tag), "{tag} should be removed");
}
}
#[test]
fn removed_ai_tags_do_not_match_unrelated_tags() {
for tag in ["girl", "boy", "humans", "solo", "landscape"] {
assert!(!is_removed_ai_tag(tag), "{tag} should be kept");
}
}
#[test]
fn removed_ai_tags_tolerate_padding_and_mixed_separators() {
assert!(is_removed_ai_tag(" 1girl "));
assert!(is_removed_ai_tag("1_-_girl"));
assert!(is_removed_ai_tag("No Humans"));
assert!(!is_removed_ai_tag(""));
assert!(!is_removed_ai_tag(" "));
}
}
+21 -118
View File
@@ -1,3 +1,6 @@
use crate::onnx_runtime::{
self, DIRECTML_DLL_FILE, ONNX_RUNTIME_DLL_FILE, ONNX_RUNTIME_PROVIDERS_DLL_FILE,
};
use anyhow::Result;
use hf_hub::{api::sync::Api, Repo, RepoType};
use image::{imageops::FilterType, ImageReader};
@@ -8,43 +11,16 @@ use ort::session::{builder::GraphOptimizationLevel, Session};
use ort::value::{Shape, Tensor};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::time::Instant;
use tokenizers::Tokenizer;
pub const FLORENCE_MODEL_ID: &str = "onnx-community/Florence-2-base-ft";
pub const FLORENCE_CAPTION_MODEL_NAME: &str = "florence-2-base-ft-onnx-q4";
const ONNX_RUNTIME_NUGET_URL: &str =
"https://www.nuget.org/api/v2/package/Microsoft.ML.OnnxRuntime.DirectML/1.24.2";
const DIRECTML_NUGET_URL: &str =
"https://www.nuget.org/api/v2/package/Microsoft.AI.DirectML/1.15.4";
const ONNX_RUNTIME_DLL_FILE: &str = "onnxruntime/onnxruntime.dll";
const ONNX_RUNTIME_PROVIDERS_DLL_FILE: &str = "onnxruntime/onnxruntime_providers_shared.dll";
const DIRECTML_DLL_FILE: &str = "onnxruntime/DirectML.dll";
const CAPTION_ACCELERATION_FILE: &str = "settings/caption_acceleration.txt";
const CAPTION_DETAIL_FILE: &str = "settings/caption_detail.txt";
const ONNX_RUNTIME_FILES: &[(&str, &str, &str)] = &[
(
ONNX_RUNTIME_DLL_FILE,
ONNX_RUNTIME_NUGET_URL,
"runtimes/win-x64/native/onnxruntime.dll",
),
(
ONNX_RUNTIME_PROVIDERS_DLL_FILE,
ONNX_RUNTIME_NUGET_URL,
"runtimes/win-x64/native/onnxruntime_providers_shared.dll",
),
(
DIRECTML_DLL_FILE,
DIRECTML_NUGET_URL,
"bin/x64-win/DirectML.dll",
),
];
const REQUIRED_FILES: &[&str] = &[
ONNX_RUNTIME_DLL_FILE,
ONNX_RUNTIME_PROVIDERS_DLL_FILE,
@@ -62,15 +38,14 @@ const REQUIRED_FILES: &[&str] = &[
"onnx/embed_tokens_fp16.onnx",
];
static ORT_RUNTIME_INIT: OnceLock<std::result::Result<(), String>> = OnceLock::new();
/// Set to `true` by `set_caption_acceleration` so the caption worker loop
/// knows to drop its cached `FlorenceCaptioner` and reload with the new EP.
pub static CAPTION_SESSION_DIRTY: AtomicBool = AtomicBool::new(false);
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CaptionAcceleration {
#[default]
Auto,
Cpu,
Directml,
@@ -86,17 +61,12 @@ impl CaptionAcceleration {
}
}
impl Default for CaptionAcceleration {
fn default() -> Self {
Self::Auto
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CaptionDetail {
Short,
Detailed,
#[default]
Paragraph,
}
@@ -126,12 +96,6 @@ impl CaptionDetail {
}
}
impl Default for CaptionDetail {
fn default() -> Self {
Self::Paragraph
}
}
#[derive(Serialize)]
pub struct CaptionModelStatus {
pub model_id: &'static str,
@@ -294,11 +258,11 @@ pub fn prepare_caption_model_with_progress(
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
if ONNX_RUNTIME_FILES
if onnx_runtime::ONNX_RUNTIME_FILES
.iter()
.any(|(runtime_file, _, _)| runtime_file == file)
{
download_onnx_runtime_files(&local_dir)?;
onnx_runtime::download_onnx_runtime_files(&local_dir)?;
completed_files = REQUIRED_FILES
.iter()
.filter(|file| local_dir.join(file).exists())
@@ -344,7 +308,7 @@ pub fn probe_caption_runtime(app_data_dir: &Path) -> Result<CaptionRuntimeProbe>
}
let local_dir = model_dir(app_data_dir);
ensure_onnx_runtime(&local_dir)?;
onnx_runtime::ensure_onnx_runtime(&local_dir)?;
let tokenizer =
Tokenizer::from_file(local_dir.join("tokenizer.json")).map_err(anyhow::Error::msg)?;
@@ -393,7 +357,7 @@ pub fn probe_caption_vision(app_data_dir: &Path, image_path: &Path) -> Result<Ca
}
let local_dir = model_dir(app_data_dir);
ensure_onnx_runtime(&local_dir)?;
onnx_runtime::ensure_onnx_runtime(&local_dir)?;
let pixels = preprocess_image(image_path)?;
let input_shape = vec![1, 3, 768, 768];
let input = Tensor::from_array(([1usize, 3, 768, 768], pixels.into_boxed_slice()))
@@ -438,7 +402,7 @@ impl FlorenceCaptioner {
}
let local_dir = model_dir(app_data_dir);
ensure_onnx_runtime(&local_dir)?;
onnx_runtime::ensure_onnx_runtime(&local_dir)?;
let tokenizer =
Tokenizer::from_file(local_dir.join("tokenizer.json")).map_err(anyhow::Error::msg)?;
let caption_detail = caption_detail(app_data_dir);
@@ -470,7 +434,7 @@ impl FlorenceCaptioner {
acceleration,
false,
)?;
println!(
log::info!(
"Florence sessions loaded in {:?} with {:?} acceleration (total init {:?})",
sessions_started_at.elapsed(),
acceleration,
@@ -490,9 +454,9 @@ impl FlorenceCaptioner {
pub fn generate(&mut self, image_path: &Path) -> Result<String> {
let started_at = Instant::now();
println!("Florence caption started: {}", image_path.display());
log::info!("Florence caption started: {}", image_path.display());
let image_features = run_vision_encoder(&mut self.vision_session, image_path)?;
println!("Florence vision encoder done in {:?}", started_at.elapsed());
log::info!("Florence vision encoder done in {:?}", started_at.elapsed());
let prompt_ids = self
.tokenizer
.encode(self.caption_detail.prompt(), false)
@@ -502,7 +466,7 @@ impl FlorenceCaptioner {
.map(|id| i64::from(*id))
.collect::<Vec<_>>();
let prompt_embeds = run_token_embedder(&mut self.embed_session, &prompt_ids)?;
println!(
log::info!(
"Florence token embeddings done in {:?}",
started_at.elapsed()
);
@@ -513,7 +477,7 @@ impl FlorenceCaptioner {
&encoder_embeds,
&encoder_attention_mask,
)?;
println!("Florence encoder done in {:?}", started_at.elapsed());
log::info!("Florence encoder done in {:?}", started_at.elapsed());
let generated_ids = run_decoder(
&mut self.decoder_prefill_session,
@@ -523,7 +487,7 @@ impl FlorenceCaptioner {
&encoder_attention_mask,
self.caption_detail.max_new_tokens(),
)?;
println!("Florence decoder done in {:?}", started_at.elapsed());
log::info!("Florence decoder done in {:?}", started_at.elapsed());
let generated_u32 = generated_ids
.into_iter()
@@ -647,67 +611,6 @@ fn probe_vision_session(
})
}
pub fn ensure_onnx_runtime(local_dir: &Path) -> Result<()> {
let dll_path = local_dir.join(ONNX_RUNTIME_DLL_FILE);
ORT_RUNTIME_INIT
.get_or_init(|| {
if !dll_path.exists() {
return Err(format!(
"ONNX Runtime DLL is missing: {}",
dll_path.display()
));
}
ort::environment::init_from(&dll_path)
.map_err(|error| error.to_string())?
.with_name("phokus-florence")
.commit();
Ok(())
})
.clone()
.map_err(anyhow::Error::msg)
}
fn download_onnx_runtime_files(local_dir: &Path) -> Result<()> {
if !cfg!(target_os = "windows") {
anyhow::bail!(
"Florence-2 ONNX Runtime download is currently configured for Windows builds"
);
}
for (destination_file, source_url, archive_path) in ONNX_RUNTIME_FILES {
let destination = local_dir.join(destination_file);
download_nuget_file(source_url, archive_path, &destination)?;
}
Ok(())
}
fn download_nuget_file(source_url: &str, archive_path: &str, destination: &Path) -> Result<()> {
let mut response = ureq::get(source_url)
.call()
.map_err(|error| anyhow::anyhow!("{error}"))?;
let mut bytes = Vec::new();
response
.body_mut()
.as_reader()
.read_to_end(&mut bytes)
.map_err(|error| anyhow::anyhow!("{error}"))?;
let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?;
let mut dll = archive.by_name(archive_path)?;
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
let temp_destination = destination.with_extension("tmp");
{
let mut file = std::fs::File::create(&temp_destination)?;
std::io::copy(&mut dll, &mut file)?;
}
std::fs::rename(temp_destination, destination)?;
Ok(())
}
fn run_vision_encoder(session: &mut Session, image_path: &Path) -> Result<TensorData> {
let pixels = preprocess_image(image_path)?;
let input = Tensor::from_array(([1usize, 3, 768, 768], pixels.into_boxed_slice()))
@@ -784,7 +687,7 @@ fn run_decoder(
"inputs_embeds" => prefill_inputs_embeds_tensor
})
.map_err(|error| anyhow::anyhow!("{error}"))?;
println!(
log::info!(
"Florence decoder prefill done in {:?}",
prefill_started_at.elapsed()
);
@@ -848,7 +751,7 @@ fn run_decoder(
decoder_kv = collect_present_key_values(&outputs, DECODER_LAYERS)?;
}
println!("Florence decoder produced {} token(s)", generated.len());
log::info!("Florence decoder produced {} token(s)", generated.len());
Ok(generated)
}
@@ -886,7 +789,7 @@ fn create_session(
CaptionAcceleration::Auto | CaptionAcceleration::Directml => {
// `allow_directml` is false for the 4 text/decoder sessions —
// they intentionally run on CPU regardless of the setting.
println!(
log::info!(
"Florence: using CPU for {} (DirectML disabled for this session type)",
path.display()
);
+91
View File
@@ -0,0 +1,91 @@
//! Dominant-color palette extraction for color search.
//!
//! Colors are sampled from the already-generated thumbnail (small, fast) rather
//! than the full image. We coarse-quantize pixels into an RGB histogram, then
//! return the most populated bins as representative colors with their weight
//! (fraction of sampled pixels). Search then filters images whose palette has a
//! color within a distance threshold of the query color.
use image::RgbImage;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub struct PaletteColor {
pub r: u8,
pub g: u8,
pub b: u8,
/// Fraction of sampled pixels (0.01.0) that fell in this color's bin.
pub weight: f32,
}
/// Bits kept per channel when binning. 4 bits → 16 levels/channel → 4096 bins:
/// coarse enough to group near-identical shades, fine enough to separate hues.
const QUANT_BITS: u32 = 4;
/// Cap on sampled pixels so very large frames stay cheap; thumbnails are tiny so
/// this rarely bites, but the backfill may read arbitrary thumbnail sizes.
const MAX_SAMPLES: usize = 50_000;
/// Extract up to `k` dominant colors from an RGB image, most-common first.
pub fn extract_palette(img: &RgbImage, k: usize) -> Vec<PaletteColor> {
let pixels = img.as_raw();
let pixel_count = pixels.len() / 3;
if pixel_count == 0 {
return Vec::new();
}
let step = (pixel_count / MAX_SAMPLES).max(1);
let shift = 8 - QUANT_BITS;
// bin key → (sum_r, sum_g, sum_b, count); summing lets us return the bin's
// average color rather than the quantized corner.
let mut bins: HashMap<u16, (u64, u64, u64, u64)> = HashMap::new();
let mut total: u64 = 0;
for pixel in pixels.chunks_exact(3).step_by(step) {
let (r, g, b) = (pixel[0], pixel[1], pixel[2]);
let key = (((r as u16) >> shift) << (QUANT_BITS * 2))
| (((g as u16) >> shift) << QUANT_BITS)
| ((b as u16) >> shift);
let entry = bins.entry(key).or_insert((0, 0, 0, 0));
entry.0 += r as u64;
entry.1 += g as u64;
entry.2 += b as u64;
entry.3 += 1;
total += 1;
}
if total == 0 {
return Vec::new();
}
let mut entries: Vec<(u64, u64, u64, u64)> = bins.into_values().collect();
entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.3));
entries
.into_iter()
.take(k)
.map(|(sum_r, sum_g, sum_b, count)| PaletteColor {
r: (sum_r / count) as u8,
g: (sum_g / count) as u8,
b: (sum_b / count) as u8,
weight: count as f32 / total as f32,
})
.collect()
}
/// Decode a thumbnail file and extract its palette. Used by the backfill pass.
pub fn extract_palette_from_file(thumbnail_path: &Path, k: usize) -> Option<Vec<PaletteColor>> {
let img = image::ImageReader::open(thumbnail_path)
.ok()?
.decode()
.ok()?;
Some(extract_palette(&img.into_rgb8(), k))
}
/// Number of palette colors stored per image.
pub const PALETTE_SIZE: usize = 5;
/// Max squared RGB distance for a palette color to count as matching a query
/// color (~70 units in RGB space). Tunable feel/precision of color search.
pub const MATCH_DISTANCE_SQ: i64 = 4900;
/// Minimum weight (fraction of pixels) a palette color must have to match, so
/// trivial specks of a color don't trigger a match.
pub const MATCH_MIN_WEIGHT: f64 = 0.05;
+1542 -213
View File
File diff suppressed because it is too large Load Diff
+1510 -59
View File
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
//! Resilient file downloads via the system `curl` binary.
//!
//! Used for all large model/runtime downloads (tagger models, ONNX Runtime
//! DLLs, caption model). ureq's read timeout does not fire on a stalled large
//! transfer on Windows (schannel doesn't honor the socket read timeout), so a
//! stall there hangs forever. curl detects an inactivity stall
//! (`--speed-time`), resumes from the partial file (`-C -`), and retries
//! internally — the same behavior a browser gets.
use anyhow::Result;
use std::io::Read;
use std::path::Path;
use std::process::Command;
// Suppress the console window when spawning curl from the GUI app.
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
/// Discard target for curl output we only need the headers of.
#[cfg(target_os = "windows")]
const NULL_DEVICE: &str = "NUL";
#[cfg(not(target_os = "windows"))]
const NULL_DEVICE: &str = "/dev/null";
/// Build a `curl` command with platform quirks applied. Windows resolves the
/// bare name to `curl.exe` (bundled since Windows 10 1803) and needs the
/// no-window flag; macOS ships curl; Linux is expected to have it installed.
fn curl_command() -> Command {
#[allow(unused_mut)]
let mut command = Command::new("curl");
#[cfg(target_os = "windows")]
command.creation_flags(CREATE_NO_WINDOW);
command
}
// Give up only after this many *consecutive* curl runs that download nothing;
// a run that makes any progress resets the counter, so a large file completes
// across however many resumes it takes. Kept low so a hard stall (e.g. a
// broken VM NIC) fails in a couple of minutes — surfacing a retryable error —
// rather than locking the UI on "preparing" for many minutes.
const MAX_STALL_RETRIES: usize = 3;
/// Resiliently download `url` to `destination` using the system `curl`.
///
/// We monitor the `.part` file's size for the progress bar and wrap curl in an
/// outer progress-aware retry as a backstop. The partial file survives an app
/// restart, so a later retry continues from disk.
pub fn download_file_resilient(
url: &str,
destination: &Path,
mut on_progress: impl FnMut(u64, Option<u64>),
) -> Result<()> {
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
let part = match destination.extension() {
Some(ext) => destination.with_extension(format!("{}.part", ext.to_string_lossy())),
None => destination.with_extension("part"),
};
let name = destination
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| url.to_string());
log::info!("{name}: resolving download size");
let total = remote_content_length(url);
// Reconcile any existing `.part` against the real size: exactly complete →
// finish; oversized (stale/corrupt) → discard so curl restarts cleanly
// (otherwise `curl -C -` would 416 forever).
if let Some(total) = total {
let size = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
if size == total {
std::fs::rename(&part, destination)?;
return Ok(());
}
if size > total {
let _ = std::fs::remove_file(&part);
}
}
log::info!(
"{name}: downloading via curl ({} bytes)",
total
.map(|t| t.to_string())
.unwrap_or_else(|| "unknown size".into())
);
let mut stalls = 0usize;
loop {
let before = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
match run_curl_download(url, &part, total, &mut on_progress) {
Ok(()) => break,
Err(error) => {
let after = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
if after > before {
log::warn!("{name}: curl interrupted at {after} bytes, resuming: {error}");
stalls = 0;
} else {
stalls += 1;
log::warn!(
"{name}: curl made no progress ({stalls}/{MAX_STALL_RETRIES}): {error}"
);
if stalls >= MAX_STALL_RETRIES {
// Discard the partial so a future attempt restarts clean
// rather than getting stuck re-resuming a bad file.
let _ = std::fs::remove_file(&part);
return Err(error);
}
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
}
if let Some(total) = total {
let got = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
if got < total {
anyhow::bail!("{name}: incomplete after curl ({got}/{total} bytes)");
}
}
std::fs::rename(&part, destination)?;
Ok(())
}
/// Size probe via `curl -r 0-0` (a 1-byte Range request), parsing the total
/// from the `Content-Range: bytes 0-0/<total>` header. Uses curl rather than
/// ureq so no part of the download path depends on ureq (which hangs on this
/// VM's TLS stack). Returns None if the server doesn't report a size.
fn remote_content_length(url: &str) -> Option<u64> {
let mut command = curl_command();
command.args([
"-sL",
"-r",
"0-0",
"-D",
"-",
"-o",
NULL_DEVICE,
"--connect-timeout",
"30",
"--max-time",
"30",
"--",
url,
]);
let output = command.output().ok()?;
let headers = String::from_utf8_lossy(&output.stdout);
for line in headers.lines() {
if let Some(rest) = line.to_ascii_lowercase().strip_prefix("content-range:") {
if let Some(total) = rest.rsplit('/').next().map(str::trim) {
if let Ok(n) = total.parse::<u64>() {
return Some(n);
}
}
}
}
None
}
/// Run one `curl` download to `dest`, resuming from any partial file, while
/// reporting progress from the growing file size. Returns an error (leaving the
/// partial in place) if curl exits non-zero.
fn run_curl_download(
url: &str,
dest: &Path,
total: Option<u64>,
on_progress: &mut impl FnMut(u64, Option<u64>),
) -> Result<()> {
let mut command = curl_command();
command
.arg("-fSL") // fail on HTTP errors, follow redirects, show errors
.args(["-C", "-"]) // resume from the existing output file
.args(["--retry", "3", "--retry-delay", "1", "--retry-connrefused"])
.args(["--connect-timeout", "30"])
// Abort (then --retry resumes) if under 1 KB/s for 30s — a real
// inactivity timeout, which is what ureq couldn't deliver here.
.args(["--speed-limit", "1024", "--speed-time", "30"])
.arg("-s") // no progress meter (we watch the file instead)
.arg("-o")
.arg(dest)
.arg("--")
.arg(url)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
let mut child = command
.spawn()
.map_err(|e| anyhow::anyhow!("failed to launch curl (required for downloads): {e}"))?;
loop {
if let Some(status) = child.try_wait()? {
if status.success() {
return Ok(());
}
let mut stderr = String::new();
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
anyhow::bail!(
"curl exited with {}: {}",
status
.code()
.map(|c| c.to_string())
.unwrap_or_else(|| "signal".into()),
stderr.trim()
);
}
let downloaded = std::fs::metadata(dest).map(|m| m.len()).unwrap_or(0);
on_progress(downloaded, total);
std::thread::sleep(std::time::Duration::from_millis(300));
}
}
/// Download a NuGet package (a zip) resiliently, then extract the single file
/// at `archive_path` into `destination`.
pub fn download_nuget_file(
source_url: &str,
archive_path: &str,
destination: &Path,
on_progress: impl FnMut(u64, Option<u64>),
) -> Result<()> {
let package = destination.with_extension("nupkg");
download_file_resilient(source_url, &package, on_progress)?;
log::info!("extracting {archive_path} from package");
let file = std::fs::File::open(&package)?;
let mut archive = zip::ZipArchive::new(file)?;
let mut dll = archive.by_name(archive_path)?;
let temp_destination = destination.with_extension("tmp");
{
let mut out = std::fs::File::create(&temp_destination)?;
std::io::copy(&mut dll, &mut out)?;
}
std::fs::rename(&temp_destination, destination)?;
let _ = std::fs::remove_file(&package);
log::info!("extracted {archive_path}");
Ok(())
}
+30 -20
View File
@@ -20,7 +20,7 @@ fn with_text_embedder<T>(f: impl FnOnce(&ClipImageEmbedder) -> Result<T>) -> Res
.lock()
.map_err(|_| anyhow::anyhow!("Text embedder lock poisoned"))?;
if guard.is_none() {
println!("Initializing CLIP text embedder...");
log::info!("Initializing CLIP text embedder...");
*guard = Some(ClipImageEmbedder::new()?);
}
f(guard.as_ref().unwrap())
@@ -35,13 +35,13 @@ pub struct ClipImageEmbedder {
impl ClipImageEmbedder {
pub fn new() -> Result<Self> {
println!("Initializing CLIP image embedder...");
log::info!("Initializing CLIP image embedder...");
let api = Api::new()?;
let repo = api.repo(Repo::new(
"laion/CLIP-ViT-B-32-laion2B-s34B-b79K".to_string(),
RepoType::Model,
));
println!("Resolving CLIP model weights from Hugging Face cache...");
log::info!("Resolving CLIP model weights from Hugging Face cache...");
let model_path = repo.get("model.safetensors")?;
let tokenizer_repo = api.repo(Repo::new(
"openai/clip-vit-base-patch32".to_string(),
@@ -60,7 +60,7 @@ impl ClipImageEmbedder {
};
let model = ClipModel::new(vb, &config)?;
let tokenizer = Tokenizer::from_file(tokenizer_path).map_err(anyhow::Error::msg)?;
println!("CLIP image embedder ready.");
log::info!("CLIP image embedder ready.");
Ok(Self {
model,
@@ -160,7 +160,7 @@ impl ClipImageEmbedder {
let ids = enc.get_ids();
let len = ids.len().min(max_len);
for j in 0..len {
flat[i * max_len + j] = ids[j] as u32;
flat[i * max_len + j] = ids[j];
}
}
@@ -177,23 +177,25 @@ impl ClipImageEmbedder {
fn resolve_device() -> Result<Device> {
match Device::cuda_if_available(0) {
Ok(device) if device.is_cuda() => {
println!("CLIP embedder: using CUDA GPU (device 0).");
log::info!("CLIP embedder: using CUDA GPU (device 0).");
return Ok(device);
}
Ok(_) => {
println!("CLIP embedder: no compatible CUDA GPU found — using CPU.");
log::info!("CLIP embedder: no compatible CUDA GPU found — using CPU.");
}
Err(e) => {
println!("CLIP embedder: CUDA init failed ({e}) — using CPU.");
log::info!("CLIP embedder: CUDA init failed ({e}) — using CPU.");
}
}
Ok(Device::Cpu)
}
fn load_image(path: &Path, image_size: usize) -> Result<Tensor> {
let image = image::ImageReader::open(path)?
.with_guessed_format()?
.decode()?;
// Scaled decode: CLIP only needs image_size² pixels, so decoding a large
// JPEG at full resolution is wasted work. Cover mode keeps the shortest
// side at or above image_size for the fill-crop below. Also applies EXIF
// orientation, so rotated photos embed the way they are displayed.
let image = crate::thumbnail::decode_image_scaled(path, image_size as u32, true)?;
let image = image.resize_to_fill(
image_size as u32,
image_size as u32,
@@ -208,28 +210,29 @@ fn load_image(path: &Path, image_size: usize) -> Result<Tensor> {
}
fn load_images(paths: &[PathBuf], image_size: usize) -> Result<Tensor> {
let mut images = Vec::with_capacity(paths.len());
for path in paths {
images.push(load_image(path, image_size)?);
}
use rayon::prelude::*;
let images = paths
.par_iter()
.map(|path| load_image(path, image_size))
.collect::<Result<Vec<_>>>()?;
Ok(Tensor::stack(&images, 0)?)
}
/// Returns the path that should be fed to the CLIP image embedder for a given media file.
///
/// For videos the thumbnail image is used (because CLIP only understands still images).
/// If a video has no thumbnail yet, an error is returned — the caller should mark the
/// embedding job as failed rather than trying to decode the raw video file.
/// For videos and AVIFs the thumbnail image is used because CLIP preprocessing
/// only uses decoders from the `image` crate, while AVIF is decoded through
/// FFmpeg into a JPEG thumbnail.
pub fn embedding_source_path(
path: &str,
thumbnail_path: Option<&str>,
media_kind: &str,
) -> Result<PathBuf> {
if media_kind == "video" {
if media_kind == "video" || is_avif_path(path) {
match thumbnail_path {
Some(thumb) => Ok(PathBuf::from(thumb)),
None => Err(anyhow::anyhow!(
"No thumbnail available yet for video '{}' — embedding deferred until thumbnail is generated",
"No thumbnail available yet for '{}' — embedding deferred until thumbnail is generated",
std::path::Path::new(path)
.file_name()
.map(|n| n.to_string_lossy())
@@ -240,3 +243,10 @@ pub fn embedding_source_path(
Ok(PathBuf::from(path))
}
}
fn is_avif_path(path: &str) -> bool {
std::path::Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("avif"))
}
+12 -4
View File
@@ -92,6 +92,7 @@ pub fn find_similar_image_matches(
conn: &Connection,
image_id: i64,
folder_id: Option<i64>,
album_id: Option<i64>,
threshold: f32,
offset: usize,
limit: usize,
@@ -103,10 +104,17 @@ pub fn find_similar_image_matches(
None => return Ok(Vec::new()),
};
// Fetch folder image IDs *before* acquiring the read lock so we don't hold
// Build the allowed-id set *before* acquiring the read lock so we don't hold
// the lock across a potentially slow SQLite query, which would delay any
// concurrent ensure_index call waiting for a write lock.
let folder_image_ids: Option<Vec<i64>> = if let Some(folder_id) = folder_id {
// concurrent ensure_index call waiting for a write lock. Album scope takes
// precedence over folder scope; both reuse the HNSW filtered search.
let allowed_image_ids: Option<Vec<i64>> = if let Some(album_id) = album_id {
let mut stmt = conn.prepare("SELECT image_id FROM album_images WHERE album_id = ?1")?;
let ids = stmt
.query_map([album_id], |row| row.get::<_, i64>(0))?
.collect::<rusqlite::Result<Vec<_>>>()?;
Some(ids)
} else if let Some(folder_id) = folder_id {
let ids = vector::get_all_image_embeddings_with_ids(conn, Some(folder_id))?
.into_iter()
.map(|(id, _)| id)
@@ -122,7 +130,7 @@ pub fn find_similar_image_matches(
};
let knbn = (offset + limit).max(limit).saturating_add(32);
let neighbours: Vec<Neighbour> = if let Some(image_ids) = folder_image_ids {
let neighbours: Vec<Neighbour> = if let Some(image_ids) = allowed_image_ids {
let mut allowed_ids = image_ids
.into_iter()
.filter_map(|allowed_image_id| {
+694 -171
View File
File diff suppressed because it is too large Load Diff
+128 -7
View File
@@ -1,10 +1,14 @@
mod ai_tag_filter;
mod captioner;
mod color;
mod commands;
mod db;
mod download;
mod embedder;
mod hnsw_index;
mod indexer;
mod media;
mod onnx_runtime;
mod storage;
mod tagger;
mod thumbnail;
@@ -16,11 +20,61 @@ use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
// Must be the first plugin: a second launch hands its args to the
// running instance and exits before anything else initializes.
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.set_focus();
}
}))
.plugin(
tauri_plugin_log::Builder::new()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
file_name: Some("phokus".into()),
}),
])
.level(log::LevelFilter::Info)
.max_file_size(5 * 1024 * 1024)
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
.build(),
)
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_notification::init())
.setup(|app| {
// Fresh installs open at the fixed config size (1280×800) because the
// window-state plugin has nothing saved yet — too tall for laptops
// like 1366×768. Clamp the window to the monitor's work area (which
// already excludes the taskbar) and re-center it there, but only when
// it actually overflows, so a restored size/position on a roomier
// display is left untouched. Sizes are physical pixels on both sides,
// so this stays correct across display scaling.
if let Some(window) = app.get_webview_window("main") {
if let Ok(Some(monitor)) = window.current_monitor() {
let area = monitor.work_area();
let max_w = area.size.width.saturating_sub(32);
let max_h = area.size.height.saturating_sub(32);
if let Ok(size) = window.outer_size() {
if size.width > max_w || size.height > max_h {
let new_w = size.width.min(max_w);
let new_h = size.height.min(max_h);
let _ = window.set_size(tauri::PhysicalSize::new(new_w, new_h));
let _ = window.set_position(tauri::PhysicalPosition::new(
area.position.x + (area.size.width as i32 - new_w as i32) / 2,
area.position.y + (area.size.height as i32 - new_h as i32) / 2,
));
}
}
}
}
let app_dir = app
.path()
.app_data_dir()
@@ -28,7 +82,10 @@ pub fn run() {
std::fs::create_dir_all(&app_dir).expect("Failed to create app data dir");
media::MediaTools::ensure_installed().expect("Failed to provision FFmpeg sidecar");
// FFmpeg provisioning happens in the background so the window
// appears immediately; workers gate video/AVIF jobs on readiness and
// the onboarding/Settings UI shows progress and retry.
media::spawn_ffmpeg_provision(app.handle().clone());
let db_path = app_dir.join("gallery.db");
let pool = db::create_pool(&db_path).expect("Failed to create database pool");
@@ -38,23 +95,46 @@ pub fn run() {
let conn = pool.get().expect("Failed to get connection for migration");
db::migrate(&conn).expect("Failed to run migrations");
db::reset_inflight_jobs(&conn).expect("Failed to reset inflight jobs");
let repaired_deferred = db::repair_deferred_embedding_jobs(&conn)
.expect("Failed to repair deferred embedding jobs");
if repaired_deferred > 0 {
log::info!("Requeued {repaired_deferred} deferred embedding jobs.");
}
let repaired_avif =
db::repair_avif_jobs(&conn).expect("Failed to repair AVIF jobs");
if repaired_avif > 0 {
log::info!("Requeued {repaired_avif} AVIF jobs.");
}
let backfilled =
db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs");
if backfilled > 0 {
println!("Backfilled {} embedding jobs.", backfilled);
log::info!("Backfilled {backfilled} embedding jobs.");
}
let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn)
.expect("Failed to repair embedding consistency");
if orphaned_vectors > 0 || missing_vectors > 0 {
println!(
"Repaired embedding consistency: removed {} orphaned vectors, requeued {} missing vectors.",
orphaned_vectors, missing_vectors
log::info!(
"Repaired embedding consistency: removed {orphaned_vectors} orphaned vectors, requeued {missing_vectors} missing vectors."
);
}
}
let thumb_dir = app_dir.join("thumbnails");
std::fs::create_dir_all(&thumb_dir).expect("Failed to create thumbnail dir");
commands::restore_persisted_worker_pauses(&app_dir);
// The asset protocol scope is no longer a blanket "**": thumbnails
// are allowed statically in tauri.conf.json, and each indexed
// folder is allowed here (and in add_folder/update_folder_path).
{
let scope = app.asset_protocol_scope();
let conn = pool.get().expect("Failed to get connection for asset scope");
for folder in db::get_folders(&conn).unwrap_or_default() {
if let Err(error) = scope.allow_directory(&folder.path, true) {
log::error!("Failed to allow asset scope for {}: {}", folder.path, error);
}
}
}
let thumbnail_worker_count = std::thread::available_parallelism()
.map(|parallelism| StorageProfile::Balanced.thumbnail_workers(parallelism.get()))
@@ -73,8 +153,10 @@ pub fn run() {
// Caption worker disabled — UI removed; keeping backend code intact for future use.
// indexer::start_caption_worker(app.handle().clone(), pool.clone(), app_dir.clone());
indexer::start_tagging_worker(app.handle().clone(), pool.clone(), app_dir.clone());
// Backfill color palettes for images indexed before color search existed.
indexer::start_color_backfill(app.handle().clone(), pool.clone());
let watcher_handle = indexer::start_watcher(app.handle().clone(), pool.clone());
let watcher_handle = indexer::start_watcher(app.handle().clone(), pool.clone(), thumb_dir.clone());
app.manage(pool);
app.manage(media_tools);
@@ -84,7 +166,10 @@ pub fn run() {
})
.invoke_handler(tauri::generate_handler![
commands::add_folder,
commands::add_folders,
commands::list_directories,
commands::get_folders,
commands::reorder_folders,
commands::get_background_job_progress,
commands::remove_folder,
commands::get_images,
@@ -113,13 +198,19 @@ pub fn run() {
commands::suggest_image_tags,
commands::set_worker_paused,
commands::get_worker_states,
commands::get_tag_cloud,
commands::get_worker_pauses_persist,
commands::set_worker_pauses_persist,
commands::get_visual_clusters,
commands::get_explore_tags,
commands::get_related_tags,
commands::get_images_by_ids,
commands::get_failed_embedding_images,
commands::get_failed_tagging_images,
commands::get_tagger_model_status,
commands::get_tagger_acceleration,
commands::set_tagger_acceleration,
commands::get_tagger_model,
commands::set_tagger_model,
commands::probe_tagger_runtime,
commands::get_tagger_threshold,
commands::set_tagger_threshold,
@@ -129,9 +220,26 @@ pub fn run() {
commands::delete_tagger_model,
commands::queue_tagging_jobs,
commands::clear_tagging_jobs,
commands::reset_ai_tags,
commands::get_image_tags,
commands::add_user_tag,
commands::remove_tag,
commands::rename_tag,
commands::delete_tag,
commands::get_image_exif,
commands::list_albums,
commands::create_album,
commands::rename_album,
commands::delete_album,
commands::delete_albums,
commands::reorder_albums,
commands::add_images_to_album,
commands::remove_images_from_album,
commands::get_album_images,
commands::bulk_update_details,
commands::bulk_add_tags,
commands::bulk_remove_tag,
commands::get_build_variant,
commands::search_tags_autocomplete,
commands::find_duplicates,
commands::load_duplicate_scan_cache,
@@ -144,10 +252,23 @@ pub fn run() {
commands::get_tagging_queue_folder_ids,
commands::set_tagging_queue_folder_ids,
commands::open_app_data_folder,
commands::open_map_location,
commands::open_changelog_url,
commands::get_database_info,
commands::vacuum_database,
commands::rebuild_semantic_index,
commands::get_orphaned_thumbnails_info,
commands::cleanup_orphaned_thumbnails,
commands::get_muted_folder_ids,
commands::set_muted_folder_ids,
commands::get_ffmpeg_status,
commands::retry_ffmpeg_download,
commands::get_onboarding_completed,
commands::set_onboarding_completed,
commands::get_last_seen_version,
commands::set_last_seen_version,
commands::get_notifications_paused,
commands::set_notifications_paused,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

Some files were not shown because too many files have changed in this diff Show More