- 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
6.5 KiB
Feature Plan: Watchdog + EXIF + Timeline
Branch: feat/watchdog-exif-timeline
Base: merged PR #8 (discovery features) into main
Overview
Three related features being implemented together:
- Phase 1 — EXIF date extraction ✅ DONE (commit
9ee5b08) - Phase 2 — Filesystem watchdog ✅ DONE (commit
ae9e806) - Phase 3 — Timeline view ✅ DONE
Phase 1: EXIF Date Extraction ✅
What it does
Extracts the capture date from EXIF metadata during indexing and stores it as
taken_at (nullable TEXT, ISO 8601) on each image. Exposes "Taken: newest /
oldest" sort options.
Key files changed
src-tauri/Cargo.toml— addedkamadak-exif = "0.5"src-tauri/src/db.rs:- Added
taken_at: Option<String>toImageRecordstruct (aftermodified_at) ensure_columnmigration +idx_images_taken_atindex- Updated
upsert_image,map_image_row(all indices shifted +1 after taken_at), all 3 SELECT statements - Added
taken_asc/taken_descsort cases usingCOALESCE(taken_at, created_at)
- Added
src-tauri/src/indexer.rs:- Added
extract_exif_date(path) -> Option<String>— tries DateTimeOriginal → DateTimeDigitized → DateTime; rejects all-zero sentinel dates ("0000:00:00 00:00:00") build_recordnow populatestaken_at: extract_exif_date(path)
- Added
src/store.ts— addedtaken_at: string | nulltoImageRecord,"taken_desc" | "taken_asc"toSortOrder, sort cases incompareImagesusinga.taken_at ?? a.created_atsrc/components/Toolbar.tsx— added "Taken: newest" / "Taken: oldest" to sort dropdown
Notes
- Existing images won't have
taken_atuntil re-indexed (file_size or mtime must change to trigger re-extraction) upsert_imageusestaken_at = excluded.taken_at(not COALESCE) — if file content changes, fresh EXIF is used
Phase 2: Filesystem Watchdog ✅
What it does
Watches all registered folders using OS-native events (ReadDirectoryChangesW
on Windows). New, modified, and deleted files are reflected automatically
without manual reindexing. Zero CPU when idle.
Key design
- Adaptive blocking:
recv()when no events pending (truly idle), switches torecv_timeout(earliest_deadline)only when debounce timers are running - 500 ms per-path debounce coalesces rapid OS event bursts
- Change detection:
build_record(path, folder_id, existing.as_ref())skips upsert if file_size + mtime unchanged → no thumbnail/embedding re-queues, no metadata clobber Accessevents filtered out (reads don't change content)
Key files changed
src-tauri/Cargo.toml— addednotify = "6"src-tauri/src/db.rs— addedget_indexed_entry_by_pathandget_image_id_by_pathsrc-tauri/src/indexer.rs— addedWatcherHandle,WatcherInner,start_watcher,process_watcher_pathsrc-tauri/src/lib.rs— starts watcher after other workers, managesWatcherHandlein app statesrc-tauri/src/commands.rs—add_folder,remove_folder,update_folder_pathnow acceptState<'_, WatcherHandle>and callwatcher.add_folder / remove_folder / update_foldersrc/store.ts—subscribeToProgresslistens for"watcher-deleted"event (number[]payload), removes images from state, clearsselectedImageif deleted
Phase 3: Timeline View ✅ DONE
What it should do
A new gallery view that groups images by date (year → month → day), virtualised
for performance, using the taken_at / created_at data from Phase 1.
Suggested approach
Backend
- No new Tauri commands needed — images are already sorted by
taken_asc/taken_descand carrytaken_at/created_at - Optionally: a
get_timeline_bucketscommand that returns counts per year/month for a navigation sidebar (nice-to-have, not essential for MVP)
Frontend — grouping logic
- New view type: add
"timeline"toActiveViewtype instore.ts - Grouping function: takes
ImageRecord[], returnsTimelineGroup[]:Useinterface TimelineGroup { label: string; // e.g. "June 2023" dateKey: string; // e.g. "2023-06" for keying images: ImageRecord[]; }taken_at ?? created_atfor the date. Group byYYYY-MM(month granularity works well).
Frontend — virtualised rendering
- Use
@tanstack/react-virtual(already a dependency) - Virtualise at the row level (each row = one group header + a row of tiles, or a row of tiles within a group)
- Simplest pattern: flatten groups into a mixed list of
{ type: 'header', label }and{ type: 'image', image }items, then useuseVirtualizeron that flat list - Tile size uses the existing
tileSizeForZoom/zoomPresetfrom the store
Frontend — navigation
- Sidebar can list the groups (year/month) as anchor links — clicking jumps
virtualizer.scrollToIndex(groupStartIndex) - Or keep it simple for MVP: just scroll, the headers are visible as you go
Frontend — state
sortshould auto-switch to"taken_desc"when entering timeline view (can be auseEffectin the Timeline component)- When leaving timeline view, restore the previous sort
Suggested component structure
src/components/Timeline.tsx
- TimelineView (main component, uses useVirtualizer)
- TimelineGroupHeader (date label row)
- reuses existing Gallery tile/card components
Toolbar
- Add "Timeline" to the view switcher alongside the existing gallery/explore/duplicates tabs
- Or add it to the
Sidebarnav
Existing hooks to reuse
tileSizeForZoom(zoomPreset)for tile sizematchesFiltersfor respecting active folder/media/favorites filters- The existing
Gallerygrid tile component for rendering individual images openImagestore action for lightbox
General notes for a new session
- Always run
feature-dev:code-reviewersweep before every commit — seeMEMORY.md - Use pnpm, never npm
- No
anytypes in TypeScript - Hot reload is active during
pnpm dev:app— don't restart for frontend changes - CUDA
cargo checkfailure is pre-existing (broken nvcc environment) and unrelated to this feature work — filter it out withgrep -v candlewhen checking - The
map_image_rowindb.rsuses positional column indices — any new column added to the SELECT must have its index maintained exactly sqlite-vecvirtual table DML is unreliable inside transactions — vector operations must happen outsideunchecked_transaction()ImageRecordis mirrored indb.rs(Rust) andstore.ts(TypeScript) — both must stay in sync