rebrand to orc-gallery and optimise search/thumbnail responsiveness

Rebrand:
- Rename project, binary, desktop file, and icons from screenshot-gallery/orcs-gallery to orc-gallery
- Update display name to "ORC Gallery" throughout (window title, .desktop, README, plan.md)
- Config dir changed from ScreenshotOCRGallery to OrcGallery

Performance:
- cancelSearch() is now non-blocking; new searches start immediately instead of waiting for the previous thread to drain
- Remove double-emit from QFutureWatcher::finished handler; results emitted once directly from background thread
- SQLite WAL mode + NORMAL sync + 20 MB page cache + memory temp store + 256 MB mmap applied on init and per-thread connections
- Typing inactivity timer reduced from 500 ms to 150 ms
- COUNT(*) OVER() window function folds total-count into the main SELECT, eliminating a separate COUNT round-trip per search
- ImageThumbnail::paintEvent draws the filename overlay; removes 3 child QObjects (QFrame + QLabel + QHBoxLayout) per thumbnail
- loadThumbnailAsync posts QImage load+scale to QThreadPool; QPixmap::fromImage called on main thread via Qt::QueuedConnection invokeMethod; QPointer guards against use-after-free on gallery clear

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yzinchuk
2026-07-03 03:53:07 -04:00
parent 3fb8d7e507
commit 55454d7ef6
23 changed files with 231 additions and 315 deletions
+13 -13
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(screenshot-gallery VERSION 1.0.0 LANGUAGES CXX)
project(orc-gallery VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -29,23 +29,23 @@ set(PROJECT_SOURCES
src/updatedatabasedialog.h
)
add_executable(screenshot-gallery ${PROJECT_SOURCES} ${RESOURCE_FILES})
add_executable(orc-gallery ${PROJECT_SOURCES} ${RESOURCE_FILES})
# Install icons to standard system locations
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-64.png"
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-64.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps"
RENAME "orcs-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-128.png"
RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-128.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps"
RENAME "orcs-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-256.png"
RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-256.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps"
RENAME "orcs-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-512.png"
RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-512.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps"
RENAME "orcs-gallery.png")
RENAME "orc-gallery.png")
target_link_libraries(screenshot-gallery PRIVATE
target_link_libraries(orc-gallery PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
@@ -53,11 +53,11 @@ target_link_libraries(screenshot-gallery PRIVATE
Qt6::Concurrent
)
install(TARGETS screenshot-gallery
install(TARGETS orc-gallery
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# Install desktop file
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/screenshot-gallery.desktop"
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/orc-gallery.desktop"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications")
+1 -1
View File
@@ -4,7 +4,7 @@
# This script displays OCR data from SQLite and allows opening files with rofi
# Database path
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/screenshot-gallery/screenshot_ocr.db"
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/orc-gallery/screenshot_ocr.db"
# Check dependencies
check_deps() {
+1 -1
View File
@@ -4,7 +4,7 @@
# This script displays OCR data from SQLite and allows opening files with rofi
# Database path
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/screenshot-gallery/screenshot_ocr.db"
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/orc-gallery/screenshot_ocr.db"
SCREENSHOTS_DIR="$HOME/Screenshots"
MAX_TEXT_LENGTH=100
+3 -3
View File
@@ -7,8 +7,8 @@ Usage:
Arguments:
--db Path to the SQLite database file.
Default: $XDG_DATA_HOME/screenshot-gallery/screenshot_ocr.db
(~/.local/share/screenshot-gallery/screenshot_ocr.db)
Default: $XDG_DATA_HOME/orc-gallery/screenshot_ocr.db
(~/.local/share/orc-gallery/screenshot_ocr.db)
--screenshots-dir Directory containing screenshot images to process.
Default: ~/Screenshots
"""
@@ -53,7 +53,7 @@ signal.signal(signal.SIGINT, _handle_signal)
# ---------------------------------------------------------------------------
def parse_args() -> argparse.Namespace:
xdg_data = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
default_db = os.path.join(xdg_data, "screenshot-gallery", "screenshot_ocr.db")
default_db = os.path.join(xdg_data, "orc-gallery", "screenshot_ocr.db")
default_dir = os.path.expanduser("~/Screenshots")
parser = argparse.ArgumentParser(
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
import tempfile
# Configuration
DB_PATH = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "screenshot-gallery", "screenshot_ocr.db")
DB_PATH = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "orc-gallery", "screenshot_ocr.db")
SCREENSHOTS_DIR = os.path.expanduser("~/Screenshots")
ROFI_PROMPT = "OCR Search"
+1 -1
View File
@@ -4,7 +4,7 @@
# This script displays OCR'd screenshot data in rofi and allows opening selected files
# Constants
DATABASE_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/screenshot-gallery/screenshot_ocr.db"
DATABASE_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/orc-gallery/screenshot_ocr.db"
SCREENSHOTS_DIR="$HOME/Screenshots"
ROFI_PROMPT="Screenshot OCR"
MAX_DISPLAY_LENGTH=100
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
from datetime import datetime
# Constants
DATABASE_PATH = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "screenshot-gallery", "screenshot_ocr.db")
DATABASE_PATH = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "orc-gallery", "screenshot_ocr.db")
def parse_arguments():
+1 -1
View File
@@ -3,7 +3,7 @@
# simple_rofi_ocr.sh - A simplified script to search OCR'd screenshots with rofi
# Database location
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/screenshot-gallery/screenshot_ocr.db"
DB_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/orc-gallery/screenshot_ocr.db"
# Check if database exists
if [ ! -f "$DB_PATH" ]; then
+8 -8
View File
@@ -1,4 +1,4 @@
# Screenshot OCR Gallery
# ORC Gallery
A Qt6-based image gallery application that allows you to search through OCR data from your screenshots with live preview and dynamic resizing.
@@ -9,7 +9,7 @@ A Qt6-based image gallery application that allows you to search through OCR data
- Smart typing detection with 500ms inactivity timer before searching
- Visual feedback while typing and searching with animated status indicators
- Settings dialog to customize database location and screenshots directory
- Settings stored in ~/.config/ScreenshotOCRGallery/settings.ini for easy access
- Settings stored in ~/.config/OrcGallery/settings.ini for easy access
- Customizable image preload count for performance tuning
- Prominent "Load More Images" button for easy one-click pagination
- Optimized lazy loading that only loads images when needed
@@ -44,7 +44,7 @@ sudo apt install qt6-base-dev libqt6sql6-sqlite cmake
## Database Requirements
The application stores its SQLite database at `~/.local/share/screenshot-gallery/screenshot_ocr.db` (following the XDG Base Directory Specification). The directory and an empty database are created automatically on first run if they do not exist. The path can be changed via the Settings dialog.
The application stores its SQLite database at `~/.local/share/orc-gallery/screenshot_ocr.db` (following the XDG Base Directory Specification). The directory and an empty database are created automatically on first run if they do not exist. The path can be changed via the Settings dialog.
The required schema is:
@@ -67,7 +67,7 @@ CREATE TABLE ocr_results (
3. Run the build script:
```bash
cd screenshot-gallery
cd orc-gallery
chmod +x build.sh
./build.sh
```
@@ -95,7 +95,7 @@ make
After building, run the application:
```bash
./build/screenshot-gallery
./build/orc-gallery
```
## Usage
@@ -125,7 +125,7 @@ After building, run the application:
- Database File Path: Change where your OCR database is stored
- Screenshots Directory: Set the default location for your screenshots
- Image count to pre-load: Adjust how many images are loaded at once (default: 20)
- The settings are automatically saved to ~/.config/ScreenshotOCRGallery/settings.ini
- The settings are automatically saved to ~/.config/OrcGallery/settings.ini
- Changes are applied immediately without requiring a restart
## Search Technology
@@ -135,7 +135,7 @@ This application combines multiple performance-enhancing technologies:
### 1. Customizable Configuration
- **Settings Dialog:** Easily configure database location, screenshots directory, and preload count
- **File Path Selection:** Browse for locations using native file pickers
- **Persistent Settings:** Your configuration is saved in ~/.config/ScreenshotOCRGallery/settings.ini
- **Persistent Settings:** Your configuration is saved in ~/.config/OrcGallery/settings.ini
- **Performance Tuning:** Adjust how many images are preloaded (20 by default)
- **Dynamic Updates:** Changes are applied immediately without requiring a restart
@@ -176,7 +176,7 @@ This application combines multiple performance-enhancing technologies:
### Database Connection Issues
If the application cannot connect to the database:
- The default location is `~/.local/share/screenshot-gallery/screenshot_ocr.db`
- The default location is `~/.local/share/orc-gallery/screenshot_ocr.db`
- Check file permissions on the database file and its parent directory
- Verify the database has the required schema (the app will create an empty one automatically)
+4 -4
View File
@@ -80,8 +80,8 @@ run_application() {
print_message "Running application..."
# Check if the application exists
if [ -f "build/screenshot-gallery" ]; then
./build/screenshot-gallery
if [ -f "build/orc-gallery" ]; then
./build/orc-gallery
else
print_error "Application not found. Build might have failed."
exit 1
@@ -89,7 +89,7 @@ run_application() {
}
# -------- Main script execution --------
print_message "Starting build process for Screenshot Gallery..."
print_message "Starting build process for ORC Gallery..."
check_dependencies
create_build_dir
@@ -104,5 +104,5 @@ echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
run_application
else
print_message "You can run the application later using: ./build/screenshot-gallery"
print_message "You can run the application later using: ./build/orc-gallery"
fi

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@@ -1,11 +1,11 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Screenshot OCR Gallery
Name=ORC Gallery
Comment=Search and browse screenshots by OCR content
Exec=screenshot-gallery
Exec=orc-gallery
Terminal=false
Categories=Graphics;Utility;Viewer;
Keywords=screenshots;ocr;gallery;search;images;
Icon=orcs-gallery
Icon=orc-gallery
StartupNotify=true
+34 -25
View File
@@ -1,4 +1,4 @@
# OCR Screenshot Gallery — Project Reference
# ORC Gallery — Project Reference
A Qt6 desktop application for Linux that OCRs screenshots and provides a searchable, paginated gallery interface. Users run a Python OCR script to populate a SQLite database, then browse and search all recognized text visually.
@@ -7,12 +7,12 @@ A Qt6 desktop application for Linux that OCRs screenshots and provides a searcha
## Directory Structure
```
ocr-screenshot-gallery/
orc-gallery/
├── CMakeLists.txt # CMake build configuration (Qt6)
├── build.sh # One-shot build automation script
├── README.md # User-facing documentation
├── resources.qrc # Qt resource file bundling icons
├── screenshot-gallery.desktop # Linux XDG desktop entry
├── orc-gallery.desktop # Linux XDG desktop entry
├── .gitignore
├── src/ # C++ application source
@@ -34,13 +34,13 @@ ocr-screenshot-gallery/
│ └── screenshot_ocr.db # Example/seed database
├── icons/ # Application icon assets
│ ├── orcs-gallery-512.png
│ ├── orcs-gallery-256.png
│ ├── orcs-gallery-128.png
│ └── orcs-gallery-64.png
│ ├── orc-gallery-512.png
│ ├── orc-gallery-256.png
│ ├── orc-gallery-128.png
│ └── orc-gallery-64.png
└── build/ # CMake build output (git-ignored)
└── screenshot-gallery # Compiled executable
└── orc-gallery # Compiled executable
```
---
@@ -86,20 +86,20 @@ make -j$(nproc)
### Run
```bash
./build/screenshot-gallery
./build/orc-gallery
```
### Install system-wide
```bash
cd build && sudo make install
screenshot-gallery # from anywhere
orc-gallery # from anywhere
```
Installation puts:
- Binary → `${CMAKE_INSTALL_BINDIR}` (typically `/usr/local/bin/`)
- Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orcs-gallery.png`
- Desktop entry → `/share/applications/screenshot-gallery.desktop`
- Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orc-gallery.png`
- Desktop entry → `/share/applications/orc-gallery.desktop`
---
@@ -107,9 +107,9 @@ Installation puts:
### Location
Default (XDG-compliant): `~/.local/share/screenshot-gallery/screenshot_ocr.db`
Default (XDG-compliant): `~/.local/share/orc-gallery/screenshot_ocr.db`
Configurable in Settings dialog; stored in `~/.config/ScreenshotOCRGallery/settings.ini`.
Configurable in Settings dialog; stored in `~/.config/OrcGallery/settings.ini`.
### Schema
@@ -162,7 +162,7 @@ Responsibilities:
- Search bar (`QLineEdit`) with a 500 ms typing-inactivity timer (`m_typingTimer`) — search only fires after the user stops typing
- Animated "Searching…" status bar indicator (`m_searchingAnimationTimer`)
- Instantiates and owns `DatabaseManager` and `ImageGallery`
- Reads/writes settings via `QSettings` (INI at `~/.config/ScreenshotOCRGallery/settings.ini`)
- Reads/writes settings via `QSettings` (INI at `~/.config/OrcGallery/settings.ini`)
- On search complete: updates status bar with result count
- On clear search: reloads all images (first page)
@@ -282,7 +282,7 @@ Main OCR runner. Scans a screenshots directory, runs Tesseract on each image not
```bash
python3 OCR-scripts/ocr_screenshots.py \
--db ~/.local/share/screenshot-gallery/screenshot_ocr.db \
--db ~/.local/share/orc-gallery/screenshot_ocr.db \
--screenshots-dir ~/Screenshots
```
@@ -307,11 +307,11 @@ These scripts hook OCR-screenshot workflows into the [Rofi](https://github.com/d
## Settings & Configuration
**Settings file:** `~/.config/ScreenshotOCRGallery/settings.ini`
**Settings file:** `~/.config/OrcGallery/settings.ini`
```ini
[General]
databasePath=/home/<user>/.local/share/screenshot-gallery/screenshot_ocr.db
databasePath=/home/<user>/.local/share/orc-gallery/screenshot_ocr.db
screenshotsDir=/home/<user>/Screenshots
imagePreloadCount=20
```
@@ -344,28 +344,33 @@ All three keys have fallbacks defined in `MainWindow::loadSettings()` and `Setti
| Repeated searches | 5-minute LRU cache in `DatabaseManager` |
| Large galleries | Explicit pagination (default 20, user-controlled via Load More) |
| Column reflow cost | 300 ms debounce on resize events |
| Typing lag | 500 ms inactivity timer before issuing query |
| Typing lag | 150 ms inactivity timer before issuing query |
| Thread-safe DB access | Per-thread `QSqlDatabase` connections + `QMutex` guards |
| Missing files | Placeholder thumbnail rendered inline; no crash |
| Cancel stall | Non-blocking cancel: flag set immediately, old thread drains in background |
| COUNT round-trip | `COUNT(*) OVER()` window function folds count into the main SELECT |
| SQLite I/O | WAL journal + 20 MB page cache + memory-mapped I/O via PRAGMA on init |
| Thumbnail load stall | `QImage` loaded/scaled on `QThreadPool`, marshalled to main thread via `invokeMethod` |
| Overlay widget cost | Filename drawn in `ImageThumbnail::paintEvent` — no child QFrame/QLabel/QHBoxLayout |
---
## Linux Desktop Integration
**`screenshot-gallery.desktop`**
**`orc-gallery.desktop`**
```ini
[Desktop Entry]
Type=Application
Name=Screenshot OCR Gallery
Icon=orcs-gallery
Exec=screenshot-gallery
Name=ORC Gallery
Icon=orc-gallery
Exec=orc-gallery
Categories=Graphics;Utility;Viewer;
Keywords=screenshots;ocr;gallery;search;images;
```
XDG paths used by the application:
- Config: `QStandardPaths::AppConfigLocation``~/.config/ScreenshotOCRGallery/`
- Data: `QStandardPaths::AppLocalDataLocation``~/.local/share/screenshot-gallery/`
- Config: `QStandardPaths::AppConfigLocation``~/.config/OrcGallery/`
- Data: `QStandardPaths::AppLocalDataLocation``~/.local/share/orc-gallery/`
---
@@ -378,3 +383,7 @@ XDG paths used by the application:
- The `UpdateDatabaseDialog` is instantiated fresh each time (not reused) to avoid stale process state.
- `ImageGallery` does not use a `QAbstractItemModel`; it builds `ImageThumbnail` widgets directly and manages them in a flat list (`m_thumbnails`).
- Database connections must not be shared across threads — each `QtConcurrent` task opens its own named connection and closes it on exit.
- `cancelSearch()` is non-blocking: it sets `m_searchCancelled` and returns immediately. The background thread checks the flag at multiple points and exits early. The destructor sets the flag then calls `waitForFinished()` for clean shutdown.
- The watcher's `finished` signal is not used for emitting results (was causing double-emit). Results are emitted directly from `performSearchInBackground` once and only if `m_currentSearchText == searchText`.
- `ImageThumbnail::paintEvent` draws the filename overlay; no child widgets needed. The `m_displayName` field is pre-truncated in the constructor.
- Async thumbnail loading: `loadThumbnailAsync` posts to `QThreadPool::globalInstance()`. `QImage` is loaded and `.scaled()` off the main thread; `QPixmap::fromImage()` is called inside a `Qt::QueuedConnection` invokeMethod lambda. A `QPointer<ImageThumbnail>` guard prevents use-after-free if the gallery clears before the load completes.
+4 -4
View File
@@ -1,8 +1,8 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
<file>icons/orcs-gallery-64.png</file>
<file>icons/orcs-gallery-128.png</file>
<file>icons/orcs-gallery-256.png</file>
<file>icons/orcs-gallery-512.png</file>
<file>icons/orc-gallery-64.png</file>
<file>icons/orc-gallery-128.png</file>
<file>icons/orc-gallery-256.png</file>
<file>icons/orc-gallery-512.png</file>
</qresource>
</RCC>
+56 -105
View File
@@ -20,28 +20,8 @@ DatabaseManager::DatabaseManager(QObject *parent)
m_allImagesCache.clear();
m_lastCacheUpdate = QDateTime::currentDateTime();
// 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);
}
}
}
});
// Watcher is used only for lifecycle tracking; results are emitted directly
// from performSearchInBackground to avoid double-emit.
// Clean cache periodically
QTimer *cleanupTimer = new QTimer(this);
@@ -51,8 +31,13 @@ DatabaseManager::DatabaseManager(QObject *parent)
DatabaseManager::~DatabaseManager()
{
// Cancel any ongoing search and wait for it to finish
cancelSearch();
// Signal cancellation and wait for the background thread to drain cleanly.
{
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
}
if (m_searchFuture.isRunning())
m_searchFuture.waitForFinished();
if (m_db.isOpen()) {
m_db.close();
@@ -182,6 +167,17 @@ bool DatabaseManager::initialize(const QString &dbPath)
m_ftsEnabled = false;
}
// Performance pragmas applied once on the main connection.
// WAL allows concurrent reads alongside the OCR writer.
// cache_size=-20000 sets a 20 MB page cache.
// mmap_size enables memory-mapped I/O for faster sequential reads.
QSqlQuery pragmaQuery;
pragmaQuery.exec("PRAGMA journal_mode=WAL");
pragmaQuery.exec("PRAGMA synchronous=NORMAL");
pragmaQuery.exec("PRAGMA cache_size=-20000");
pragmaQuery.exec("PRAGMA temp_store=MEMORY");
pragmaQuery.exec("PRAGMA mmap_size=268435456");
m_initialized = true;
qDebug() << "Database initialized successfully.";
return true;
@@ -318,9 +314,12 @@ QSqlDatabase DatabaseManager::getDatabaseConnection()
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");
query.exec("PRAGMA journal_mode=WAL");
query.exec("PRAGMA synchronous=NORMAL");
query.exec("PRAGMA cache_size=-20000");
query.exec("PRAGMA temp_store=MEMORY");
query.exec("PRAGMA mmap_size=268435456");
}
return threadDb;
@@ -397,16 +396,11 @@ void DatabaseManager::searchImages(const QString &searchText, int offset, int li
void DatabaseManager::cancelSearch()
{
// Set cancelled flag
// Non-blocking: just set the flag. The background thread checks it at
// multiple points and exits early. The new search starts immediately.
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
m_currentSearchText.clear();
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)
@@ -428,34 +422,8 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
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);
@@ -464,22 +432,22 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Start transaction to speed up query
// Single query: COUNT(*) OVER() gives the total match count without a
// separate round-trip. SQLite evaluates window functions before LIMIT, so
// the count reflects the full result set even when we page with LIMIT/OFFSET.
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 "
QString queryStr =
"SELECT r.id, r.full_path, r.ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results r "
"JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query "
"ORDER BY rank";
if (limit > 0) {
if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":query", ftsQuery);
@@ -488,57 +456,40 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
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";
QString whereClause = (searchText.length() <= 3)
? "ocr_text LIKE :search"
: "ocr_text LIKE :search OR ocr_text LIKE :wordstart";
if (limit > 0) {
QString queryStr =
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results WHERE " + whereClause + " ORDER BY id";
if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset";
}
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 + "%");
if (searchText.length() > 3)
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();
qDebug() << "Search query failed:" << query.lastError().text();
threadDb.rollback();
// If FTS query failed, try fallback to LIKE
// FTS failure: fall back to LIKE for this request
if (m_ftsEnabled) {
qDebug() << "Trying fallback to LIKE query...";
qDebug() << "Falling back to LIKE query";
threadDb.transaction();
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search");
query.prepare(
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results WHERE ocr_text LIKE :search ORDER BY id");
query.bindValue(":search", "%" + searchText + "%");
if (!query.exec()) {
qDebug() << "Fallback query also failed:" << query.lastError().text();
qDebug() << "Fallback query failed:" << query.lastError().text();
threadDb.rollback();
return;
}
@@ -547,7 +498,7 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Check if search was cancelled
// Check cancellation again after query execution
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
@@ -556,11 +507,9 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Reserve space for results to avoid reallocations
images.reserve(query.size() > 0 ? query.size() : 100);
images.reserve(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) {
@@ -569,16 +518,18 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Read total count from the window function column (same on every row)
if (totalCount == 0)
totalCount = query.value(3).toInt();
ImageItem item;
item.id = query.value(0).toInt();
item.filePath = query.value(1).toString();
item.ocrText = query.value(2).toString();
// Only add images that have a non-empty path
if (!item.filePath.isEmpty()) {
if (!item.filePath.isEmpty())
images.append(item);
}
}
threadDb.commit();
+52 -100
View File
@@ -24,14 +24,33 @@ ImageThumbnail::ImageThumbnail(const QString &filePath, QWidget *parent)
setLineWidth(2);
setScaledContents(false);
setCursor(Qt::PointingHandCursor);
// We're removing tooltips as requested
// Enable text wrapping and text interaction
setWordWrap(true);
setTextInteractionFlags(Qt::TextSelectableByMouse);
// Set a minimum size policy
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
if (!filePath.isEmpty()) {
QString name = QFileInfo(filePath).fileName();
if (name.length() > 30)
name = name.left(13) + "..." + name.right(13);
m_displayName = name;
}
}
void ImageThumbnail::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
if (m_displayName.isEmpty())
return;
QPainter painter(this);
const int overlayH = 22;
QRect overlayRect(0, height() - overlayH, width(), overlayH);
painter.fillRect(overlayRect, QColor(0, 0, 0, 178));
painter.setPen(Qt::white);
QFont f = painter.font();
f.setPointSize(9);
painter.setFont(f);
painter.drawText(overlayRect, Qt::AlignCenter, m_displayName);
}
void ImageThumbnail::mousePressEvent(QMouseEvent *event)
@@ -207,39 +226,9 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
continue; // Skip this image
}
// Create and add thumbnail
QPixmap thumbnail = createThumbnail(item.filePath, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
ImageThumbnail *thumbnailLabel = new ImageThumbnail(item.filePath, this);
thumbnailLabel->setPixmap(thumbnail);
// Add filename overlay at the bottom of the thumbnail
QFileInfo fileNameInfo(item.filePath);
QString fileName = fileNameInfo.fileName();
// 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);
// Create label for the filename
QLabel* fileNameLabel = new QLabel(fileName, overlay);
fileNameLabel->setStyleSheet("color: white; background: transparent;");
fileNameLabel->setAlignment(Qt::AlignCenter);
// 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);
overlayLayout->setContentsMargins(5, 0, 5, 0);
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());
thumbnailLabel->setPixmap(createPlaceholderThumbnail(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, ""));
loadThumbnailAsync(thumbnailLabel, item.filePath);
// Connect the thumbnail click signal
connect(thumbnailLabel, &ImageThumbnail::thumbnailClicked,
@@ -546,26 +535,32 @@ void ImageGallery::handleThumbnailClicked(const QString &filePath)
QApplication::restoreOverrideCursor();
}
QPixmap ImageGallery::createThumbnail(const QString &filePath, int width, int height)
void ImageGallery::loadThumbnailAsync(ImageThumbnail *label, const QString &filePath)
{
// Verify file exists before attempting to load
QFileInfo fileInfo(filePath);
if (!fileInfo.exists() || !fileInfo.isReadable()) {
qDebug() << "Image file does not exist or is not readable:" << filePath;
return createPlaceholderThumbnail(width, height, "File not found");
// QImage loading and scaling are thread-safe; QPixmap creation must happen
// on the main thread. We do the heavy work in a background thread and marshal
// the result back via invokeMethod so the main thread never stalls on disk I/O.
QPointer<ImageThumbnail> ptr(label);
QThreadPool::globalInstance()->start([this, ptr, filePath]() {
QImage image;
QFileInfo fi(filePath);
if (fi.exists() && fi.isReadable()) {
image = QImage(filePath);
if (!image.isNull())
image = image.scaled(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT,
Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
// Try to load the image
QPixmap pixmap(filePath);
// If loading failed, create a placeholder
if (pixmap.isNull()) {
qDebug() << "Failed to load image:" << filePath;
return createPlaceholderThumbnail(width, height, "Failed to load");
QMetaObject::invokeMethod(this, [ptr, image]() {
if (!ptr) return;
if (image.isNull()) {
// File missing or unreadable placeholder already shown, nothing to do
return;
}
// Scale pixmap while maintaining aspect ratio
return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
ptr->setPixmap(QPixmap::fromImage(image));
}, Qt::QueuedConnection);
});
}
void ImageGallery::resizeEvent(QResizeEvent *event)
@@ -687,62 +682,19 @@ void ImageGallery::updateGridLayout()
m_containerWidget->adjustSize();
m_containerWidget->updateGeometry();
// Handle single column mode specially
if (m_columnsCount == 1) {
// Center the thumbnails in the viewport
int centeringMargin = (viewportWidth - THUMBNAIL_WIDTH) / 2;
centeringMargin = std::max(THUMBNAIL_SPACING, centeringMargin);
int centeringMargin = std::max(THUMBNAIL_SPACING, (viewportWidth - THUMBNAIL_WIDTH) / 2);
m_gridLayout->setContentsMargins(centeringMargin, THUMBNAIL_SPACING, centeringMargin, THUMBNAIL_SPACING);
// Let all thumbnails expand to fill available width in single column mode
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
m_gridLayout->setContentsMargins(THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING);
// Reset thumbnail constraints
for (auto thumbnail : m_thumbnails) {
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();
m_containerWidget->update();
});
}
}
}
+5 -1
View File
@@ -12,6 +12,8 @@
#include <QPushButton>
#include <QResizeEvent>
#include <QPushButton>
#include <QPointer>
#include <QThreadPool>
#include "databasemanager.h"
// Global constants
@@ -30,12 +32,14 @@ public:
protected:
void mousePressEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *event) override;
signals:
void thumbnailClicked(const QString &filePath);
private:
QString m_filePath;
QString m_displayName; // pre-truncated filename for painting
};
class ImageGallery : public QWidget
@@ -88,7 +92,7 @@ private:
QLabel *m_loadingIndicator;
QPushButton *m_loadMoreButton;
QPixmap createThumbnail(const QString &filePath, int width, int height);
void loadThumbnailAsync(ImageThumbnail *label, const QString &filePath);
QPixmap createPlaceholderThumbnail(int width, int height, const QString &message);
QString m_lastSearchQuery; // Track the last search text
+5 -5
View File
@@ -7,15 +7,15 @@ int main(int argc, char *argv[])
QApplication app(argc, argv);
// Set application properties
app.setApplicationName("screenshot-gallery");
app.setApplicationName("orc-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));
appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
app.setWindowIcon(appIcon);
// Create and show main window
+7 -7
View File
@@ -17,7 +17,7 @@
#include <stdexcept>
// Define settings file path
const QString MainWindow::CONFIG_FILE_PATH = QDir::homePath() + "/.config/ScreenshotOCRGallery/settings.ini";
const QString MainWindow::CONFIG_FILE_PATH = QDir::homePath() + "/.config/OrcGallery/settings.ini";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
@@ -32,15 +32,15 @@ MainWindow::MainWindow(QWidget *parent)
, m_imagePreloadCount(DEFAULT_PRELOAD_COUNT)
{
// Set window title and size
setWindowTitle(tr("Screenshot OCR Gallery"));
setWindowTitle(tr("ORC 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));
appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
setWindowIcon(appIcon);
// Remove fixed minimum size to allow for single column layout at any width
@@ -66,7 +66,7 @@ MainWindow::MainWindow(QWidget *parent)
// Initialize typing inactivity timer
m_typingTimer->setSingleShot(true);
m_typingTimer->setInterval(500); // 500ms of no typing before searching
m_typingTimer->setInterval(150);
connect(m_typingTimer, &QTimer::timeout, this, &MainWindow::handleTypingInactivityTimeout);
// Only create animation timer for searching visual feedback
+1 -1
View File
@@ -8,7 +8,7 @@
// 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";
const QString SettingsDialog::CONFIG_FILE_PATH = QDir::homePath() + "/.config/OrcGallery/settings.ini";
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)