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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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
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
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
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.
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.
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.
Scans the thumbnails directory and cross-references against thumbnail_path
values in the images table, deleting any files not linked to an indexed image.
Surfaces count and reclaimable MB in the General section alongside the
existing Compact database card.
Adds get_database_info and vacuum_database Tauri commands. The General
section now shows current DB size and reclaimable space on load, with a
Compact now button that runs PRAGMA wal_checkpoint(FULL) + VACUUM and
reports how many MB were freed. Button disables automatically when the
database is already compact (< 0.5 MB reclaimable).
- folder_ids: use snake_case in both set_tagging_queue_folder_ids invoke
calls (toggleTaggingQueueFolder and setTaggingQueueFolderIds) to match
the Rust SetTaggingQueueFolderIdsParams serde field name
- threshold save error: route setTaggerThreshold() catch to
setTaggerThresholdError instead of setTaggerQueueStatus so the error
surfaces next to the threshold input, not the queue targets card
- Remove Workers section (read-only status, belongs in background tasks panel)
- Remove Captioning coming-soon placeholder
- Add General section with Open data folder button (tauri-plugin-opener)
- Persist queue scope and folder selection across sessions via settings files
- Show inline validation errors on threshold/batch size inputs instead of silent revert
- Fix acceleration save errors appearing in wrong card
- Disable folder selection controls when target scope is All media
- Fix cols overflow: use (containerWidth - GAP) formula so the grid
never exceeds the container width
- Explicitly pin "unknown" date keys to the end of the sort rather than
relying on alphabetical accident ("u" > digits)
- Guard buildLabel against non-YYYY-MM keys with finite/range checks
and an isNaN fallback to "Unknown Date"
- Clear similarSourceFolderId, similarFolderId, and similarCrop when
switching to the timeline view so all similar-state is consistently
reset
db.rs:
- Move idx_images_taken_at creation to after ensure_column so it never
runs against a schema where taken_at doesn't exist yet; this was causing
a startup panic on any DB that predates Phase 1
- COALESCE(taken_at, created_at) → COALESCE(taken_at, modified_at) since
created_at is never populated (modified_at is always set)
- Remove dead is_tagging_job_cancelled function (superseded by
is_tagging_job_processing) and unused thumbnail_path from ImagePathRecord
indexer.rs:
- Watcher create path now calls update_folder_count after commit_batch and
emits folder-counts-changed so the sidebar count stays in sync; the
notification is only emitted when the DB write actually succeeds
store.ts / Timeline.tsx:
- compareImages taken_asc/taken_desc: fall back to modified_at not
created_at (created_at is always null)
- Timeline groupImages: same fallback fix so images group by month
correctly for libraries not yet re-indexed for EXIF
- subscribeToProgress: listen for folder-counts-changed and call
loadFolders() to keep the sidebar image count live
- New Timeline view groups all media by YYYY-MM using taken_at ?? created_at
- @tanstack/react-virtual (group-level virtualiser) keeps scroll smooth on
large libraries; estimateSize is exact so no measureElement needed
- ResizeObserver tracks container width; virtualizer.measure() is called on
cols change to prevent stale position cache after window resize
- setView("timeline") in store auto-sets sort:"taken_asc", resets images,
and triggers a fresh load
- Calendar nav item added to Sidebar between Explore and Duplicates
- ContextMenu and ImageTile exported from Gallery for reuse in Timeline
Monitors all registered folders using OS-native events (ReadDirectoryChangesW
on Windows) so new, modified, and deleted files are reflected in the gallery
automatically without manual reindexing.
Key design points:
- Adaptive blocking: recv() when no events pending (zero CPU), switches to
recv_timeout(earliest_deadline) only when debounce timers are running — wakes
exactly when the soonest event is ready, no busy-polling
- 500 ms per-path debounce coalesces rapid OS event bursts into one action
- Change detection preserved: build_record skips upsert if file_size + mtime
unchanged, preventing spurious thumbnail/embedding re-queues and avoiding
clobbering of existing metadata like thumbnail_path
- Access events (reads) filtered out; only Create/Modify/Remove trigger work
- Deletion path: emits watcher-deleted event with image IDs; frontend removes
those images from state and clears selectedImage if it was deleted
WatcherHandle stored in app state; add_folder / remove_folder / update_folder_path
commands keep the watched path set in sync with the DB.
Adds EXIF date extraction during indexing so photos can be sorted by when
they were actually taken, not just when the file was modified on disk.
- Add `taken_at` (nullable TEXT, ISO 8601) column to the images table via
ensure_column migration and a new idx_images_taken_at index
- Populate taken_at in build_record via extract_exif_date, which tries
DateTimeOriginal → DateTimeDigitized → DateTime tags and rejects all-zero
sentinel dates written by uninitialised cameras
- upsert_image uses taken_at = excluded.taken_at so a re-indexed file gets
fresh EXIF; stale dates from replaced files are not preserved
- Add taken_asc / taken_desc sort using COALESCE(taken_at, created_at) so
images without EXIF gracefully fall back to their creation date
- Surface as "Taken: newest / oldest" in the Toolbar sort dropdown
- Shift map_image_row column indices after the new taken_at (index 10)
Adds a full discovery and organisation layer to the gallery.
## AI Tagger
- WD tagger (ONNX via ort) with DirectML/CPU acceleration
- Per-image and per-folder tag queuing; configurable batch size and
confidence threshold; model downloaded on first use via ureq/zip
- Tags stored with source ('ai' | 'user'); user tags are never
overwritten by AI; AI tags protected from accidental promotion
- Tagger pause/resume per folder; in-flight batches discarded cleanly
on pause or cancellation without leaving jobs stuck in processing
## Semantic & Tag Search
- CLIP text-query embedding via candle (HuggingFace hub model)
- Progressive candidate doubling with filter post-processing so
folder/rating/media-kind filters do not silently under-return results
- Tag search with DB-backed pagination (offset + COUNT total)
- Filename search unchanged; prefix syntax: s:, t:, f: in search bar
## Similarity Search
- HNSW in-memory index (hnsw_rs) with monotonic embedding_revision
counter for cache invalidation; build_index retries if a concurrent
write advances the revision during construction
- Folder-scoped similar search uses brute-force cosine scan (no k limit)
- Region-based similarity: crop an arbitrary rectangle in the lightbox,
embed it with CLIP, find the nearest images globally or within folder
- Pagination for both similar and region results; scope toggle
(all media / current folder) re-runs the query without reopening image
## Duplicate Finder
- Three-phase detection: stat (size) → sample hash (4×16 KB windows)
→ full-file hash (xxh3, large files only) to eliminate false positives
- Filesystem-first deletion: only removes DB rows for files successfully
deleted from disk; failed files remain visible for retry
- Persisted scan cache (SQLite) with invalidation on reindex and deletion;
both global and per-folder scopes invalidated after any deletion
## Tag Cloud & Explore
- Visual cluster explore: k-means over CLIP embeddings, configurable k,
representative thumbnail per cluster; cache keyed on xxh3 of embedding set
- Tag explore: ranked tag list with image counts and representative
thumbnail per tag; invalidated when AI tagging completes or folder removed
- Tag autocomplete for search bar
## Folder Management
- Inline rename (display name only, not OS rename)
- Relocate: file-picker to update folder path; all image paths rewritten
using SUBSTR prefix replacement to avoid corrupting paths that contain
the folder name as a substring
- Missing-folder recovery banner: Locate or Remove, never silent deletion
- Right-click context menu on sidebar items: Reindex, Rename, Locate,
Remove; hover buttons preserved alongside context menu
## Background Tasks
- Per-folder progress panel with embedding, tagging, caption, and
thumbnail stage breakdown
- Retry button per task for failed embeddings and tagging jobs
- Completed-task notifications (system tray via tauri-plugin-notification)
- Scan errors shown inline; WalkDir permission errors protect existing
records from deletion rather than cascading data loss
## Backend correctness
- sqlite-vec DML performed outside transactions throughout (virtual-table
limitation); embedding revision incremented on delete as well as insert
- Tagging job discard checks status = 'processing' so paused jobs reset
to 'pending' are also skipped, not just cancelled ones
- Stale async responses in Lightbox guarded with cancellation flags and
currentImageIdRef for both tag-load and tag-add callbacks
- Duplicate cache cleared on reindex; folder-removal clears vector rows
outside transaction before deleting the folder row
- Fix root cause: embedding_source_path() returns Result<PathBuf>, returning
Err for videos without a thumbnail instead of silently falling back to the
raw .mp4 path that CLIP cannot decode
- indexer: split embedding batch into pre-failed (no source) and embeddable
jobs; pre-failed are marked immediately without hitting the CLIP model
- db: retry_failed_embedding_jobs skips videos still without a thumbnail so
they no longer re-fail immediately on retry
- Add get_failed_embedding_images command listing failed files + error per folder
- Gallery: amber warning badge on tiles with embedding_status = 'failed'
- BackgroundTasks: fetch and show failed filenames/errors in expanded panel
- Toolbar: conditional 'Failed Embeddings' amber filter pill shown when any
folder has embedding_failed > 0; filters at DB level via new
embedding_failed_only param on get_images / count_images
- TagCloud: replaced vocabulary/dictionary label system with representative
image thumbnails per cluster; results cached in SQLite by image-id hash
Introduces an Explore view with a tag cloud that clusters image embeddings
using cosine k-means and labels clusters via vocabulary-nearest-neighbour
CLIP matching. Vocabulary embeddings are disk-cached (FNV hash-keyed) to
avoid redundant inference. Enables CUDA for candle dependencies and adds a
build.rs check that surfaces a clear error when the toolkit is missing.
- add semantic text search with toolbar mode switching and sqlite-vec query support
- improve embedding progress visibility, failure recovery, and similar-image affordances
- add search clearing and keyboard controls for filename vs semantic search modes
- refine background task interactions and gallery/lightbox embedding states
Refs: #4
- add Candle + HF Hub CLIP image embedding pipeline with background embedding worker
- write image embeddings into sqlite-vec and expose similar-image lookup through a new backend command
- surface embedding progress and recovery in the UI, including retries for failed embeddings
- improve gallery/lightbox embedding UX and make similar-image actions directly accessible
Refs: #3, #4
Sets up Tauri v2 + React frontend with SQLite metadata store,
sqlite-vec for CLIP embedding infrastructure, and r2d2 connection
pool replacing the single Arc<Mutex<Connection>>. Indexer now uses
rayon for parallel file metadata collection.