helper scripts

This commit is contained in:
master
2025-11-16 01:42:27 -05:00
parent af8784a15e
commit 274f3ad344
26 changed files with 2926 additions and 175 deletions
+603 -92
View File
@@ -4,23 +4,68 @@
#include <QDebug>
#include <QVariant>
#include <QFileInfo>
#include <QTimer>
DatabaseManager::DatabaseManager(QObject *parent)
: QObject(parent)
, m_initialized(false)
, m_ftsEnabled(false)
, m_searchCancelled(false)
, m_cachedImageCount(-1) // Initialize to invalid value
, m_currentOffset(0)
, m_currentLimit(0)
{
// Initialize search cache
m_searchCache.clear();
m_allImagesCache.clear();
m_lastCacheUpdate = QDateTime::currentDateTime();
// Create index on ocr_text if it doesn't exist
// This will be executed once the database is initialized
// Connect future watcher to handle search results
connect(&m_searchWatcher, &QFutureWatcher<void>::finished,
this, [this]() {
if (!m_searchCancelled) {
// Emit signal with the results only if not cancelled
QMutexLocker locker(&m_searchMutex);
QString searchText = m_currentSearchText;
int offset = m_currentOffset;
int limit = m_currentLimit;
if (!searchText.isEmpty()) {
QMutexLocker cacheLocker(&m_cacheMutex);
if (m_searchCache.contains(searchText) &&
m_searchCache[searchText].contains(qMakePair(offset, limit))) {
SearchCacheItem cacheItem = m_searchCache[searchText][qMakePair(offset, limit)];
emit searchResultsReady(cacheItem.results, searchText,
offset, limit, cacheItem.totalCount);
}
}
}
});
// Clean cache periodically
QTimer *cleanupTimer = new QTimer(this);
connect(cleanupTimer, &QTimer::timeout, this, &DatabaseManager::cleanupCache);
cleanupTimer->start(60000); // Clean cache every minute
}
DatabaseManager::~DatabaseManager()
{
// Cancel any ongoing search and wait for it to finish
cancelSearch();
if (m_db.isOpen()) {
m_db.close();
}
// Close any thread-specific database connections
QStringList connectionNames = QSqlDatabase::connectionNames();
for (const QString &connName : connectionNames) {
// Remove thread-specific database connections that start with "tdb_"
if (connName.startsWith("tdb_") && connName != QString("tdb_%1").arg((quintptr)QThread::currentThread())) {
QSqlDatabase::removeDatabase(connName);
}
}
}
bool DatabaseManager::initialize(const QString &dbPath)
@@ -68,31 +113,40 @@ bool DatabaseManager::initialize(const QString &dbPath)
return false;
}
bool hasId = false;
bool hasFullPath = false;
bool hasOcrText = false;
while (query.next()) {
QString columnName = query.value(1).toString();
if (columnName == "id") hasId = true;
if (columnName == "full_path") hasFullPath = true;
if (columnName == "ocr_text") hasOcrText = true;
}
if (!hasFullPath || !hasOcrText) {
qDebug() << "Missing required columns in ocr_results table. Need 'full_path' and 'ocr_text'";
if (!hasId || !hasFullPath || !hasOcrText) {
qDebug() << "Missing required columns in ocr_results table. Need 'id', 'full_path', and 'ocr_text'";
m_db.close();
return false;
}
// Create an index on the ocr_text column if it doesn't exist
// This will speed up text searches dramatically
query.exec("CREATE INDEX IF NOT EXISTS idx_ocr_text ON ocr_results(ocr_text)");
// Initialize FTS5 if available
if (initializeFTS()) {
qDebug() << "FTS5 initialized successfully.";
m_ftsEnabled = true;
} else {
// Fallback to regular index if FTS5 is unavailable
qDebug() << "FTS5 not available, using standard index instead.";
query.exec("CREATE INDEX IF NOT EXISTS idx_ocr_text ON ocr_results(ocr_text)");
m_ftsEnabled = false;
}
m_initialized = true;
qDebug() << "Database initialized successfully.";
return true;
}
QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages()
QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages(int offset, int limit)
{
QList<ImageItem> images;
@@ -100,7 +154,18 @@ QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages()
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) {
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();
@@ -112,91 +177,30 @@ QList<DatabaseManager::ImageItem> DatabaseManager::getAllImages()
m_db.transaction();
QSqlQuery query;
query.prepare("SELECT full_path, ocr_text FROM ocr_results");
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();
return images;
}
// Check if files exist as we add them
// Reserve space for results to avoid reallocations
images.reserve(query.size() > 0 ? query.size() : 100);
while (query.next()) {
ImageItem item;
item.filePath = query.value(0).toString();
item.ocrText = query.value(1).toString();
// Only add images that have a non-empty path
if (!item.filePath.isEmpty()) {
images.append(item);
}
}
m_db.commit();
return images;
}
QList<DatabaseManager::ImageItem> DatabaseManager::searchImages(const QString &searchText)
{
QList<ImageItem> images;
if (!m_initialized) {
qDebug() << "Database not initialized.";
return images;
}
// 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;
}
// If search text is empty, return all images
if (searchText.isEmpty()) {
// Clear the search cache when empty search is performed
m_searchCache.clear();
return getAllImages();
}
// Check if we have a cached result for this search query
if (m_searchCache.contains(searchText)) {
return m_searchCache[searchText];
}
// Start transaction to speed up queries
m_db.transaction();
QSqlQuery query;
// Optimize the query based on length of search text
if (searchText.length() <= 3) {
// For short search terms, use a more targeted approach
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search");
query.bindValue(":search", "%" + searchText + "%");
} else {
// For longer search terms, use LIKE with a more specific pattern at start
// which can utilize indexes better if they exist
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search OR ocr_text LIKE :wordstart");
query.bindValue(":search", "%" + searchText + "%");
query.bindValue(":wordstart", "% " + searchText + "%");
}
if (!query.exec()) {
qDebug() << "Failed to search images:" << query.lastError().text();
m_db.rollback();
return images;
}
// Reserve space for results to avoid reallocations
images.reserve(query.size() > 0 ? query.size() : 100);
while (query.next()) {
ImageItem item;
item.filePath = query.value(0).toString();
item.ocrText = query.value(1).toString();
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()) {
@@ -206,18 +210,525 @@ QList<DatabaseManager::ImageItem> DatabaseManager::searchImages(const QString &s
m_db.commit();
// Cache the result for future queries
if (images.size() > 0) {
m_searchCache.insert(searchText, images);
// Update cache
cacheLocker.relock();
m_allImagesCache[cacheKey] = images;
m_lastCacheUpdate = QDateTime::currentDateTime();
cacheLocker.unlock();
return images;
}
int DatabaseManager::getImageCount()
{
// Return cached count if available
QMutexLocker cacheLocker(&m_cacheMutex);
if (m_cachedImageCount >= 0 &&
m_lastCacheUpdate.secsTo(QDateTime::currentDateTime()) < CACHE_LIFETIME_SECS) {
return m_cachedImageCount;
}
cacheLocker.unlock();
if (!m_initialized) {
qDebug() << "Database not initialized.";
return 0;
}
// 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 0;
}
QSqlQuery query;
query.prepare("SELECT COUNT(*) FROM ocr_results");
if (!query.exec() || !query.next()) {
qDebug() << "Failed to get image count:" << query.lastError().text();
return 0;
}
int count = query.value(0).toInt();
// Update cache
cacheLocker.relock();
m_cachedImageCount = count;
cacheLocker.unlock();
return count;
}
QSqlDatabase DatabaseManager::getDatabaseConnection()
{
// Get current thread ID to create unique connection name
QThread* currentThread = QThread::currentThread();
QString connectionName = QString("tdb_%1").arg((quintptr)currentThread);
// Check if connection already exists for this thread
if (QSqlDatabase::contains(connectionName)) {
return QSqlDatabase::database(connectionName);
}
// Create new connection for this thread
QSqlDatabase threadDb = QSqlDatabase::addDatabase("QSQLITE", connectionName);
threadDb.setDatabaseName(m_db.databaseName());
if (!threadDb.open()) {
qDebug() << "Failed to open database in thread:" << threadDb.lastError().text();
} else {
// Enable foreign keys in this connection
QSqlQuery query(threadDb);
query.exec("PRAGMA foreign_keys = ON");
}
return threadDb;
}
void DatabaseManager::searchImages(const QString &searchText, int offset, int limit)
{
if (!m_initialized) {
qDebug() << "Database not initialized.";
emit searchResultsReady(QList<ImageItem>(), searchText, offset, limit, 0);
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);
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);
int totalCount = getImageCount();
// Limit cache size to avoid memory issues
if (m_searchCache.size() > MAX_CACHE_SIZE) {
// Remove the first key (oldest entry)
if (!m_searchCache.isEmpty()) {
m_searchCache.remove(m_searchCache.firstKey());
emit searchResultsReady(allImages, searchText, offset, limit, totalCount);
return;
}
// Check if we have a cached result for this search query
{
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);
return;
}
}
}
return images;
// 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;
}
// 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);
});
// Show immediate feedback that search is starting
emit searchStarted(searchText);
m_searchWatcher.setFuture(m_searchFuture);
}
void DatabaseManager::cancelSearch()
{
// Set cancelled flag
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
m_currentSearchText.clear();
locker.unlock();
// Wait for any running search to complete
if (m_searchFuture.isRunning()) {
m_searchFuture.waitForFinished();
}
}
void DatabaseManager::performSearchInBackground(const QString &searchText, int offset, int limit)
{
QList<ImageItem> images;
// Get a thread-specific database connection
QSqlDatabase threadDb = getDatabaseConnection();
if (!threadDb.isOpen() && !threadDb.open()) {
qDebug() << "Thread database connection failed:" << threadDb.lastError().text();
return;
}
// Check if search was cancelled
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
return;
}
}
// First, get total count for pagination info
QSqlQuery countQuery(threadDb);
int totalCount = 0;
if (m_ftsEnabled) {
QString ftsQuery = prepareFTSQuery(searchText);
countQuery.prepare("SELECT COUNT(*) FROM ocr_results r "
"JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query");
countQuery.bindValue(":query", ftsQuery);
} else {
if (searchText.length() <= 3) {
countQuery.prepare("SELECT COUNT(*) FROM ocr_results WHERE ocr_text LIKE :search");
countQuery.bindValue(":search", "%" + searchText + "%");
} else {
countQuery.prepare("SELECT COUNT(*) FROM ocr_results WHERE ocr_text LIKE :search OR ocr_text LIKE :wordstart");
countQuery.bindValue(":search", "%" + searchText + "%");
countQuery.bindValue(":wordstart", "% " + searchText + "%");
}
}
if (countQuery.exec() && countQuery.next()) {
totalCount = countQuery.value(0).toInt();
} else {
qDebug() << "Failed to get count:" << countQuery.lastError().text();
totalCount = 0;
}
// Check if search was cancelled before main query
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
return;
}
}
// Start transaction to speed up query
threadDb.transaction();
QSqlQuery query(threadDb);
if (m_ftsEnabled) {
// Use FTS5 virtual table for much faster text search
QString ftsQuery = prepareFTSQuery(searchText);
QString queryStr = "SELECT r.id, r.full_path, r.ocr_text 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";
}
query.prepare(queryStr);
query.bindValue(":query", ftsQuery);
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
} else {
// Fallback to LIKE queries if FTS is not available
// Optimize the query based on length of search text
if (searchText.length() <= 3) {
// For short search terms, use a more targeted approach
QString queryStr = "SELECT id, full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search "
"ORDER BY id";
if (limit > 0) {
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":search", "%" + searchText + "%");
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
} else {
// For longer search terms, use LIKE with a more specific pattern at start
QString queryStr = "SELECT id, full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search OR ocr_text LIKE :wordstart "
"ORDER BY id";
if (limit > 0) {
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":search", "%" + searchText + "%");
query.bindValue(":wordstart", "% " + searchText + "%");
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
}
}
if (!query.exec()) {
qDebug() << "Failed to search images:" << query.lastError().text();
qDebug() << "Error details:" << query.lastError().databaseText();
threadDb.rollback();
// If FTS query failed, try fallback to LIKE
if (m_ftsEnabled) {
qDebug() << "Trying fallback to LIKE query...";
threadDb.transaction();
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search");
query.bindValue(":search", "%" + searchText + "%");
if (!query.exec()) {
qDebug() << "Fallback query also failed:" << query.lastError().text();
threadDb.rollback();
return;
}
} else {
return;
}
}
// Check if search was cancelled
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
threadDb.rollback();
return;
}
}
// Reserve space for results to avoid reallocations
images.reserve(query.size() > 0 ? query.size() : 100);
while (query.next()) {
// Periodically check if search was cancelled
if (query.at() % 20 == 0) {
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
threadDb.rollback();
return;
}
}
ImageItem item;
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()) {
images.append(item);
}
}
threadDb.commit();
// Check if search was cancelled before storing results
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
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 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
QMutexLocker searchLocker(&m_searchMutex);
if (!m_searchCancelled && m_currentSearchText == searchText) {
searchLocker.unlock();
emit searchResultsReady(images, searchText, offset, limit, totalCount);
}
}
void DatabaseManager::cleanupCache()
{
QMutexLocker locker(&m_cacheMutex);
// 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();
QMutableMapIterator<QPair<int, int>, SearchCacheItem> j(i.value());
while (j.hasNext()) {
j.next();
if (j.value().timestamp.secsTo(now) > CACHE_LIFETIME_SECS) {
j.remove();
}
}
// If no more results for this search text, remove the entry
if (i.value().isEmpty()) {
i.remove();
}
}
// Expire old all-images cache
if (m_lastCacheUpdate.secsTo(now) > CACHE_LIFETIME_SECS) {
m_allImagesCache.clear();
m_cachedImageCount = -1; // Invalidate count cache
}
}
bool DatabaseManager::initializeFTS()
{
// Check if SQLite has FTS5 support
QSqlQuery query;
query.exec("SELECT sqlite_compileoption_used('ENABLE_FTS5')");
if (!query.next() || !query.value(0).toBool()) {
qDebug() << "FTS5 not available in this SQLite installation";
return false;
}
// Check if our FTS table already exists
query.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='ocr_fts'");
if (!query.next()) {
// Create FTS5 virtual table
qDebug() << "Creating FTS5 virtual table...";
bool success = query.exec(
"CREATE VIRTUAL TABLE IF NOT EXISTS ocr_fts USING fts5("
"ocr_text, "
"content='ocr_results', "
"content_rowid='id', "
"tokenize='porter unicode61');"
);
if (!success) {
qDebug() << "Failed to create FTS5 table:" << query.lastError().text();
return false;
}
// Populate the FTS table from existing data
query.exec("BEGIN TRANSACTION;");
success = query.exec(
"INSERT INTO ocr_fts(rowid, ocr_text) "
"SELECT id, ocr_text FROM ocr_results;"
);
query.exec("COMMIT;");
if (!success) {
qDebug() << "Failed to populate FTS5 table:" << query.lastError().text();
return false;
}
// Create triggers to keep FTS table in sync with ocr_results
success = query.exec(
"CREATE TRIGGER IF NOT EXISTS ocr_fts_insert AFTER INSERT ON ocr_results BEGIN "
" INSERT INTO ocr_fts(rowid, ocr_text) VALUES (new.id, new.ocr_text); "
"END;"
);
if (!success) {
qDebug() << "Failed to create insert trigger:" << query.lastError().text();
return false;
}
success = query.exec(
"CREATE TRIGGER IF NOT EXISTS ocr_fts_delete AFTER DELETE ON ocr_results BEGIN "
" INSERT INTO ocr_fts(ocr_fts, rowid, ocr_text) VALUES('delete', old.id, old.ocr_text); "
"END;"
);
if (!success) {
qDebug() << "Failed to create delete trigger:" << query.lastError().text();
return false;
}
success = query.exec(
"CREATE TRIGGER IF NOT EXISTS ocr_fts_update AFTER UPDATE ON ocr_results BEGIN "
" INSERT INTO ocr_fts(ocr_fts, rowid, ocr_text) VALUES('delete', old.id, old.ocr_text); "
" INSERT INTO ocr_fts(rowid, ocr_text) VALUES (new.id, new.ocr_text); "
"END;"
);
if (!success) {
qDebug() << "Failed to create update trigger:" << query.lastError().text();
return false;
}
}
return true;
}
QString DatabaseManager::prepareFTSQuery(const QString &searchText)
{
// Split the search text into tokens
QStringList tokens = searchText.simplified().split(' ', Qt::SkipEmptyParts);
// For single word searches, search for the word as-is and with a wildcard
if (tokens.size() == 1) {
QString token = tokens.first();
// Use prefix search (words starting with the term)
return QString("%1* OR %1").arg(token);
}
// For multi-word searches
else {
// Build both exact phrase search and individual term search
QStringList tokenQueries;
// Add phrase match (higher relevance)
tokenQueries << QString("\"%1\"").arg(searchText);
// Add individual token matches with wildcards
for (const QString &token : tokens) {
if (token.length() > 2) { // Only use wildcards for tokens with 3+ chars
tokenQueries << QString("%1*").arg(token);
} else {
tokenQueries << token;
}
}
return tokenQueries.join(" OR ");
}
}
+89 -7
View File
@@ -11,6 +11,13 @@
#include <QPair>
#include <QThread>
#include <QMap>
#include <QMutex>
#include <QWaitCondition>
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include <QRegularExpression>
/**
* @brief The DatabaseManager class handles all database operations
@@ -27,6 +34,7 @@ public:
struct ImageItem {
QString filePath;
QString ocrText;
int id; // Add id to support pagination
};
/**
@@ -48,28 +56,102 @@ public:
bool initialize(const QString &dbPath);
/**
* @brief Get all images from the database
* @brief Get all images from the database with pagination
* @param offset Starting position (0-based) for pagination
* @param limit Maximum number of items to return, 0 means no limit
* @return List of ImageItem objects
*/
QList<ImageItem> getAllImages();
QList<ImageItem> getAllImages(int offset = 0, int limit = 0);
/**
* @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
* @return List of ImageItem objects that match the search
* @param offset Starting position (0-based) for pagination
* @param limit Maximum number of items to return, 0 means no limit
*/
QList<ImageItem> searchImages(const QString &searchText);
void searchImages(const QString &searchText, int offset = 0, int limit = 0);
/**
* @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);
/**
* @brief Signal emitted when search operation starts
* @param searchText The search text being processed
*/
void searchStarted(const QString &searchText);
private:
QSqlDatabase m_db;
bool m_initialized;
bool m_ftsEnabled; // Whether FTS5 is available and enabled
// Cache for search results to improve response time
QMap<QString, QList<ImageItem>> m_searchCache;
// Cache structure includes both results and total count
struct SearchCacheItem {
QList<ImageItem> results;
int totalCount;
QDateTime timestamp;
};
// Maximum number of cached search queries
static const int MAX_CACHE_SIZE = 50;
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
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;
int m_currentOffset;
int m_currentLimit;
bool m_searchCancelled;
QMutex m_searchMutex;
// Cache settings
static const int MAX_CACHE_SIZE = 50; // Maximum number of cached search queries
static const int DEFAULT_PAGE_SIZE = 100; // Default number of items per page
static const int CACHE_LIFETIME_SECS = 300; // Cache lifetime in seconds (5 minutes)
// Initialize FTS5 full-text search
bool initializeFTS();
// 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);
// Clear expired cache items
void cleanupCache();
// Get a database connection for the current thread
QSqlDatabase getDatabaseConnection();
};
#endif // DATABASEMANAGER_H
+390 -11
View File
@@ -9,6 +9,9 @@
#include <QFrame>
#include <QHBoxLayout>
#include <QTimer>
#include <QScrollBar>
// ImageThumbnail implementation
ImageThumbnail::ImageThumbnail(const QString &filePath, QWidget *parent)
@@ -43,9 +46,17 @@ void ImageThumbnail::mousePressEvent(QMouseEvent *event)
ImageGallery::ImageGallery(QWidget *parent)
: QWidget(parent)
, m_dbManager(nullptr)
, m_currentOffset(0)
, m_pageSize(DEFAULT_PAGE_SIZE)
, m_totalCount(0)
, m_isLoading(false)
, m_hasMoreImages(false)
, m_lastScrollPosition(0)
, m_loadingIndicator(nullptr)
{
// Create scroll area
m_scrollArea = new QScrollArea(this);
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Prevent horizontal scrollbar
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
@@ -83,6 +94,34 @@ ImageGallery::ImageGallery(QWidget *parent)
QTimer *resizeTimer = new QTimer(this);
connect(resizeTimer, &QTimer::timeout, this, &ImageGallery::handleContainerResized);
resizeTimer->start(300); // Check every 300ms
// Create scroll timer for detecting end of scroll events
m_scrollTimer = new QTimer(this);
m_scrollTimer->setSingleShot(true);
m_scrollTimer->setInterval(200);
connect(m_scrollTimer, &QTimer::timeout, this, &ImageGallery::handleScrolledToBottom);
// Create loading indicator
m_loadingIndicator = new QLabel(tr("Loading more images..."), this);
m_loadingIndicator->setAlignment(Qt::AlignCenter);
m_loadingIndicator->setStyleSheet("color: #666; font-size: 14px; padding: 10px;");
m_loadingIndicator->hide();
// Install event filter on scroll area viewport to catch scroll events
m_scrollArea->viewport()->installEventFilter(this);
// Create the loading indicator
m_loadingIndicator = new QLabel(tr("Loading more images..."), this);
m_loadingIndicator->setAlignment(Qt::AlignCenter);
m_loadingIndicator->setStyleSheet("color: #666; font-size: 14px; padding: 10px;");
m_loadingIndicator->hide();
// Create Load More button
m_loadMoreButton = new QPushButton(tr("Load More Images"), this);
m_loadMoreButton->setStyleSheet("font-size: 14px; padding: 10px; margin: 10px; background-color: #e0e0e0; border-radius: 5px; border: 1px solid #c0c0c0;");
m_loadMoreButton->setMinimumHeight(50);
m_loadMoreButton->setCursor(Qt::PointingHandCursor);
connect(m_loadMoreButton, &QPushButton::clicked, this, &ImageGallery::handleLoadMoreClicked);
}
ImageGallery::~ImageGallery()
@@ -93,19 +132,71 @@ ImageGallery::~ImageGallery()
void ImageGallery::setDatabaseManager(DatabaseManager *dbManager)
{
m_dbManager = dbManager;
// Connect to the DatabaseManager signals
if (m_dbManager) {
connect(m_dbManager, &DatabaseManager::searchResultsReady,
this, &ImageGallery::handleSearchResults);
connect(m_dbManager, &DatabaseManager::searchStarted,
this, &ImageGallery::handleSearchStarted);
}
// Initialize the last search query
m_lastSearchQuery = QString();
}
void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images)
void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images, bool clearExisting)
{
// Clear existing thumbnails
clearGallery();
// Clear existing thumbnails if requested
if (clearExisting) {
clearGallery();
m_currentOffset = 0;
// Don't reset m_hasMoreImages here - we need to know if there are more to show the button
// It will be set correctly in handleSearchResults
}
// Update grid layout to ensure correct column count before adding images
updateGridLayout();
// Remove any previous instances of the Load More button and loading indicator from the layout
if (m_loadMoreButton->parent()) {
m_gridLayout->removeWidget(m_loadMoreButton);
}
if (m_loadingIndicator->parent()) {
m_gridLayout->removeWidget(m_loadingIndicator);
}
m_loadMoreButton->hide();
m_loadingIndicator->hide();
const int numImages = images.size();
int row = 0, col = 0;
// If we have existing thumbnails, start from the last position
if (!clearExisting && !m_thumbnails.isEmpty()) {
// Remove any "no images" or loading indicators first
for (int i = m_thumbnails.size() - 1; i >= 0; i--) {
if (m_thumbnails[i]->text().contains("No images found") ||
m_thumbnails[i]->text().contains("Loading")) {
m_gridLayout->removeWidget(m_thumbnails[i]);
delete m_thumbnails[i];
m_thumbnails.removeAt(i);
}
}
if (!m_thumbnails.isEmpty()) {
int lastIndex = m_thumbnails.size() - 1;
row = lastIndex / m_columnsCount;
col = lastIndex % m_columnsCount;
// Move to the next position
col++;
if (col >= m_columnsCount) {
col = 0;
row++;
}
}
}
for (int i = 0; i < numImages; ++i) {
const auto &item = images[i];
@@ -125,17 +216,20 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
QFileInfo fileNameInfo(item.filePath);
QString fileName = fileNameInfo.fileName();
// Create overlay container with dark background
// Create overlay container with dark background - store it as a property of the thumbnail
QFrame* overlay = new QFrame(thumbnailLabel);
overlay->setObjectName("filenameOverlay"); // Set object name for finding it later
overlay->setStyleSheet("background-color: rgba(0, 0, 0, 0.7);");
overlay->setFixedHeight(20);
overlay->setFixedWidth(THUMBNAIL_WIDTH);
// Create label for the filename
QLabel* fileNameLabel = new QLabel(fileName, overlay);
fileNameLabel->setStyleSheet("color: white; background: transparent;");
fileNameLabel->setAlignment(Qt::AlignCenter);
fileNameLabel->setFixedWidth(THUMBNAIL_WIDTH - 10);
// Truncate filename if too long (more than 30 chars)
if (fileName.length() > 30) {
fileNameLabel->setText(fileName.left(13) + "..." + fileName.right(13));
}
// Layout for the overlay
QHBoxLayout* overlayLayout = new QHBoxLayout(overlay);
@@ -143,6 +237,8 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
overlayLayout->addWidget(fileNameLabel);
// Position the overlay at the bottom of the thumbnail
// Width and position will be adjusted in updateGridLayout()
overlay->setFixedWidth(thumbnailLabel->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
// Connect the thumbnail click signal
@@ -169,10 +265,32 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
noImagesLabel->setAlignment(Qt::AlignCenter);
m_gridLayout->addWidget(noImagesLabel, 0, 0, 1, m_columnsCount);
m_thumbnails.append(noImagesLabel);
} else if (images.size() > 0) {
// This final row + 1 position is where we'll add the Load More button
// or nothing if we don't have more images
if (m_hasMoreImages) {
// Update Load More button text with current count info
m_loadMoreButton->setText(tr("Load More Images (%1 of %2)")
.arg(m_currentOffset)
.arg(m_totalCount));
// Add to layout and show
m_gridLayout->addWidget(m_loadMoreButton, row + 1, 0, 1, m_columnsCount, Qt::AlignCenter);
m_loadMoreButton->setVisible(true);
m_loadMoreButton->raise(); // Ensure it's on top
qDebug() << "Added Load More button at row" << row + 1 << "showing"
<< m_currentOffset << "of" << m_totalCount;
} else {
m_loadMoreButton->setVisible(false);
}
m_loadingIndicator->setVisible(false);
}
// Add stretch to the bottom of the grid
m_gridLayout->setRowStretch(row + 1, 1);
// Add stretch to the bottom of the grid, but after the Load More button
m_gridLayout->setRowStretch(row + 3, 1);
}
void ImageGallery::clearGallery()
@@ -190,11 +308,219 @@ void ImageGallery::clearGallery()
void ImageGallery::handleSearchTextChanged(const QString &searchText)
{
if (m_dbManager) {
QList<DatabaseManager::ImageItem> images = m_dbManager->searchImages(searchText);
displayImages(images);
// Clear existing search results
clearGallery();
// Store the search text for tracking
m_lastSearchQuery = searchText;
// Reset pagination for new search
m_currentOffset = 0;
m_totalCount = 0;
m_hasMoreImages = false;
m_isLoading = true;
// Show loading indicator
m_loadingIndicator->setText(tr("Searching..."));
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);
}
}
void ImageGallery::handleSearchResults(const QList<DatabaseManager::ImageItem> &results,
const QString &searchText, int offset, int limit,
int totalCount)
{
// Only update display if this is the result for the most recent search
if (searchText == m_lastSearchQuery) {
m_isLoading = false;
m_totalCount = totalCount;
// If this is the first page or a different search
bool isFirstPage = (offset == 0);
// Display images, clearing existing ones only if this is first page
displayImages(results, isFirstPage);
// Update pagination state
m_currentOffset = offset + results.size();
m_totalCount = totalCount;
m_hasMoreImages = m_currentOffset < totalCount;
// Always hide the loading indicator when search completes
m_loadingIndicator->hide();
// Update Load More button if we have more images
// Update Load More button state based on whether there are more images
if (m_hasMoreImages) {
// Update button text with current counts
m_loadMoreButton->setText(tr("Load More Images (%1 of %2)")
.arg(m_currentOffset)
.arg(totalCount));
// Add to layout at bottom of grid and ensure it's visible
int row = (m_thumbnails.size() / m_columnsCount);
m_gridLayout->removeWidget(m_loadMoreButton); // Remove from any previous location
m_gridLayout->addWidget(m_loadMoreButton, row + 1, 0, 1, m_columnsCount, Qt::AlignCenter);
m_loadMoreButton->setVisible(true);
m_loadMoreButton->raise(); // Ensure it's on top
qDebug() << "Search results - Added Load More button at row" << row + 1
<< "showing" << m_currentOffset << "of" << totalCount;
} else {
// Hide both indicators if no more images
m_loadingIndicator->setVisible(false);
m_loadMoreButton->setVisible(false);
}
}
}
// Method to append more images to the gallery
void ImageGallery::appendImages(const QList<DatabaseManager::ImageItem> &images)
{
// Call displayImages with clearExisting=false
displayImages(images, false);
}
// Handle search start event
void ImageGallery::handleSearchStarted(const QString &searchText)
{
// Show loading indicator or status message
// This is called when a search operation begins
// Set loading state
m_isLoading = true;
// Only show "Searching" indicator when search actually begins (not during typing)
if (!m_thumbnails.isEmpty() && !m_lastSearchQuery.isEmpty() && searchText != m_lastSearchQuery) {
// New search, keep thumbnails but show loading overlay
m_loadingIndicator->setText(tr("Searching..."));
m_loadingIndicator->show();
// Move loading indicator to be visible
m_scrollArea->verticalScrollBar()->setValue(0);
}
}
// Load more images when scrolled to bottom
void ImageGallery::loadMoreImages()
{
if (!m_dbManager || m_isLoading || !m_hasMoreImages) {
return;
}
m_isLoading = true;
// Hide the Load More button while loading
m_loadMoreButton->hide();
// Show loading indicator
m_loadingIndicator->setText(tr("Loading more images (%1 of %2)...")
.arg(m_currentOffset)
.arg(m_totalCount));
m_loadingIndicator->show();
// Position loading indicator at bottom of grid
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);
}
// Handle scroll to bottom event
void ImageGallery::handleScrolledToBottom()
{
if (!m_isLoading && m_hasMoreImages) {
qDebug() << "Loading more images from handleScrolledToBottom";
loadMoreImages();
}
}
// Handle Load More button click
void ImageGallery::handleLoadMoreClicked()
{
if (!m_isLoading && m_hasMoreImages) {
qDebug() << "Loading more images from Load More button";
// Hide the button and show loading indicator during loading
m_loadMoreButton->hide();
m_loadingIndicator->setText(tr("Loading images..."));
m_loadingIndicator->show();
int row = (m_thumbnails.size() / m_columnsCount);
m_gridLayout->addWidget(m_loadingIndicator, row + 1, 0, 1, m_columnsCount);
// Request more images
loadMoreImages();
// Debug output
qDebug() << "Total thumbnails:" << m_thumbnails.size()
<< "Grid size:" << m_gridLayout->rowCount() << "x" << m_gridLayout->columnCount()
<< "Has more:" << m_hasMoreImages;
}
}
// Set page size for lazy loading
void ImageGallery::setPageSize(int pageSize)
{
if (pageSize > 0) {
m_pageSize = pageSize;
}
}
// Reset pagination state to initial values
void ImageGallery::resetPagination()
{
// Reset pagination state variables
m_currentOffset = 0;
m_isLoading = false;
m_hasMoreImages = false;
m_totalCount = 0;
// Hide loading indicators
if (m_loadingIndicator) {
m_loadingIndicator->hide();
}
// Hide "Load More" button until we know there are more images
if (m_loadMoreButton) {
m_loadMoreButton->hide();
}
}
// Override wheel event to detect end of scrolling
void ImageGallery::wheelEvent(QWheelEvent *event)
{
QWidget::wheelEvent(event);
// Don't check scroll position during the wheel event
// This will be handled by the eventFilter
}
void ImageGallery::updateSearchTextDisplay(const QString &searchText)
{
// Update the UI to reflect the current search text without performing a search
// This gives immediate feedback while typing, before the actual search happens
// Store the search text for tracking
m_lastSearchQuery = searchText;
// Keep existing thumbnails visible without distracting visual changes
// If we have a "no results" message showing, update it quietly
for (auto thumbnail : m_thumbnails) {
if (thumbnail->text().contains("No images found")) {
thumbnail->setText(tr("Search in progress..."));
}
}
// No typing indicator or popups while typing
// Just wait for the actual search to complete
}
void ImageGallery::handleThumbnailClicked(const QString &filePath)
{
// Show a wait cursor while attempting to open the file
@@ -260,7 +586,7 @@ void ImageGallery::resizeEvent(QResizeEvent *event)
}
}
// Event filter to catch viewport resize events
// Event filter to catch viewport resize events and scroll events
bool ImageGallery::eventFilter(QObject *watched, QEvent *event)
{
// Check if this is a resize event on the viewport
@@ -270,6 +596,33 @@ bool ImageGallery::eventFilter(QObject *watched, QEvent *event)
return false; // Allow event to propagate
}
// Check for various scroll-related events
if (watched == m_scrollArea->viewport() &&
(event->type() == QEvent::Wheel ||
event->type() == QEvent::MouseMove ||
event->type() == QEvent::MouseButtonRelease)) {
QScrollBar* vScrollBar = m_scrollArea->verticalScrollBar();
if (vScrollBar) {
int max = vScrollBar->maximum();
int current = vScrollBar->value();
int viewportHeight = m_scrollArea->viewport()->height();
// If we're near the bottom and not already loading, prepare to load more
// Use a percentage of viewport height as threshold for very tall viewports
int dynamicThreshold = qMin(LOAD_THRESHOLD_PX, viewportHeight / 4);
if ((max - current) <= dynamicThreshold && max > 0 && !m_isLoading && m_hasMoreImages) {
// Cancel any pending timer and start a new one for debouncing
m_scrollTimer->stop();
m_scrollTimer->start();
// We'll rely on the Load More button for explicit user action
// rather than showing a loading indicator during scroll
}
}
}
// Pass unhandled events to parent
return QWidget::eventFilter(watched, event);
}
@@ -345,6 +698,13 @@ void ImageGallery::updateGridLayout()
for (auto thumbnail : m_thumbnails) {
thumbnail->setMaximumWidth(THUMBNAIL_WIDTH);
thumbnail->setAlignment(Qt::AlignCenter);
// Update the overlay to match thumbnail width
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(THUMBNAIL_WIDTH);
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
} else {
// For multi-column, use minimal margins
@@ -353,12 +713,31 @@ void ImageGallery::updateGridLayout()
// Reset thumbnail constraints
for (auto thumbnail : m_thumbnails) {
thumbnail->setMaximumWidth(QWIDGETSIZE_MAX);
// Update the overlay to match thumbnail width in multi-column mode
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(thumbnail->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
// For multi-column, let the container fill the viewport
m_containerWidget->setMinimumWidth(viewportWidth);
}
// Force update of overlay position and size after a short delay
// This ensures the overlays are sized correctly after all layout changes
QTimer::singleShot(50, this, [this]() {
for (auto thumbnail : m_thumbnails) {
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(thumbnail->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
});
// Update the layout again after a short delay to handle edge cases
QTimer::singleShot(10, this, [this](){
m_gridLayout->update();
+31 -1
View File
@@ -11,6 +11,7 @@
#include <QDebug>
#include <QPushButton>
#include <QResizeEvent>
#include <QPushButton>
#include "databasemanager.h"
// Global constants
@@ -46,18 +47,29 @@ public:
~ImageGallery();
void setDatabaseManager(DatabaseManager *dbManager);
void displayImages(const QList<DatabaseManager::ImageItem> &images);
void displayImages(const QList<DatabaseManager::ImageItem> &images, bool clearExisting = true);
void appendImages(const QList<DatabaseManager::ImageItem> &images);
void clearGallery();
void loadMoreImages();
void setPageSize(int pageSize); // Set the number of images to load per page
void resetPagination(); // Reset pagination state to initial values
public slots:
void handleSearchTextChanged(const QString &searchText);
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 handleSearchStarted(const QString &searchText);
void updateSearchTextDisplay(const QString &searchText); // Update UI with search text without performing search
void handleScrolledToBottom();
void handleLoadMoreClicked();
protected:
void resizeEvent(QResizeEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
QGridLayout *m_gridLayout;
@@ -67,8 +79,26 @@ private:
QList<ImageThumbnail*> m_thumbnails;
int m_columnsCount; // Dynamic column count based on window size
// Pagination variables
int m_currentOffset;
int m_pageSize;
int m_totalCount;
bool m_isLoading;
bool m_hasMoreImages;
QLabel *m_loadingIndicator;
QPushButton *m_loadMoreButton;
QPixmap createThumbnail(const QString &filePath, int width, int height);
QPixmap createPlaceholderThumbnail(int width, int height, const QString &message);
QString m_lastSearchQuery; // Track the last search text
// Track scroll position
int m_lastScrollPosition;
QTimer *m_scrollTimer;
// Lazy loading constants
static constexpr int DEFAULT_PAGE_SIZE = 20;
static constexpr int LOAD_THRESHOLD_PX = 100; // Pixels from bottom to trigger load
};
#endif // IMAGEGALLERY_H
+9
View File
@@ -1,4 +1,5 @@
#include <QApplication>
#include <QIcon>
#include "mainwindow.h"
int main(int argc, char *argv[])
@@ -9,6 +10,14 @@ int main(int argc, char *argv[])
app.setApplicationName("Screenshot Gallery");
app.setApplicationVersion("1.0.0");
// Set application icon
QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512));
app.setWindowIcon(appIcon);
// Create and show main window
MainWindow mainWindow;
mainWindow.show();
+305 -50
View File
@@ -1,24 +1,47 @@
#include "mainwindow.h"
#include "settingsdialog.h"
#include <QIcon>
#include <QApplication>
#include <QScreen>
#include <QResizeEvent>
#include <QFileInfo>
#include <QMessageBox>
#include <QInputDialog>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QFileDialog>
#include <QDesktopServices>
#include <QSettings>
#include <stdexcept>
// Define the static constant for the default database path
const QString MainWindow::DEFAULT_DB_PATH = "/home/master/screenshot_ocr.db";
// Define settings file path
const QString MainWindow::CONFIG_FILE_PATH = QDir::homePath() + "/.config/ScreenshotOCRGallery/settings.ini";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_dbManager(new DatabaseManager(this))
, m_searchDelayTimer(new QTimer(this))
, m_typingTimer(new QTimer(this))
, m_searchingAnimationTimer(new QTimer(this))
, m_searchingDots(1)
, m_isTyping(false)
, m_hasValidDatabase(false)
, m_databasePath("")
, m_screenshotsDir("")
, m_imagePreloadCount(DEFAULT_PRELOAD_COUNT)
{
// Set window title and size
setWindowTitle(tr("Screenshot OCR Gallery"));
resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
// Set window icon
QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512));
setWindowIcon(appIcon);
// Remove fixed minimum size to allow for single column layout at any width
// setMinimumSize(640, 480);
@@ -31,20 +54,61 @@ MainWindow::MainWindow(QWidget *parent)
(availableGeometry.height() - size.height()) / 2);
}
// Load settings first
loadSettings();
// Set up UI
createLayout();
// Initialize timer for delayed search
m_searchDelayTimer->setSingleShot(true);
m_searchDelayTimer->setInterval(SEARCH_DELAY_MS);
connect(m_searchDelayTimer, &QTimer::timeout, this, &MainWindow::performSearch);
// Initialize typing inactivity timer
m_typingTimer->setSingleShot(true);
m_typingTimer->setInterval(500); // 500ms of no typing before searching
connect(m_typingTimer, &QTimer::timeout, this, &MainWindow::handleTypingInactivityTimeout);
// Only create animation timer for searching visual feedback
m_searchingAnimationTimer = new QTimer(this);
m_searchingAnimationTimer->setInterval(200); // Animation speed for searching dots
m_searchingDots = 1;
connect(m_searchingAnimationTimer, &QTimer::timeout, this, [this]() {
m_searchingDots = (m_searchingDots % 3) + 1; // Cycle between 1, 2, and 3 dots
QString dots;
for(int i = 0; i < m_searchingDots; ++i) dots += ".";
QString searchingIndicator = QString("%1 %2").arg(tr("Searching")).arg(dots);
statusBar()->showMessage(searchingIndicator);
});
// Initialize database and display images
initializeDatabase();
// Connect database signals for status updates
if (m_dbManager) {
connect(m_dbManager, &DatabaseManager::searchStarted,
this, [this](const QString &text) {
statusBar()->showMessage(tr("Searching..."), 1000);
});
connect(m_dbManager, &DatabaseManager::searchResultsReady,
this, [this](const QList<DatabaseManager::ImageItem> &results, const QString &text,
int offset, int limit, int totalCount) {
if (text.isEmpty()) {
statusBar()->showMessage(tr("Showing all images (paginated) - %1 total").arg(totalCount));
} else {
statusBar()->showMessage(tr("Found %1 results for \"%2\" (showing %3 to %4)")
.arg(totalCount)
.arg(text)
.arg(offset - results.size() + 1)
.arg(offset), 3000);
}
});
}
displayAllImages();
// Set focus to search bar
// Set focus to search bar and disable autocomplete which can cause UI lag
m_searchBar->setFocus();
m_searchBar->setAutoFillBackground(true);
m_searchBar->setAttribute(Qt::WA_MacShowFocusRect, false); // Reduce drawing overhead
// Set status bar
statusBar()->showMessage(tr("Ready"));
@@ -67,13 +131,42 @@ void MainWindow::createLayout()
m_mainLayout->setSpacing(5); // Reduce spacing between elements
m_mainLayout->setContentsMargins(5, 5, 5, 5); // Reduce margins
// Create title label
m_titleLabel = new QLabel(tr("Screenshot OCR Gallery"), this);
QFont titleFont = m_titleLabel->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
m_titleLabel->setFont(titleFont);
m_titleLabel->setAlignment(Qt::AlignCenter);
// Title removed - now shown only in the title bar
// Create menu bar
QMenuBar *menuBar = new QMenuBar(this);
setMenuBar(menuBar);
// Create File menu
QMenu *fileMenu = menuBar->addMenu(tr("&File"));
// Add Open action
QAction *openAction = new QAction(tr("&Open"), this);
openAction->setShortcut(QKeySequence::Open);
connect(openAction, &QAction::triggered, this, &MainWindow::handleOpenFile);
fileMenu->addAction(openAction);
// Add Open With action
QAction *openWithAction = new QAction(tr("Open &With..."), this);
connect(openWithAction, &QAction::triggered, this, &MainWindow::handleOpenFileWith);
fileMenu->addAction(openWithAction);
// Add separator
fileMenu->addSeparator();
// Add Settings action
QAction *settingsAction = new QAction(tr("&Settings"), this);
connect(settingsAction, &QAction::triggered, this, &MainWindow::handleSettings);
fileMenu->addAction(settingsAction);
// Add separator
fileMenu->addSeparator();
// Add Quit action
QAction *quitAction = new QAction(tr("&Quit"), this);
quitAction->setShortcut(QKeySequence::Quit);
connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
fileMenu->addAction(quitAction);
// Create search bar
m_searchBar = new QLineEdit(this);
@@ -85,34 +178,33 @@ void MainWindow::createLayout()
m_imageGallery->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// Add widgets to layout
m_mainLayout->addWidget(m_titleLabel);
m_mainLayout->addWidget(m_searchBar);
m_mainLayout->addWidget(m_imageGallery, 1);
// Connect signals
connect(m_searchBar, &QLineEdit::textChanged, this, &MainWindow::handleSearchTextChanged);
// Connect signals - use textEdited instead of textChanged to handle only user input
connect(m_searchBar, &QLineEdit::textEdited, this, &MainWindow::handleSearchTextChanged);
}
void MainWindow::initializeDatabase()
{
// Check if database file exists
QFileInfo dbFileInfo(DEFAULT_DB_PATH);
QFileInfo dbFileInfo(m_databasePath);
if (!dbFileInfo.exists() || !dbFileInfo.isFile()) {
QString errorMsg = tr("Database file not found: %1").arg(DEFAULT_DB_PATH);
QString errorMsg = tr("Database file not found: %1").arg(m_databasePath);
statusBar()->showMessage(errorMsg, 10000);
QMessageBox::critical(this, tr("Database Error"), errorMsg);
qDebug() << "Database file not found:" << DEFAULT_DB_PATH;
qDebug() << "Database file not found:" << m_databasePath;
m_hasValidDatabase = false;
return;
}
// Try to initialize database
if (!m_dbManager->initialize(DEFAULT_DB_PATH)) {
if (!m_dbManager->initialize(m_databasePath)) {
QString errorMsg = tr("Failed to connect to database");
statusBar()->showMessage(errorMsg, 10000);
QMessageBox::warning(this, tr("Database Error"),
tr("Failed to connect to database: %1\nThe application will continue with limited functionality.")
.arg(DEFAULT_DB_PATH));
.arg(m_databasePath));
qDebug() << "Failed to initialize database";
m_hasValidDatabase = false;
} else {
@@ -130,12 +222,25 @@ void MainWindow::displayAllImages()
return;
}
QList<DatabaseManager::ImageItem> allImages = m_dbManager->getAllImages();
// Get total image count first
int totalImageCount = m_dbManager->getImageCount();
// Get the first page of images with pagination
QList<DatabaseManager::ImageItem> allImages = m_dbManager->getAllImages(0, m_imagePreloadCount);
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);
}
m_imageGallery->displayImages(allImages);
// 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);
updateStatusBar();
}
@@ -146,42 +251,193 @@ void MainWindow::handleSearchTextChanged()
return;
}
// Capture current text
QString currentText = m_searchBar->text();
// If search bar is cleared, immediately show all images
if (m_searchBar->text().isEmpty()) {
if (currentText.isEmpty()) {
m_lastSearchText.clear();
m_searchDelayTimer->stop();
m_typingTimer->stop();
// No typing animation to stop
m_searchingAnimationTimer->stop();
m_isTyping = false;
// No typing dots to reset
m_searchingDots = 1;
// Show loading status
statusBar()->showMessage(tr("Loading gallery..."));
// First, explicitly reset current offset in gallery
m_imageGallery->resetPagination();
// Clear gallery and load first page
displayAllImages(); // Show all images immediately
return; // Skip the timer since we've already updated
}
// For non-empty searches, restart the timer each time the user types
m_searchDelayTimer->start();
// Update last search text
m_lastSearchText = currentText;
// Indicate user is typing and reset the inactivity timer
m_isTyping = true;
m_typingTimer->start();
// Don't show any typing indicator in the status bar
// Only update the search text display without animation
// Update the search text display but don't search yet
m_imageGallery->updateSearchTextDisplay(m_lastSearchText);
}
void MainWindow::handleTypingInactivityTimeout()
{
// User has stopped typing for 500ms, perform the search
m_isTyping = false;
// Start search animation directly without typing animation
// Update the status bar with search indicator
QString dots;
for(int i = 0; i < m_searchingDots; ++i) dots += ".";
QString searchingIndicator = QString("%1 %2").arg(tr("Searching")).arg(dots);
statusBar()->showMessage(searchingIndicator);
// Start the searching animation
if (!m_searchingAnimationTimer->isActive()) {
m_searchingAnimationTimer->start();
}
// Actually perform the search
performSearch();
}
void MainWindow::performSearch()
{
if (!m_hasValidDatabase) {
statusBar()->showMessage(tr("Cannot perform search: No database connection"), 3000);
if (!m_hasValidDatabase || m_isTyping) {
// Don't search if database is not available or if user is still typing
return;
}
QString currentText = m_searchBar->text();
// Only perform search if text has changed
if (currentText != m_lastSearchText) {
m_lastSearchText = currentText;
try {
// Clear the gallery and prepare for new search with pagination
m_imageGallery->clearGallery();
try {
m_imageGallery->handleSearchTextChanged(currentText);
updateStatusBar();
} catch (const std::exception &e) {
QString errorMsg = tr("Search error: %1").arg(e.what());
statusBar()->showMessage(errorMsg, 5000);
qDebug() << errorMsg;
// Directly pass the search to the image gallery
// The gallery will handle the threaded search operation with pagination
m_imageGallery->handleSearchTextChanged(m_lastSearchText);
updateStatusBar();
// Show searching status
statusBar()->showMessage(tr("Searching..."), 1000);
} catch (const std::exception &e) {
QString errorMsg = tr("Search error: %1").arg(e.what());
statusBar()->showMessage(errorMsg, 5000);
qDebug() << errorMsg;
}
}
// File menu action handlers
void MainWindow::handleOpenFile()
{
QString filePath = QFileDialog::getOpenFileName(this,
tr("Open Image File"), m_screenshotsDir,
tr("Image Files (*.png *.jpg *.jpeg *.bmp *.gif)"));
if (!filePath.isEmpty()) {
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
}
}
void MainWindow::handleOpenFileWith()
{
QString filePath = QFileDialog::getOpenFileName(this,
tr("Open Image File"), m_screenshotsDir,
tr("Image Files (*.png *.jpg *.jpeg *.bmp *.gif)"));
if (!filePath.isEmpty()) {
// Get the program to open the file with
bool ok;
QString program = QInputDialog::getText(this, tr("Open With"),
tr("Enter program name:"), QLineEdit::Normal,
"", &ok);
if (ok && !program.isEmpty()) {
QProcess *process = new QProcess(this);
connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
process, &QProcess::deleteLater);
process->start(program, QStringList() << filePath);
}
}
}
void MainWindow::handleSettings()
{
SettingsDialog settingsDialog(this);
// Show the settings dialog
if (settingsDialog.exec() == QDialog::Accepted) {
// Save and apply new settings
m_databasePath = settingsDialog.getDatabasePath();
m_screenshotsDir = settingsDialog.getScreenshotsDir();
m_imagePreloadCount = settingsDialog.getImagePreloadCount();
// Apply the new settings
applySettings();
}
}
void MainWindow::loadSettings()
{
// Ensure config directory exists
QFileInfo configFile(CONFIG_FILE_PATH);
QDir configDir = configFile.absoluteDir();
if (!configDir.exists()) {
configDir.mkpath(".");
}
QSettings settings(CONFIG_FILE_PATH, QSettings::IniFormat);
// Load database path with fallback to default
m_databasePath = settings.value("databasePath", "/home/master/screenshot_ocr.db").toString();
// Load screenshots directory with fallback to home/Screenshots
m_screenshotsDir = settings.value("screenshotsDir", QDir::homePath() + "/Screenshots").toString();
// Load preload count
m_imagePreloadCount = settings.value("imagePreloadCount", DEFAULT_PRELOAD_COUNT).toInt();
}
void MainWindow::applySettings()
{
// Re-initialize database with new path
bool wasValid = m_hasValidDatabase;
m_hasValidDatabase = false;
// Try to initialize the database with the new path
initializeDatabase();
// Show appropriate message
if (!wasValid && m_hasValidDatabase) {
statusBar()->showMessage(tr("Successfully connected to database"), 3000);
} else if (wasValid && !m_hasValidDatabase) {
statusBar()->showMessage(tr("Lost connection to database"), 5000);
} else if (!wasValid && !m_hasValidDatabase) {
statusBar()->showMessage(tr("Could not connect to database"), 5000);
} else {
// Update page size in ImageGallery
if (m_imageGallery) {
m_imageGallery->setPageSize(m_imagePreloadCount);
}
// Refresh gallery with new database
displayAllImages();
// Ensure the status is updated with correct count
int totalImageCount = m_dbManager->getImageCount();
statusBar()->showMessage(tr("Database loaded with %1 total images").arg(totalImageCount), 3000);
}
}
void MainWindow::updateStatusBar()
{
if (!m_hasValidDatabase) {
@@ -190,16 +446,15 @@ void MainWindow::updateStatusBar()
}
try {
int imageCount = m_dbManager->searchImages(m_lastSearchText).count();
int totalImages = m_dbManager->getAllImages().count();
// Get the total count of images in the database
int totalImages = m_dbManager->getImageCount();
if (m_lastSearchText.isEmpty()) {
statusBar()->showMessage(tr("Displaying all %1 images").arg(totalImages));
statusBar()->showMessage(tr("Displaying images (paginated) - %1 total").arg(totalImages));
} else {
statusBar()->showMessage(tr("Found %1 of %2 images matching \"%3\"")
.arg(imageCount)
.arg(totalImages)
.arg(m_lastSearchText));
// Status message will be updated when search results arrive
// via the searchResultsReady signal
statusBar()->showMessage(tr("Searching..."));
}
} catch (const std::exception &e) {
statusBar()->showMessage(tr("Error updating status: %1").arg(e.what()), 5000);
+20 -4
View File
@@ -26,6 +26,12 @@ private slots:
void handleSearchTextChanged();
void performSearch();
void updateStatusBar();
void handleTypingInactivityTimeout();
void handleOpenFile();
void handleOpenFileWith();
void handleSettings();
void applySettings();
void loadSettings();
private:
void createLayout();
@@ -36,20 +42,30 @@ private:
QWidget *m_centralWidget;
QVBoxLayout *m_mainLayout;
QLineEdit *m_searchBar;
QLabel *m_titleLabel;
ImageGallery *m_imageGallery;
// Data
DatabaseManager *m_dbManager;
QString m_lastSearchText;
QTimer *m_searchDelayTimer;
QTimer *m_typingTimer;
QTimer *m_searchingAnimationTimer;
int m_searchingDots;
bool m_isTyping;
bool m_hasValidDatabase;
// Settings
QString m_databasePath;
QString m_screenshotsDir;
int m_imagePreloadCount;
// Settings file path
static const QString CONFIG_FILE_PATH;
// Constants
static constexpr int SEARCH_DELAY_MS = 50; // Reduced delay for more responsive typing
static constexpr int SEARCH_DELAY_MS = 300; // Delay for search typing
static constexpr int DEFAULT_WINDOW_WIDTH = 1200;
static constexpr int DEFAULT_WINDOW_HEIGHT = 800;
static const QString DEFAULT_DB_PATH;
static constexpr int DEFAULT_PRELOAD_COUNT = 20;
};
#endif // MAINWINDOW_H
+260
View File
@@ -0,0 +1,260 @@
#include "settingsdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFileInfo>
#include <QStandardPaths>
#include <QIntValidator>
// Define static constants
const QString SettingsDialog::DEFAULT_SCREENSHOTS_DIR = QDir::homePath() + "/Screenshots";
const QString SettingsDialog::DEFAULT_DATABASE_FILENAME = "screenshot_ocr.db";
const QString SettingsDialog::CONFIG_FILE_PATH = QDir::homePath() + "/.config/ScreenshotOCRGallery/settings.ini";
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Settings"));
setMinimumWidth(500);
// Create UI elements
createLayout();
// Load saved settings
loadSettings();
// Connect signals and slots
connect(m_browseScreenshotsDirBtn, &QPushButton::clicked, this, &SettingsDialog::handleBrowseScreenshotsDir);
connect(m_browseDatabaseBtn, &QPushButton::clicked, this, &SettingsDialog::handleBrowseDatabase);
connect(m_buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::handleAccepted);
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
SettingsDialog::~SettingsDialog()
{
// Qt will handle cleanup of UI elements through parent-child relationships
}
void SettingsDialog::createLayout()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
QGridLayout *formLayout = new QGridLayout();
// Ensure config directory exists
QFileInfo configFile(CONFIG_FILE_PATH);
QDir configDir = configFile.absoluteDir();
if (!configDir.exists()) {
configDir.mkpath(".");
}
// Screenshots directory row
QLabel *screenshotsDirLabel = new QLabel(tr("Screenshots Directory:"), this);
m_screenshotsDirEdit = new QLineEdit(this);
m_browseScreenshotsDirBtn = new QPushButton(tr("..."), this);
m_browseScreenshotsDirBtn->setMaximumWidth(40);
formLayout->addWidget(screenshotsDirLabel, 0, 0);
formLayout->addWidget(m_screenshotsDirEdit, 0, 1);
formLayout->addWidget(m_browseScreenshotsDirBtn, 0, 2);
// Database path row
QLabel *databasePathLabel = new QLabel(tr("Database File Path:"), this);
m_databasePathEdit = new QLineEdit(this);
m_browseDatabaseBtn = new QPushButton(tr("..."), this);
m_browseDatabaseBtn->setMaximumWidth(40);
formLayout->addWidget(databasePathLabel, 1, 0);
formLayout->addWidget(m_databasePathEdit, 1, 1);
formLayout->addWidget(m_browseDatabaseBtn, 1, 2);
// Preload count row
QLabel *preloadCountLabel = new QLabel(tr("Images to pre-load:"), this);
m_imagePreloadCountEdit = new QLineEdit(this);
m_imagePreloadCountEdit->setValidator(new QIntValidator(1, 100, this));
formLayout->addWidget(preloadCountLabel, 2, 0);
formLayout->addWidget(m_imagePreloadCountEdit, 2, 1);
// Help text
QLabel *helpText = new QLabel(tr("Note: If no filename is provided for the database path, "
"'screenshot_ocr.db' will be used automatically."), this);
helpText->setWordWrap(true);
helpText->setStyleSheet("color: #666; font-size: 11px;");
// Button box
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
// Add to main layout
mainLayout->addLayout(formLayout);
mainLayout->addWidget(helpText);
mainLayout->addSpacing(10);
mainLayout->addWidget(m_buttonBox);
setLayout(mainLayout);
}
void SettingsDialog::handleBrowseScreenshotsDir()
{
QString dir = QFileDialog::getExistingDirectory(
this, tr("Select Screenshots Directory"),
m_screenshotsDirEdit->text().isEmpty() ? QDir::homePath() : m_screenshotsDirEdit->text(),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
m_screenshotsDirEdit->setText(dir);
}
}
void SettingsDialog::handleBrowseDatabase()
{
QString currentPath = m_databasePathEdit->text();
if (currentPath.isEmpty()) {
currentPath = QDir::homePath();
} else {
QFileInfo fileInfo(currentPath);
if (fileInfo.isFile()) {
currentPath = fileInfo.absolutePath();
}
}
QString filePath = QFileDialog::getSaveFileName(
this, tr("Select Database File"),
currentPath,
tr("SQLite Database (*.db);;All Files (*)"));
if (!filePath.isEmpty()) {
m_databasePathEdit->setText(filePath);
}
}
void SettingsDialog::handleAccepted()
{
// Validate settings
QDir screenshotsDir(m_screenshotsDirEdit->text());
if (!screenshotsDir.exists()) {
// Ask if we should create the directory
QMessageBox::StandardButton reply = QMessageBox::question(
this, tr("Directory Not Found"),
tr("The screenshots directory does not exist. Create it?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
if (!screenshotsDir.mkpath(".")) {
QMessageBox::warning(this, tr("Error"),
tr("Failed to create directory: %1").arg(screenshotsDir.path()));
return;
}
} else {
// User chose not to create directory
return;
}
}
// Ensure database path has a filename
m_databasePathEdit->setText(ensureDatabaseFilename(m_databasePathEdit->text()));
// Check if database directory exists
QFileInfo dbFileInfo(m_databasePathEdit->text());
QDir dbDir = dbFileInfo.absoluteDir();
if (!dbDir.exists()) {
QMessageBox::StandardButton reply = QMessageBox::question(
this, tr("Directory Not Found"),
tr("The database directory does not exist. Create it?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
if (!dbDir.mkpath(".")) {
QMessageBox::warning(this, tr("Error"),
tr("Failed to create directory: %1").arg(dbDir.path()));
return;
}
} else {
// User chose not to create directory
return;
}
}
// Save settings
saveSettings();
// Accept dialog
accept();
}
QString SettingsDialog::getScreenshotsDir() const
{
return m_screenshotsDirEdit->text();
}
QString SettingsDialog::getDatabasePath() const
{
return m_databasePathEdit->text();
}
void SettingsDialog::loadSettings()
{
QSettings settings(CONFIG_FILE_PATH, QSettings::IniFormat);
// Load screenshots directory
QString screenshotsDir = settings.value("screenshotsDir", DEFAULT_SCREENSHOTS_DIR).toString();
m_screenshotsDirEdit->setText(screenshotsDir);
// Load database path
QString databasePath = settings.value("databasePath", QDir::homePath() + "/" + DEFAULT_DATABASE_FILENAME).toString();
m_databasePathEdit->setText(databasePath);
// Load preload count
int preloadCount = settings.value("imagePreloadCount", DEFAULT_PRELOAD_COUNT).toInt();
m_imagePreloadCountEdit->setText(QString::number(preloadCount));
}
void SettingsDialog::saveSettings()
{
QSettings settings(CONFIG_FILE_PATH, QSettings::IniFormat);
// Save screenshots directory
settings.setValue("screenshotsDir", getScreenshotsDir());
// Save database path
settings.setValue("databasePath", getDatabasePath());
// Save preload count
settings.setValue("imagePreloadCount", getImagePreloadCount());
settings.sync();
}
int SettingsDialog::getImagePreloadCount() const
{
bool ok;
int count = m_imagePreloadCountEdit->text().toInt(&ok);
// Return the default if conversion fails or value is invalid
if (!ok || count < 1) {
return DEFAULT_PRELOAD_COUNT;
}
return count;
}
QString SettingsDialog::ensureDatabaseFilename(const QString &path)
{
if (path.isEmpty()) {
// If path is empty, use default in home directory
return QDir::homePath() + "/" + DEFAULT_DATABASE_FILENAME;
}
QFileInfo fileInfo(path);
if (fileInfo.isDir()) {
// If path is a directory, append default filename
QString dirPath = path;
// Ensure path ends with a separator
if (!dirPath.endsWith('/') && !dirPath.endsWith('\\')) {
dirPath += '/';
}
return dirPath + DEFAULT_DATABASE_FILENAME;
}
// If path already has a filename component, use it as is
return path;
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QGridLayout>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QSettings>
#include <QDir>
#include <QMessageBox>
class SettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit SettingsDialog(QWidget *parent = nullptr);
~SettingsDialog();
// Get settings values
QString getScreenshotsDir() const;
QString getDatabasePath() const;
int getImagePreloadCount() const;
// Load settings from persistent storage
void loadSettings();
// Save settings to persistent storage
void saveSettings();
private slots:
// Button click handlers
void handleBrowseScreenshotsDir();
void handleBrowseDatabase();
void handleAccepted();
private:
// UI Elements
QLineEdit *m_screenshotsDirEdit;
QLineEdit *m_databasePathEdit;
QLineEdit *m_imagePreloadCountEdit;
QPushButton *m_browseScreenshotsDirBtn;
QPushButton *m_browseDatabaseBtn;
QDialogButtonBox *m_buttonBox;
// Helper methods
void createLayout();
QString ensureDatabaseFilename(const QString &path);
// Default settings
static const QString DEFAULT_SCREENSHOTS_DIR;
static const QString DEFAULT_DATABASE_FILENAME;
static const QString CONFIG_FILE_PATH;
static const int DEFAULT_PRELOAD_COUNT = 20;
};
#endif // SETTINGSDIALOG_H