add thumbnail disk cache, sort order, and date range filter

- Thumbnail disk cache: scaled PNGs persisted under ~/.cache/orc-gallery/thumbs/
  keyed by MD5(path+filesize); avoids re-decode/re-scale on gallery reload
- SortOrder enum (DateNewest/Oldest, NameAsc/Desc) threaded through
  DatabaseManager, ImageGallery, and MainWindow; SQL ORDER BY generated
  by sortClause() helper
- Date range filter: QDateEdit pair in filter toolbar with special-value
  "Any" sentinel; dateWhereFragment() omits WHERE clause when dates are
  invalid (no-filter); QSignalBlocker used in handleClearDates for atomic
  reset without double-refresh
- Cache keys updated to encode sort+date+text via makeSearchKey()
- plan.md updated with new features, corrected API signatures, and GUI
  layout diagram

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yzinchuk
2026-07-03 04:19:04 -04:00
parent 55454d7ef6
commit 850864dc58
7 changed files with 406 additions and 268 deletions
+23 -8
View File
@@ -159,13 +159,17 @@ Central controller. Inherits `QMainWindow`.
Responsibilities:
- Menu bar: File → Open, Settings, Update Database, Quit
- Search bar (`QLineEdit`) with a 500 ms typing-inactivity timer (`m_typingTimer`) — search only fires after the user stops typing
- Search bar (`QLineEdit`) with a 150 ms typing-inactivity timer (`m_typingTimer`) — search only fires after the user stops typing
- Sort/filter toolbar row: sort combo (`QComboBox`) + From/To date pickers (`QDateEdit`) + Clear button
- Animated "Searching…" status bar indicator (`m_searchingAnimationTimer`)
- Instantiates and owns `DatabaseManager` and `ImageGallery`
- Reads/writes settings via `QSettings` (INI at `~/.config/OrcGallery/settings.ini`)
- On search complete: updates status bar with result count
- On clear search: reloads all images (first page)
Sort options: Newest First (default), Oldest First, Name A-Z, Name Z-A.
Date filter: "Any" (special value = minimum date, treated as no-filter) to any specific date. `QSignalBlocker` used in `handleClearDates` to reset both date edits atomically without double-refresh.
Settings keys:
- `databasePath` — path to `.db` file
- `screenshotsDir` — directory scanned by OCR script
@@ -198,17 +202,21 @@ Key public API:
|---|---|
| `initialize(path)` | Opens/validates an existing database |
| `createDatabase(path)` | Creates schema from scratch |
| `getAllImages(offset, limit)` | Paginated full-path list, no search filter |
| `getImageCount()` | Total row count (cached) |
| `searchImages(text, offset, limit)` | FTS5 or LIKE search, runs in background thread via `QtConcurrent::run` |
| `getAllImages(offset, limit, sort, from, to)` | Paginated list with optional sort order and date range |
| `getImageCount()` | Total row count (unfiltered) |
| `searchImages(text, offset, limit, sort, from, to)` | FTS5 or LIKE search, runs async; `SortOrder` enum + `QDate` range |
| `cancelSearch()` | Sets cancellation flag; background thread checks it |
| `initializeFTS()` | Creates virtual table + triggers if FTS5 available |
| `prepareFTSQuery(text)` | Converts user text to FTS5 query syntax |
`SortOrder` enum: `DateNewest` (default), `DateOldest`, `NameAsc`, `NameDesc`.
Static helpers (internal): `sortClause(sort)` → SQL ORDER BY fragment; `dateWhereFragment(from, to)` → SQL WHERE fragment (empty string when dates are invalid/unset); `bindDateParams(query, from, to)` → binds `:from`/`:to`; `makeSearchKey(text, sort, from, to)` → composite string cache key.
Caching:
- `m_searchCache`: `QMap<QString, CachedResult>` keyed by query string, 5-minute TTL
- `m_allImagesCache`: paginated all-images results
- `m_imageCountCache`: row count integer
- `m_searchCache`: `QMap<QString, QMap<QPair<int,int>, SearchCacheItem>>` — outer key is `makeSearchKey(...)` encoding text+sort+date, inner key is `(offset, limit)`, 5-minute TTL
- `m_allImagesCache`: `QMap<QString, QList<ImageItem>>` — key encodes sort+date+offset+limit
- `m_cachedImageCount`: row count integer
- Cache eviction when `m_searchCache` exceeds 50 entries
- All cache access under `m_cacheMutex`
@@ -248,6 +256,9 @@ Behavior:
├───────────────────────────────────────────────────────────┤
│ [ Search OCR text... ] [×] │ ← Search bar
├───────────────────────────────────────────────────────────┤
│ Sort: [Newest First v] From: [ Any ] To: [ Any ] │ ← Filter bar
│ [Clear Dates] │
├───────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │ │ │ │ │ ← Thumbnails
@@ -352,6 +363,7 @@ All three keys have fallbacks defined in `MainWindow::loadSettings()` and `Setti
| SQLite I/O | WAL journal + 20 MB page cache + memory-mapped I/O via PRAGMA on init |
| Thumbnail load stall | `QImage` loaded/scaled on `QThreadPool`, marshalled to main thread via `invokeMethod` |
| Overlay widget cost | Filename drawn in `ImageThumbnail::paintEvent` — no child QFrame/QLabel/QHBoxLayout |
| Disk thumbnail cache | Scaled PNGs cached under `~/.cache/orc-gallery/thumbs/` keyed by MD5(path+size); avoids repeated disk I/O and scaling on gallery reload |
---
@@ -384,6 +396,9 @@ XDG paths used by the application:
- `ImageGallery` does not use a `QAbstractItemModel`; it builds `ImageThumbnail` widgets directly and manages them in a flat list (`m_thumbnails`).
- Database connections must not be shared across threads — each `QtConcurrent` task opens its own named connection and closes it on exit.
- `cancelSearch()` is non-blocking: it sets `m_searchCancelled` and returns immediately. The background thread checks the flag at multiple points and exits early. The destructor sets the flag then calls `waitForFinished()` for clean shutdown.
- The watcher's `finished` signal is not used for emitting results (was causing double-emit). Results are emitted directly from `performSearchInBackground` once and only if `m_currentSearchText == searchText`.
- The watcher's `finished` signal is not used for emitting results (was causing double-emit). Results are emitted directly from `performSearchInBackground` once and only if `m_currentSearchKey == makeSearchKey(searchText, sort, from, to)`.
- `ImageThumbnail::paintEvent` draws the filename overlay; no child widgets needed. The `m_displayName` field is pre-truncated in the constructor.
- Async thumbnail loading: `loadThumbnailAsync` posts to `QThreadPool::globalInstance()`. `QImage` is loaded and `.scaled()` off the main thread; `QPixmap::fromImage()` is called inside a `Qt::QueuedConnection` invokeMethod lambda. A `QPointer<ImageThumbnail>` guard prevents use-after-free if the gallery clears before the load completes.
- Thumbnail disk cache stored at `QStandardPaths::CacheLocation + "/thumbs/"`. Cache key = `QCryptographicHash::Md5(path + "|" + fileSize)` as hex string + `.png`. Avoids reloading/rescaling images that haven't changed.
- Sort/date state is held in `ImageGallery` (`m_sortOrder`, `m_fromDate`, `m_toDate`). `MainWindow` pushes state to the gallery via `setSortOrder()`/`setDateFilter()` before triggering any refresh.
- Date filter uses `QDateEdit::setSpecialValueText(" Any ")` with `minimumDate()` as the sentinel; the gallery receives `QDate()` (invalid) when the date edit is at minimum, which `dateWhereFragment()` treats as "no filter".