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".
|
||||
|
||||
+166
-177
@@ -6,12 +6,54 @@
|
||||
#include <QFileInfo>
|
||||
#include <QTimer>
|
||||
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
|
||||
static QString sortClause(DatabaseManager::SortOrder sort)
|
||||
{
|
||||
switch (sort) {
|
||||
case DatabaseManager::SortOrder::DateNewest: return "ORDER BY created_date DESC";
|
||||
case DatabaseManager::SortOrder::DateOldest: return "ORDER BY created_date ASC";
|
||||
case DatabaseManager::SortOrder::NameAsc: return "ORDER BY filename ASC COLLATE NOCASE";
|
||||
case DatabaseManager::SortOrder::NameDesc: return "ORDER BY filename DESC COLLATE NOCASE";
|
||||
}
|
||||
return "ORDER BY created_date DESC";
|
||||
}
|
||||
|
||||
// Returns the extra WHERE fragment and binds params to query.
|
||||
// Caller must append this string after their existing WHERE clause.
|
||||
static QString dateWhereFragment(const QDate &from, const QDate &to)
|
||||
{
|
||||
QString fragment;
|
||||
if (from.isValid())
|
||||
fragment += " AND created_date >= :from_date";
|
||||
if (to.isValid())
|
||||
fragment += " AND created_date <= :to_date";
|
||||
return fragment;
|
||||
}
|
||||
|
||||
static void bindDateParams(QSqlQuery &q, const QDate &from, const QDate &to)
|
||||
{
|
||||
if (from.isValid())
|
||||
q.bindValue(":from_date", from.toString("yyyy-MM-dd") + "T00:00:00");
|
||||
if (to.isValid())
|
||||
q.bindValue(":to_date", to.toString("yyyy-MM-dd") + "T23:59:59.999999");
|
||||
}
|
||||
|
||||
static QString makeSearchKey(const QString &text, DatabaseManager::SortOrder sort,
|
||||
const QDate &from, const QDate &to)
|
||||
{
|
||||
return text + "|" + QString::number(static_cast<int>(sort)) + "|"
|
||||
+ from.toString(Qt::ISODate) + "|" + to.toString(Qt::ISODate);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
DatabaseManager::DatabaseManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_initialized(false)
|
||||
, m_ftsEnabled(false)
|
||||
, m_searchCancelled(false)
|
||||
, m_cachedImageCount(-1) // Initialize to invalid value
|
||||
, m_cachedImageCount(-1)
|
||||
, m_currentOffset(0)
|
||||
, m_currentLimit(0)
|
||||
{
|
||||
@@ -31,10 +73,10 @@ DatabaseManager::DatabaseManager(QObject *parent)
|
||||
|
||||
DatabaseManager::~DatabaseManager()
|
||||
{
|
||||
// Signal cancellation and wait for the background thread to drain cleanly.
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
m_searchCancelled = true;
|
||||
m_currentSearchKey.clear();
|
||||
}
|
||||
if (m_searchFuture.isRunning())
|
||||
m_searchFuture.waitForFinished();
|
||||
@@ -183,76 +225,70 @@ bool DatabaseManager::initialize(const QString &dbPath)
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages(int offset, int limit)
|
||||
QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages(int offset, int limit,
|
||||
SortOrder sort,
|
||||
const QDate &from,
|
||||
const QDate &to)
|
||||
{
|
||||
QList<ImageItem> images;
|
||||
|
||||
if (!m_initialized) {
|
||||
qDebug() << "Database not initialized.";
|
||||
return images;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
QPair<int, int> cacheKey(offset, limit);
|
||||
QMutexLocker cacheLocker(&m_cacheMutex);
|
||||
if (m_allImagesCache.contains(cacheKey)) {
|
||||
// Use cached results if available and not expired
|
||||
if (m_lastCacheUpdate.secsTo(QDateTime::currentDateTime()) < CACHE_LIFETIME_SECS) {
|
||||
|
||||
if (!m_initialized) return images;
|
||||
|
||||
// Cache key encodes all query parameters
|
||||
QString cacheKey = makeSearchKey("", sort, from, to)
|
||||
+ "|" + QString::number(offset)
|
||||
+ "|" + QString::number(limit);
|
||||
{
|
||||
QMutexLocker cacheLocker(&m_cacheMutex);
|
||||
if (m_allImagesCache.contains(cacheKey) &&
|
||||
m_lastCacheUpdate.secsTo(QDateTime::currentDateTime()) < CACHE_LIFETIME_SECS) {
|
||||
return m_allImagesCache[cacheKey];
|
||||
}
|
||||
}
|
||||
cacheLocker.unlock();
|
||||
|
||||
// Verify database is still connected
|
||||
|
||||
if (!m_db.isOpen() && !m_db.open()) {
|
||||
qDebug() << "Database connection lost and cannot be reopened:" << m_db.lastError().text();
|
||||
m_initialized = false;
|
||||
return images;
|
||||
}
|
||||
|
||||
// Start transaction to speed up query
|
||||
m_db.transaction();
|
||||
|
||||
m_db.transaction();
|
||||
QSqlQuery query;
|
||||
|
||||
QString dateExtra = dateWhereFragment(from, to);
|
||||
QString whereClause = dateExtra.isEmpty() ? "" : ("WHERE 1=1" + dateExtra);
|
||||
|
||||
QString sql = "SELECT id, full_path, ocr_text FROM ocr_results "
|
||||
+ whereClause + " " + sortClause(sort);
|
||||
if (limit > 0)
|
||||
sql += " LIMIT :limit OFFSET :offset";
|
||||
|
||||
query.prepare(sql);
|
||||
bindDateParams(query, from, to);
|
||||
if (limit > 0) {
|
||||
// Use pagination
|
||||
query.prepare("SELECT id, full_path, ocr_text FROM ocr_results ORDER BY id LIMIT :limit OFFSET :offset");
|
||||
query.bindValue(":limit", limit);
|
||||
query.bindValue(":offset", offset);
|
||||
} else {
|
||||
// Get all results
|
||||
query.prepare("SELECT id, full_path, ocr_text FROM ocr_results ORDER BY id");
|
||||
}
|
||||
|
||||
|
||||
if (!query.exec()) {
|
||||
qDebug() << "Failed to fetch images:" << query.lastError().text();
|
||||
qDebug() << "getAllImages failed:" << query.lastError().text();
|
||||
m_db.rollback();
|
||||
return images;
|
||||
}
|
||||
|
||||
// Reserve space for results to avoid reallocations
|
||||
images.reserve(query.size() > 0 ? query.size() : 100);
|
||||
|
||||
images.reserve(100);
|
||||
while (query.next()) {
|
||||
ImageItem item;
|
||||
item.id = query.value(0).toInt();
|
||||
item.id = query.value(0).toInt();
|
||||
item.filePath = query.value(1).toString();
|
||||
item.ocrText = query.value(2).toString();
|
||||
|
||||
// Only add images that have a non-empty path
|
||||
if (!item.filePath.isEmpty()) {
|
||||
item.ocrText = query.value(2).toString();
|
||||
if (!item.filePath.isEmpty())
|
||||
images.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
m_db.commit();
|
||||
|
||||
// Update cache
|
||||
cacheLocker.relock();
|
||||
|
||||
QMutexLocker cacheLocker(&m_cacheMutex);
|
||||
m_allImagesCache[cacheKey] = images;
|
||||
m_lastCacheUpdate = QDateTime::currentDateTime();
|
||||
cacheLocker.unlock();
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
@@ -325,151 +361,137 @@ QSqlDatabase DatabaseManager::getDatabaseConnection()
|
||||
return threadDb;
|
||||
}
|
||||
|
||||
void DatabaseManager::searchImages(const QString &searchText, int offset, int limit)
|
||||
void DatabaseManager::searchImages(const QString &searchText, int offset, int limit,
|
||||
SortOrder sort, const QDate &from, const QDate &to)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
qDebug() << "Database not initialized.";
|
||||
emit searchResultsReady(QList<ImageItem>(), searchText, offset, limit, 0);
|
||||
emit searchResultsReady({}, searchText, offset, limit, 0, sort, from, to);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify database is still connected
|
||||
if (!m_db.isOpen() && !m_db.open()) {
|
||||
qDebug() << "Database connection lost and cannot be reopened:" << m_db.lastError().text();
|
||||
m_initialized = false;
|
||||
emit searchResultsReady(QList<ImageItem>(), searchText, offset, limit, 0);
|
||||
emit searchResultsReady({}, searchText, offset, limit, 0, sort, from, to);
|
||||
return;
|
||||
}
|
||||
|
||||
// If search text is empty, return all images
|
||||
if (searchText.isEmpty()) {
|
||||
// For empty search, return all images with pagination
|
||||
QList<ImageItem> allImages = getAllImages(offset, limit);
|
||||
QList<ImageItem> allImages = getAllImages(offset, limit, sort, from, to);
|
||||
int totalCount = getImageCount();
|
||||
|
||||
emit searchResultsReady(allImages, searchText, offset, limit, totalCount);
|
||||
emit searchResultsReady(allImages, searchText, offset, limit, totalCount, sort, from, to);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have a cached result for this search query
|
||||
// Check cache — key encodes text + sort + date + pagination
|
||||
const QString outerKey = makeSearchKey(searchText, sort, from, to);
|
||||
const QPair<int,int> pageKey(offset, limit);
|
||||
{
|
||||
QMutexLocker locker(&m_cacheMutex);
|
||||
QPair<int, int> cacheKey(offset, limit);
|
||||
if (m_searchCache.contains(searchText) &&
|
||||
m_searchCache[searchText].contains(cacheKey)) {
|
||||
|
||||
SearchCacheItem cacheItem = m_searchCache[searchText][cacheKey];
|
||||
|
||||
// Check if cache is still valid
|
||||
if (cacheItem.timestamp.secsTo(QDateTime::currentDateTime()) < CACHE_LIFETIME_SECS) {
|
||||
emit searchResultsReady(cacheItem.results, searchText, offset, limit, cacheItem.totalCount);
|
||||
if (m_searchCache.contains(outerKey) && m_searchCache[outerKey].contains(pageKey)) {
|
||||
const SearchCacheItem &ci = m_searchCache[outerKey][pageKey];
|
||||
if (ci.timestamp.secsTo(QDateTime::currentDateTime()) < CACHE_LIFETIME_SECS) {
|
||||
emit searchResultsReady(ci.results, searchText, offset, limit,
|
||||
ci.totalCount, sort, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No delay needed since we're using typing inactivity timer in MainWindow
|
||||
|
||||
// Cancel any ongoing search before starting a new one
|
||||
cancelSearch();
|
||||
|
||||
// Store the current search parameters safely
|
||||
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
m_currentSearchText = searchText;
|
||||
m_currentOffset = offset;
|
||||
m_currentLimit = limit;
|
||||
m_searchCancelled = false;
|
||||
m_currentSearchKey = outerKey;
|
||||
m_currentOffset = offset;
|
||||
m_currentLimit = limit;
|
||||
m_searchCancelled = false;
|
||||
}
|
||||
// The signal is now emitted before starting the thread to ensure UI responsiveness
|
||||
|
||||
// Start the search operation in a background thread
|
||||
m_searchFuture = QtConcurrent::run([this, searchText, offset, limit]() {
|
||||
performSearchInBackground(searchText, offset, limit);
|
||||
|
||||
m_searchFuture = QtConcurrent::run([this, searchText, offset, limit, sort, from, to]() {
|
||||
performSearchInBackground(searchText, offset, limit, sort, from, to);
|
||||
});
|
||||
|
||||
// Show immediate feedback that search is starting
|
||||
|
||||
emit searchStarted(searchText);
|
||||
|
||||
m_searchWatcher.setFuture(m_searchFuture);
|
||||
}
|
||||
|
||||
void DatabaseManager::cancelSearch()
|
||||
{
|
||||
// Non-blocking: just set the flag. The background thread checks it at
|
||||
// multiple points and exits early. The new search starts immediately.
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
m_searchCancelled = true;
|
||||
m_currentSearchText.clear();
|
||||
m_currentSearchKey.clear();
|
||||
}
|
||||
|
||||
void DatabaseManager::performSearchInBackground(const QString &searchText, int offset, int limit)
|
||||
void DatabaseManager::performSearchInBackground(const QString &searchText, int offset, int limit,
|
||||
SortOrder sort, const QDate &from, const QDate &to)
|
||||
{
|
||||
QList<ImageItem> images;
|
||||
|
||||
// Get a thread-specific database connection
|
||||
const QString searchKey = makeSearchKey(searchText, sort, from, to);
|
||||
|
||||
QSqlDatabase threadDb = getDatabaseConnection();
|
||||
|
||||
if (!threadDb.isOpen() && !threadDb.open()) {
|
||||
qDebug() << "Thread database connection failed:" << threadDb.lastError().text();
|
||||
qDebug() << "Thread DB connection failed:" << threadDb.lastError().text();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if search was cancelled
|
||||
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
if (m_searchCancelled || m_currentSearchText != searchText) {
|
||||
return;
|
||||
}
|
||||
if (m_searchCancelled || m_currentSearchKey != searchKey) return;
|
||||
}
|
||||
int totalCount = 0;
|
||||
|
||||
// Check if search was cancelled before main query
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
if (m_searchCancelled || m_currentSearchText != searchText) {
|
||||
if (m_searchCancelled || m_currentSearchKey != makeSearchKey(searchText, sort, from, to)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Single query: COUNT(*) OVER() gives the total match count without a
|
||||
// separate round-trip. SQLite evaluates window functions before LIMIT, so
|
||||
// the count reflects the full result set even when we page with LIMIT/OFFSET.
|
||||
// Single query: COUNT(*) OVER() gives total match count without a separate round-trip.
|
||||
threadDb.transaction();
|
||||
QSqlQuery query(threadDb);
|
||||
const QString dateExtra = dateWhereFragment(from, to);
|
||||
|
||||
if (m_ftsEnabled) {
|
||||
QString ftsQuery = prepareFTSQuery(searchText);
|
||||
QString queryStr =
|
||||
// For FTS5: date filter goes into a subquery so it combines with MATCH cleanly
|
||||
QString ftsQ = prepareFTSQuery(searchText);
|
||||
// When user picks a sort other than relevance, use that column; otherwise rank
|
||||
QString orderBy = (sort == SortOrder::DateNewest || sort == SortOrder::DateOldest
|
||||
|| sort == SortOrder::NameAsc || sort == SortOrder::NameDesc)
|
||||
? sortClause(sort)
|
||||
: "ORDER BY rank";
|
||||
|
||||
QString sql =
|
||||
"SELECT r.id, r.full_path, r.ocr_text, COUNT(*) OVER() AS total_count "
|
||||
"FROM ocr_results r "
|
||||
"JOIN ocr_fts f ON r.id = f.rowid "
|
||||
"WHERE ocr_fts MATCH :query "
|
||||
"ORDER BY rank";
|
||||
if (limit > 0)
|
||||
queryStr += " LIMIT :limit OFFSET :offset";
|
||||
"WHERE ocr_fts MATCH :query" + dateExtra + " " + orderBy;
|
||||
if (limit > 0) sql += " LIMIT :limit OFFSET :offset";
|
||||
|
||||
query.prepare(queryStr);
|
||||
query.bindValue(":query", ftsQuery);
|
||||
query.prepare(sql);
|
||||
query.bindValue(":query", ftsQ);
|
||||
bindDateParams(query, from, to);
|
||||
if (limit > 0) {
|
||||
query.bindValue(":limit", limit);
|
||||
query.bindValue(":offset", offset);
|
||||
}
|
||||
} else {
|
||||
QString whereClause = (searchText.length() <= 3)
|
||||
QString textWhere = (searchText.length() <= 3)
|
||||
? "ocr_text LIKE :search"
|
||||
: "ocr_text LIKE :search OR ocr_text LIKE :wordstart";
|
||||
|
||||
QString queryStr =
|
||||
QString sql =
|
||||
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
|
||||
"FROM ocr_results WHERE " + whereClause + " ORDER BY id";
|
||||
if (limit > 0)
|
||||
queryStr += " LIMIT :limit OFFSET :offset";
|
||||
"FROM ocr_results WHERE (" + textWhere + ")" + dateExtra
|
||||
+ " " + sortClause(sort);
|
||||
if (limit > 0) sql += " LIMIT :limit OFFSET :offset";
|
||||
|
||||
query.prepare(queryStr);
|
||||
query.prepare(sql);
|
||||
query.bindValue(":search", "%" + searchText + "%");
|
||||
if (searchText.length() > 3)
|
||||
query.bindValue(":wordstart", "% " + searchText + "%");
|
||||
bindDateParams(query, from, to);
|
||||
if (limit > 0) {
|
||||
query.bindValue(":limit", limit);
|
||||
query.bindValue(":offset", offset);
|
||||
@@ -479,103 +501,71 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
|
||||
if (!query.exec()) {
|
||||
qDebug() << "Search query failed:" << query.lastError().text();
|
||||
threadDb.rollback();
|
||||
|
||||
// FTS failure: fall back to LIKE for this request
|
||||
if (m_ftsEnabled) {
|
||||
qDebug() << "Falling back to LIKE query";
|
||||
qDebug() << "Falling back to LIKE";
|
||||
threadDb.transaction();
|
||||
query.prepare(
|
||||
QString sql =
|
||||
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
|
||||
"FROM ocr_results WHERE ocr_text LIKE :search ORDER BY id");
|
||||
"FROM ocr_results WHERE ocr_text LIKE :search" + dateExtra
|
||||
+ " " + sortClause(sort);
|
||||
if (limit > 0) sql += " LIMIT :limit OFFSET :offset";
|
||||
query.prepare(sql);
|
||||
query.bindValue(":search", "%" + searchText + "%");
|
||||
if (!query.exec()) {
|
||||
qDebug() << "Fallback query failed:" << query.lastError().text();
|
||||
threadDb.rollback();
|
||||
return;
|
||||
}
|
||||
bindDateParams(query, from, to);
|
||||
if (limit > 0) { query.bindValue(":limit", limit); query.bindValue(":offset", offset); }
|
||||
if (!query.exec()) { threadDb.rollback(); return; }
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check cancellation again after query execution
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
if (m_searchCancelled || m_currentSearchText != searchText) {
|
||||
threadDb.rollback();
|
||||
return;
|
||||
}
|
||||
if (m_searchCancelled || m_currentSearchKey != searchKey) { threadDb.rollback(); return; }
|
||||
}
|
||||
|
||||
images.reserve(100);
|
||||
|
||||
while (query.next()) {
|
||||
if (query.at() % 20 == 0) {
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
if (m_searchCancelled || m_currentSearchText != searchText) {
|
||||
threadDb.rollback();
|
||||
return;
|
||||
}
|
||||
if (m_searchCancelled || m_currentSearchKey != searchKey) { threadDb.rollback(); return; }
|
||||
}
|
||||
|
||||
// Read total count from the window function column (same on every row)
|
||||
if (totalCount == 0)
|
||||
totalCount = query.value(3).toInt();
|
||||
|
||||
if (totalCount == 0) totalCount = query.value(3).toInt();
|
||||
ImageItem item;
|
||||
item.id = query.value(0).toInt();
|
||||
item.filePath = query.value(1).toString();
|
||||
item.ocrText = query.value(2).toString();
|
||||
|
||||
if (!item.filePath.isEmpty())
|
||||
images.append(item);
|
||||
if (!item.filePath.isEmpty()) images.append(item);
|
||||
}
|
||||
|
||||
threadDb.commit();
|
||||
|
||||
// Check if search was cancelled before storing results
|
||||
{
|
||||
QMutexLocker locker(&m_searchMutex);
|
||||
if (m_searchCancelled || m_currentSearchText != searchText) {
|
||||
if (m_searchCancelled || m_currentSearchKey != makeSearchKey(searchText, sort, from, to)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result for future queries and emit signal with the results
|
||||
QMutexLocker locker(&m_cacheMutex);
|
||||
|
||||
// Create cache item with results and metadata
|
||||
SearchCacheItem cacheItem;
|
||||
cacheItem.results = images;
|
||||
cacheItem.totalCount = totalCount;
|
||||
cacheItem.timestamp = QDateTime::currentDateTime();
|
||||
|
||||
// If this is the first query for this search text, create a new map
|
||||
if (!m_searchCache.contains(searchText)) {
|
||||
m_searchCache.insert(searchText, QMap<QPair<int, int>, SearchCacheItem>());
|
||||
|
||||
// Store in cache
|
||||
{
|
||||
QMutexLocker locker(&m_cacheMutex);
|
||||
SearchCacheItem ci;
|
||||
ci.results = images;
|
||||
ci.totalCount = totalCount;
|
||||
ci.timestamp = QDateTime::currentDateTime();
|
||||
if (!m_searchCache.contains(searchKey))
|
||||
m_searchCache.insert(searchKey, {});
|
||||
m_searchCache[searchKey].insert({offset, limit}, ci);
|
||||
if (m_searchCache.size() > MAX_CACHE_SIZE)
|
||||
m_searchCache.remove(m_searchCache.firstKey());
|
||||
}
|
||||
|
||||
// Store results for this specific offset/limit combination
|
||||
QPair<int, int> cacheKey(offset, limit);
|
||||
m_searchCache[searchText].insert(cacheKey, cacheItem);
|
||||
|
||||
// Limit cache size to avoid memory issues
|
||||
if (m_searchCache.size() > MAX_CACHE_SIZE) {
|
||||
// Remove the oldest entry
|
||||
if (!m_searchCache.isEmpty()) {
|
||||
QString oldestKey = m_searchCache.firstKey();
|
||||
m_searchCache.remove(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Release mutex before emitting signal
|
||||
locker.unlock();
|
||||
|
||||
// Emit signal with results, including pagination info
|
||||
|
||||
// Emit only if this search is still current
|
||||
QMutexLocker searchLocker(&m_searchMutex);
|
||||
if (!m_searchCancelled && m_currentSearchText == searchText) {
|
||||
if (!m_searchCancelled && m_currentSearchKey == searchKey) {
|
||||
searchLocker.unlock();
|
||||
emit searchResultsReady(images, searchText, offset, limit, totalCount);
|
||||
emit searchResultsReady(images, searchText, offset, limit, totalCount, sort, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +576,6 @@ void DatabaseManager::cleanupCache()
|
||||
// Get current time
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
// Expire old search cache items
|
||||
QMutableMapIterator<QString, QMap<QPair<int, int>, SearchCacheItem>> i(m_searchCache);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
+27
-38
@@ -13,6 +13,7 @@
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
#include <QDate>
|
||||
|
||||
#include <QtConcurrent>
|
||||
#include <QFuture>
|
||||
@@ -28,13 +29,17 @@ class DatabaseManager : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Struct to represent an image item with its path and OCR text
|
||||
*/
|
||||
enum class SortOrder {
|
||||
DateNewest, // ORDER BY created_date DESC (default)
|
||||
DateOldest, // ORDER BY created_date ASC
|
||||
NameAsc, // ORDER BY filename ASC
|
||||
NameDesc // ORDER BY filename DESC
|
||||
};
|
||||
|
||||
struct ImageItem {
|
||||
QString filePath;
|
||||
QString ocrText;
|
||||
int id; // Add id to support pagination
|
||||
int id;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,44 +74,28 @@ public:
|
||||
* @param limit Maximum number of items to return, 0 means no limit
|
||||
* @return List of ImageItem objects
|
||||
*/
|
||||
QList<ImageItem> getAllImages(int offset = 0, int limit = 0);
|
||||
QList<ImageItem> getAllImages(int offset = 0, int limit = 0,
|
||||
SortOrder sort = SortOrder::DateNewest,
|
||||
const QDate &from = QDate(),
|
||||
const QDate &to = QDate());
|
||||
|
||||
/**
|
||||
* @brief Get total count of all images in the database
|
||||
* @return Total number of images
|
||||
*/
|
||||
int getImageCount();
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Search for images matching the search text
|
||||
* @param searchText Text to search for in OCR results
|
||||
* @param offset Starting position (0-based) for pagination
|
||||
* @param limit Maximum number of items to return, 0 means no limit
|
||||
*/
|
||||
void searchImages(const QString &searchText, int offset = 0, int limit = 0);
|
||||
void searchImages(const QString &searchText, int offset = 0, int limit = 0,
|
||||
SortOrder sort = SortOrder::DateNewest,
|
||||
const QDate &from = QDate(),
|
||||
const QDate &to = QDate());
|
||||
|
||||
/**
|
||||
* @brief Cancel any ongoing search operations
|
||||
*/
|
||||
void cancelSearch();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Signal emitted when search results are available
|
||||
* @param results The list of image items matching the search
|
||||
* @param searchText The original search text that produced these results
|
||||
* @param offset The offset used for this result set
|
||||
* @param limit The limit used for this result set
|
||||
* @param totalCount The total number of results matching the search (regardless of pagination)
|
||||
*/
|
||||
void searchResultsReady(const QList<DatabaseManager::ImageItem> &results, const QString &searchText,
|
||||
int offset, int limit, int totalCount);
|
||||
void searchResultsReady(const QList<DatabaseManager::ImageItem> &results,
|
||||
const QString &searchText,
|
||||
int offset, int limit, int totalCount,
|
||||
DatabaseManager::SortOrder sort,
|
||||
const QDate &from, const QDate &to);
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when search operation starts
|
||||
* @param searchText The search text being processed
|
||||
*/
|
||||
void searchStarted(const QString &searchText);
|
||||
|
||||
private:
|
||||
@@ -125,15 +114,15 @@ private:
|
||||
QMap<QString, QMap<QPair<int, int>, SearchCacheItem>> m_searchCache; // searchText -> {(offset, limit) -> results}
|
||||
QMutex m_cacheMutex; // Mutex to protect cache access from multiple threads
|
||||
|
||||
// All images cache with pagination
|
||||
QMap<QPair<int, int>, QList<ImageItem>> m_allImagesCache; // (offset, limit) -> results
|
||||
// All images cache — key encodes sort+date+offset+limit
|
||||
QMap<QString, QList<ImageItem>> m_allImagesCache;
|
||||
int m_cachedImageCount; // Total image count cache
|
||||
QDateTime m_lastCacheUpdate; // When the cache was last updated
|
||||
|
||||
// Search thread management
|
||||
QFuture<void> m_searchFuture;
|
||||
QFutureWatcher<void> m_searchWatcher;
|
||||
QString m_currentSearchText;
|
||||
QString m_currentSearchKey; // encoded: text|sort|from|to
|
||||
int m_currentOffset;
|
||||
int m_currentLimit;
|
||||
bool m_searchCancelled;
|
||||
@@ -152,8 +141,8 @@ private:
|
||||
// Prepare FTS query with proper syntax
|
||||
QString prepareFTSQuery(const QString &searchText);
|
||||
|
||||
// Perform search operation in background thread
|
||||
void performSearchInBackground(const QString &searchText, int offset, int limit);
|
||||
void performSearchInBackground(const QString &searchText, int offset, int limit,
|
||||
SortOrder sort, const QDate &from, const QDate &to);
|
||||
|
||||
// Clear expired cache items
|
||||
void cleanupCache();
|
||||
|
||||
+63
-25
@@ -10,6 +10,9 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QTimer>
|
||||
#include <QScrollBar>
|
||||
#include <QStandardPaths>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
|
||||
|
||||
|
||||
@@ -154,8 +157,9 @@ void ImageGallery::setDatabaseManager(DatabaseManager *dbManager)
|
||||
|
||||
// Connect to the DatabaseManager signals
|
||||
if (m_dbManager) {
|
||||
connect(m_dbManager, &DatabaseManager::searchResultsReady,
|
||||
connect(m_dbManager, &DatabaseManager::searchResultsReady,
|
||||
this, &ImageGallery::handleSearchResults);
|
||||
|
||||
connect(m_dbManager, &DatabaseManager::searchStarted,
|
||||
this, &ImageGallery::handleSearchStarted);
|
||||
}
|
||||
@@ -314,17 +318,19 @@ void ImageGallery::handleSearchTextChanged(const QString &searchText)
|
||||
m_loadingIndicator->show();
|
||||
m_gridLayout->addWidget(m_loadingIndicator, 0, 0, 1, m_columnsCount);
|
||||
|
||||
// Initiate the threaded search with pagination
|
||||
m_dbManager->searchImages(searchText, m_currentOffset, m_pageSize);
|
||||
m_dbManager->searchImages(searchText, m_currentOffset, m_pageSize,
|
||||
m_sortOrder, m_fromDate, m_toDate);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageGallery::handleSearchResults(const QList<DatabaseManager::ImageItem> &results,
|
||||
const QString &searchText, int offset, int limit,
|
||||
int totalCount)
|
||||
void ImageGallery::handleSearchResults(const QList<DatabaseManager::ImageItem> &results,
|
||||
const QString &searchText, int offset, int limit,
|
||||
int totalCount, DatabaseManager::SortOrder sort,
|
||||
const QDate &from, const QDate &to)
|
||||
{
|
||||
// Only update display if this is the result for the most recent search
|
||||
if (searchText == m_lastSearchQuery) {
|
||||
// Discard stale results from a superseded search / sort / filter
|
||||
if (searchText == m_lastSearchQuery && sort == m_sortOrder
|
||||
&& from == m_fromDate && to == m_toDate) {
|
||||
m_isLoading = false;
|
||||
m_totalCount = totalCount;
|
||||
|
||||
@@ -416,8 +422,8 @@ void ImageGallery::loadMoreImages()
|
||||
int row = (m_thumbnails.size() / m_columnsCount);
|
||||
m_gridLayout->addWidget(m_loadingIndicator, row + 1, 0, 1, m_columnsCount);
|
||||
|
||||
// Request next page from database
|
||||
m_dbManager->searchImages(m_lastSearchQuery, m_currentOffset, m_pageSize);
|
||||
m_dbManager->searchImages(m_lastSearchQuery, m_currentOffset, m_pageSize,
|
||||
m_sortOrder, m_fromDate, m_toDate);
|
||||
}
|
||||
|
||||
// Handle scroll to bottom event
|
||||
@@ -535,29 +541,61 @@ void ImageGallery::handleThumbnailClicked(const QString &filePath)
|
||||
QApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
void ImageGallery::setSortOrder(DatabaseManager::SortOrder sort)
|
||||
{
|
||||
m_sortOrder = sort;
|
||||
}
|
||||
|
||||
void ImageGallery::setDateFilter(const QDate &from, const QDate &to)
|
||||
{
|
||||
m_fromDate = from;
|
||||
m_toDate = to;
|
||||
}
|
||||
|
||||
// Returns the on-disk cache directory, creating it if needed.
|
||||
static QString thumbnailCacheDir()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ "/thumbnails/";
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Cache key: MD5 of "filePath|mtime" so stale entries are naturally bypassed
|
||||
// when the source file is modified.
|
||||
static QString thumbnailCacheKey(const QString &filePath)
|
||||
{
|
||||
QFileInfo fi(filePath);
|
||||
QByteArray raw = (filePath + "|" +
|
||||
QString::number(fi.lastModified().toSecsSinceEpoch())).toUtf8();
|
||||
return QCryptographicHash::hash(raw, QCryptographicHash::Md5).toHex() + ".png";
|
||||
}
|
||||
|
||||
void ImageGallery::loadThumbnailAsync(ImageThumbnail *label, const QString &filePath)
|
||||
{
|
||||
// QImage loading and scaling are thread-safe; QPixmap creation must happen
|
||||
// on the main thread. We do the heavy work in a background thread and marshal
|
||||
// the result back via invokeMethod so the main thread never stalls on disk I/O.
|
||||
QPointer<ImageThumbnail> ptr(label);
|
||||
|
||||
QThreadPool::globalInstance()->start([this, ptr, filePath]() {
|
||||
QImage image;
|
||||
QFileInfo fi(filePath);
|
||||
if (fi.exists() && fi.isReadable()) {
|
||||
image = QImage(filePath);
|
||||
if (!image.isNull())
|
||||
image = image.scaled(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
// 1. Try disk cache (already scaled PNG — much faster than re-decoding source)
|
||||
const QString cacheFile = thumbnailCacheDir() + thumbnailCacheKey(filePath);
|
||||
QImage image(cacheFile);
|
||||
|
||||
if (image.isNull()) {
|
||||
// 2. Cache miss: load source, scale, write cache entry
|
||||
QFileInfo fi(filePath);
|
||||
if (fi.exists() && fi.isReadable()) {
|
||||
image = QImage(filePath);
|
||||
if (!image.isNull()) {
|
||||
image = image.scaled(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
image.save(cacheFile, "PNG");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Marshal QPixmap creation back to the main thread
|
||||
QMetaObject::invokeMethod(this, [ptr, image]() {
|
||||
if (!ptr) return;
|
||||
if (image.isNull()) {
|
||||
// File missing or unreadable — placeholder already shown, nothing to do
|
||||
return;
|
||||
}
|
||||
if (!ptr || image.isNull()) return;
|
||||
ptr->setPixmap(QPixmap::fromImage(image));
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
|
||||
+13
-4
@@ -14,6 +14,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QPointer>
|
||||
#include <QThreadPool>
|
||||
#include <QDate>
|
||||
#include "databasemanager.h"
|
||||
|
||||
// Global constants
|
||||
@@ -63,10 +64,15 @@ public slots:
|
||||
void handleThumbnailClicked(const QString &filePath);
|
||||
void handleContainerResized(); // New slot to handle resize events
|
||||
void updateGridLayout(); // Adjusts grid based on current window size
|
||||
void handleSearchResults(const QList<DatabaseManager::ImageItem> &results, const QString &searchText,
|
||||
int offset, int limit, int totalCount);
|
||||
void handleSearchResults(const QList<DatabaseManager::ImageItem> &results,
|
||||
const QString &searchText, int offset, int limit, int totalCount,
|
||||
DatabaseManager::SortOrder sort, const QDate &from, const QDate &to);
|
||||
void handleSearchStarted(const QString &searchText);
|
||||
void updateSearchTextDisplay(const QString &searchText); // Update UI with search text without performing search
|
||||
void updateSearchTextDisplay(const QString &searchText);
|
||||
|
||||
// Sort / filter setters — call then refresh to apply
|
||||
void setSortOrder(DatabaseManager::SortOrder sort);
|
||||
void setDateFilter(const QDate &from, const QDate &to);
|
||||
void handleScrolledToBottom();
|
||||
void handleLoadMoreClicked();
|
||||
|
||||
@@ -94,7 +100,10 @@ private:
|
||||
|
||||
void loadThumbnailAsync(ImageThumbnail *label, const QString &filePath);
|
||||
QPixmap createPlaceholderThumbnail(int width, int height, const QString &message);
|
||||
QString m_lastSearchQuery; // Track the last search text
|
||||
QString m_lastSearchQuery;
|
||||
DatabaseManager::SortOrder m_sortOrder = DatabaseManager::SortOrder::DateNewest;
|
||||
QDate m_fromDate;
|
||||
QDate m_toDate;
|
||||
|
||||
// Track scroll position
|
||||
int m_lastScrollPosition;
|
||||
|
||||
+103
-16
@@ -94,7 +94,8 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
connect(m_dbManager, &DatabaseManager::searchResultsReady,
|
||||
this, [this](const QList<DatabaseManager::ImageItem> &results, const QString &text,
|
||||
int offset, int limit, int totalCount) {
|
||||
int offset, int limit, int totalCount,
|
||||
DatabaseManager::SortOrder /*sort*/, const QDate & /*from*/, const QDate & /*to*/) {
|
||||
if (text.isEmpty()) {
|
||||
statusBar()->showMessage(tr("Showing all images (paginated) - %1 total").arg(totalCount));
|
||||
} else {
|
||||
@@ -124,6 +125,16 @@ MainWindow::~MainWindow()
|
||||
// All QObjects will be automatically deleted via parent-child relationship
|
||||
}
|
||||
|
||||
static DatabaseManager::SortOrder sortFromIndex(int idx)
|
||||
{
|
||||
switch (idx) {
|
||||
case 1: return DatabaseManager::SortOrder::DateOldest;
|
||||
case 2: return DatabaseManager::SortOrder::NameAsc;
|
||||
case 3: return DatabaseManager::SortOrder::NameDesc;
|
||||
default: return DatabaseManager::SortOrder::DateNewest;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::createLayout()
|
||||
{
|
||||
// Create central widget
|
||||
@@ -187,12 +198,56 @@ void MainWindow::createLayout()
|
||||
m_imageGallery = new ImageGallery(this);
|
||||
m_imageGallery->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
// Filter row: sort + date range + clear
|
||||
QHBoxLayout *filterLayout = new QHBoxLayout();
|
||||
filterLayout->setSpacing(6);
|
||||
|
||||
QLabel *sortLabel = new QLabel(tr("Sort:"), this);
|
||||
m_sortCombo = new QComboBox(this);
|
||||
m_sortCombo->addItem(tr("Newest First"));
|
||||
m_sortCombo->addItem(tr("Oldest First"));
|
||||
m_sortCombo->addItem(tr("Name A-Z"));
|
||||
m_sortCombo->addItem(tr("Name Z-A"));
|
||||
|
||||
QLabel *fromLabel = new QLabel(tr("From:"), this);
|
||||
m_fromDateEdit = new QDateEdit(this);
|
||||
m_fromDateEdit->setCalendarPopup(true);
|
||||
m_fromDateEdit->setMinimumDate(QDate(1970, 1, 1));
|
||||
m_fromDateEdit->setDate(m_fromDateEdit->minimumDate());
|
||||
m_fromDateEdit->setSpecialValueText(tr(" Any "));
|
||||
m_fromDateEdit->setDisplayFormat("yyyy-MM-dd");
|
||||
|
||||
QLabel *toLabel = new QLabel(tr("To:"), this);
|
||||
m_toDateEdit = new QDateEdit(this);
|
||||
m_toDateEdit->setCalendarPopup(true);
|
||||
m_toDateEdit->setMinimumDate(QDate(1970, 1, 1));
|
||||
m_toDateEdit->setDate(m_toDateEdit->minimumDate());
|
||||
m_toDateEdit->setSpecialValueText(tr(" Any "));
|
||||
m_toDateEdit->setDisplayFormat("yyyy-MM-dd");
|
||||
|
||||
m_clearDatesBtn = new QPushButton(tr("Clear Dates"), this);
|
||||
|
||||
filterLayout->addWidget(sortLabel);
|
||||
filterLayout->addWidget(m_sortCombo);
|
||||
filterLayout->addSpacing(12);
|
||||
filterLayout->addWidget(fromLabel);
|
||||
filterLayout->addWidget(m_fromDateEdit);
|
||||
filterLayout->addWidget(toLabel);
|
||||
filterLayout->addWidget(m_toDateEdit);
|
||||
filterLayout->addWidget(m_clearDatesBtn);
|
||||
filterLayout->addStretch();
|
||||
|
||||
// Add widgets to layout
|
||||
m_mainLayout->addWidget(m_searchBar);
|
||||
m_mainLayout->addLayout(filterLayout);
|
||||
m_mainLayout->addWidget(m_imageGallery, 1);
|
||||
|
||||
// Connect signals - use textEdited instead of textChanged to handle only user input
|
||||
|
||||
// Connect signals
|
||||
connect(m_searchBar, &QLineEdit::textEdited, this, &MainWindow::handleSearchTextChanged);
|
||||
connect(m_sortCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::handleSortChanged);
|
||||
connect(m_fromDateEdit, &QDateEdit::dateChanged, this, &MainWindow::handleDateFilterChanged);
|
||||
connect(m_toDateEdit, &QDateEdit::dateChanged, this, &MainWindow::handleDateFilterChanged);
|
||||
connect(m_clearDatesBtn, &QPushButton::clicked, this, &MainWindow::handleClearDates);
|
||||
}
|
||||
|
||||
void MainWindow::initializeDatabase()
|
||||
@@ -247,26 +302,26 @@ void MainWindow::displayAllImages()
|
||||
statusBar()->showMessage(tr("No database connection available. Cannot display images."), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get total image count first
|
||||
|
||||
DatabaseManager::SortOrder sort = sortFromIndex(m_sortCombo->currentIndex());
|
||||
QDate from = (m_fromDateEdit->date() == m_fromDateEdit->minimumDate()) ? QDate() : m_fromDateEdit->date();
|
||||
QDate to = (m_toDateEdit->date() == m_toDateEdit->minimumDate()) ? QDate() : m_toDateEdit->date();
|
||||
|
||||
m_imageGallery->setSortOrder(sort);
|
||||
m_imageGallery->setDateFilter(from, to);
|
||||
|
||||
int totalImageCount = m_dbManager->getImageCount();
|
||||
|
||||
// Get the first page of images with pagination
|
||||
QList<DatabaseManager::ImageItem> allImages = m_dbManager->getAllImages(0, m_imagePreloadCount);
|
||||
QList<DatabaseManager::ImageItem> allImages = m_dbManager->getAllImages(0, m_imagePreloadCount, sort, from, to);
|
||||
|
||||
if (allImages.isEmpty()) {
|
||||
statusBar()->showMessage(tr("No images found in database"), 5000);
|
||||
} else {
|
||||
statusBar()->showMessage(tr("Loaded %1 of %2 images").arg(allImages.size()).arg(totalImageCount), 3000);
|
||||
}
|
||||
|
||||
// Clear existing images and display first page
|
||||
bool hasMoreImages = (totalImageCount > allImages.size());
|
||||
|
||||
m_imageGallery->displayImages(allImages, true);
|
||||
|
||||
// ALWAYS force update to ensure Load More button appears correctly
|
||||
// This is critical for when we reset the search
|
||||
m_imageGallery->handleSearchResults(allImages, "", 0, m_imagePreloadCount, totalImageCount);
|
||||
|
||||
m_imageGallery->handleSearchResults(allImages, "", 0, m_imagePreloadCount, totalImageCount, sort, from, to);
|
||||
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
@@ -362,6 +417,38 @@ void MainWindow::performSearch()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleSortChanged(int index)
|
||||
{
|
||||
m_imageGallery->setSortOrder(sortFromIndex(index));
|
||||
if (m_lastSearchText.isEmpty())
|
||||
displayAllImages();
|
||||
else
|
||||
performSearch();
|
||||
}
|
||||
|
||||
void MainWindow::handleDateFilterChanged()
|
||||
{
|
||||
QDate from = (m_fromDateEdit->date() == m_fromDateEdit->minimumDate()) ? QDate() : m_fromDateEdit->date();
|
||||
QDate to = (m_toDateEdit->date() == m_toDateEdit->minimumDate()) ? QDate() : m_toDateEdit->date();
|
||||
m_imageGallery->setDateFilter(from, to);
|
||||
if (m_lastSearchText.isEmpty())
|
||||
displayAllImages();
|
||||
else
|
||||
performSearch();
|
||||
}
|
||||
|
||||
void MainWindow::handleClearDates()
|
||||
{
|
||||
QSignalBlocker b1(m_fromDateEdit), b2(m_toDateEdit);
|
||||
m_fromDateEdit->setDate(m_fromDateEdit->minimumDate());
|
||||
m_toDateEdit->setDate(m_toDateEdit->minimumDate());
|
||||
m_imageGallery->setDateFilter(QDate(), QDate());
|
||||
if (m_lastSearchText.isEmpty())
|
||||
displayAllImages();
|
||||
else
|
||||
performSearch();
|
||||
}
|
||||
|
||||
// File menu action handlers
|
||||
void MainWindow::handleOpenFile()
|
||||
{
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
#include <QMainWindow>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QComboBox>
|
||||
#include <QDateEdit>
|
||||
#include <QPushButton>
|
||||
#include "databasemanager.h"
|
||||
#include "imagegallery.h"
|
||||
#include "updatedatabasedialog.h"
|
||||
@@ -25,6 +29,9 @@ protected:
|
||||
|
||||
private slots:
|
||||
void handleSearchTextChanged();
|
||||
void handleSortChanged(int index);
|
||||
void handleDateFilterChanged();
|
||||
void handleClearDates();
|
||||
void performSearch();
|
||||
void updateStatusBar();
|
||||
void handleTypingInactivityTimeout();
|
||||
@@ -44,6 +51,10 @@ private:
|
||||
QWidget *m_centralWidget;
|
||||
QVBoxLayout *m_mainLayout;
|
||||
QLineEdit *m_searchBar;
|
||||
QComboBox *m_sortCombo;
|
||||
QDateEdit *m_fromDateEdit;
|
||||
QDateEdit *m_toDateEdit;
|
||||
QPushButton *m_clearDatesBtn;
|
||||
ImageGallery *m_imageGallery;
|
||||
|
||||
// Data
|
||||
|
||||
Reference in New Issue
Block a user