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:
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user