helper scripts
This commit is contained in:
+603
-92
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user