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) 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 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -29,23 +29,23 @@ set(PROJECT_SOURCES
src/updatedatabasedialog.h 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 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" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps"
RENAME "orcs-gallery.png") RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-128.png" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-128.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps"
RENAME "orcs-gallery.png") RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-256.png" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-256.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps"
RENAME "orcs-gallery.png") RENAME "orc-gallery.png")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orcs-gallery-512.png" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/icons/orc-gallery-512.png"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps" 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::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
@@ -53,11 +53,11 @@ target_link_libraries(screenshot-gallery PRIVATE
Qt6::Concurrent Qt6::Concurrent
) )
install(TARGETS screenshot-gallery install(TARGETS orc-gallery
BUNDLE DESTINATION . BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
# Install desktop file # 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") 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 # This script displays OCR data from SQLite and allows opening files with rofi
# Database path # 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 dependencies
check_deps() { check_deps() {
+1 -1
View File
@@ -4,7 +4,7 @@
# This script displays OCR data from SQLite and allows opening files with rofi # This script displays OCR data from SQLite and allows opening files with rofi
# Database path # 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" SCREENSHOTS_DIR="$HOME/Screenshots"
MAX_TEXT_LENGTH=100 MAX_TEXT_LENGTH=100
+3 -3
View File
@@ -7,8 +7,8 @@ Usage:
Arguments: Arguments:
--db Path to the SQLite database file. --db Path to the SQLite database file.
Default: $XDG_DATA_HOME/screenshot-gallery/screenshot_ocr.db Default: $XDG_DATA_HOME/orc-gallery/screenshot_ocr.db
(~/.local/share/screenshot-gallery/screenshot_ocr.db) (~/.local/share/orc-gallery/screenshot_ocr.db)
--screenshots-dir Directory containing screenshot images to process. --screenshots-dir Directory containing screenshot images to process.
Default: ~/Screenshots Default: ~/Screenshots
""" """
@@ -53,7 +53,7 @@ signal.signal(signal.SIGINT, _handle_signal)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
xdg_data = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) 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") default_dir = os.path.expanduser("~/Screenshots")
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
import tempfile import tempfile
# Configuration # 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") SCREENSHOTS_DIR = os.path.expanduser("~/Screenshots")
ROFI_PROMPT = "OCR Search" 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 # This script displays OCR'd screenshot data in rofi and allows opening selected files
# Constants # 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" SCREENSHOTS_DIR="$HOME/Screenshots"
ROFI_PROMPT="Screenshot OCR" ROFI_PROMPT="Screenshot OCR"
MAX_DISPLAY_LENGTH=100 MAX_DISPLAY_LENGTH=100
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
from datetime import datetime from datetime import datetime
# Constants # 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(): 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 # simple_rofi_ocr.sh - A simplified script to search OCR'd screenshots with rofi
# Database location # 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 # Check if database exists
if [ ! -f "$DB_PATH" ]; then 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. 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 - Smart typing detection with 500ms inactivity timer before searching
- Visual feedback while typing and searching with animated status indicators - Visual feedback while typing and searching with animated status indicators
- Settings dialog to customize database location and screenshots directory - 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 - Customizable image preload count for performance tuning
- Prominent "Load More Images" button for easy one-click pagination - Prominent "Load More Images" button for easy one-click pagination
- Optimized lazy loading that only loads images when needed - Optimized lazy loading that only loads images when needed
@@ -44,7 +44,7 @@ sudo apt install qt6-base-dev libqt6sql6-sqlite cmake
## Database Requirements ## 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: The required schema is:
@@ -67,7 +67,7 @@ CREATE TABLE ocr_results (
3. Run the build script: 3. Run the build script:
```bash ```bash
cd screenshot-gallery cd orc-gallery
chmod +x build.sh chmod +x build.sh
./build.sh ./build.sh
``` ```
@@ -95,7 +95,7 @@ make
After building, run the application: After building, run the application:
```bash ```bash
./build/screenshot-gallery ./build/orc-gallery
``` ```
## Usage ## Usage
@@ -125,7 +125,7 @@ After building, run the application:
- Database File Path: Change where your OCR database is stored - Database File Path: Change where your OCR database is stored
- Screenshots Directory: Set the default location for your screenshots - Screenshots Directory: Set the default location for your screenshots
- Image count to pre-load: Adjust how many images are loaded at once (default: 20) - 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 - Changes are applied immediately without requiring a restart
## Search Technology ## Search Technology
@@ -135,7 +135,7 @@ This application combines multiple performance-enhancing technologies:
### 1. Customizable Configuration ### 1. Customizable Configuration
- **Settings Dialog:** Easily configure database location, screenshots directory, and preload count - **Settings Dialog:** Easily configure database location, screenshots directory, and preload count
- **File Path Selection:** Browse for locations using native file pickers - **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) - **Performance Tuning:** Adjust how many images are preloaded (20 by default)
- **Dynamic Updates:** Changes are applied immediately without requiring a restart - **Dynamic Updates:** Changes are applied immediately without requiring a restart
@@ -176,7 +176,7 @@ This application combines multiple performance-enhancing technologies:
### Database Connection Issues ### Database Connection Issues
If the application cannot connect to the database: 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 - 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) - 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..." print_message "Running application..."
# Check if the application exists # Check if the application exists
if [ -f "build/screenshot-gallery" ]; then if [ -f "build/orc-gallery" ]; then
./build/screenshot-gallery ./build/orc-gallery
else else
print_error "Application not found. Build might have failed." print_error "Application not found. Build might have failed."
exit 1 exit 1
@@ -89,7 +89,7 @@ run_application() {
} }
# -------- Main script execution -------- # -------- Main script execution --------
print_message "Starting build process for Screenshot Gallery..." print_message "Starting build process for ORC Gallery..."
check_dependencies check_dependencies
create_build_dir create_build_dir
@@ -104,5 +104,5 @@ echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
run_application run_application
else 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 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] [Desktop Entry]
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Screenshot OCR Gallery Name=ORC Gallery
Comment=Search and browse screenshots by OCR content Comment=Search and browse screenshots by OCR content
Exec=screenshot-gallery Exec=orc-gallery
Terminal=false Terminal=false
Categories=Graphics;Utility;Viewer; Categories=Graphics;Utility;Viewer;
Keywords=screenshots;ocr;gallery;search;images; Keywords=screenshots;ocr;gallery;search;images;
Icon=orcs-gallery Icon=orc-gallery
StartupNotify=true 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. 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 ## Directory Structure
``` ```
ocr-screenshot-gallery/ orc-gallery/
├── CMakeLists.txt # CMake build configuration (Qt6) ├── CMakeLists.txt # CMake build configuration (Qt6)
├── build.sh # One-shot build automation script ├── build.sh # One-shot build automation script
├── README.md # User-facing documentation ├── README.md # User-facing documentation
├── resources.qrc # Qt resource file bundling icons ├── resources.qrc # Qt resource file bundling icons
├── screenshot-gallery.desktop # Linux XDG desktop entry ├── orc-gallery.desktop # Linux XDG desktop entry
├── .gitignore ├── .gitignore
├── src/ # C++ application source ├── src/ # C++ application source
@@ -34,13 +34,13 @@ ocr-screenshot-gallery/
│ └── screenshot_ocr.db # Example/seed database │ └── screenshot_ocr.db # Example/seed database
├── icons/ # Application icon assets ├── icons/ # Application icon assets
│ ├── orcs-gallery-512.png │ ├── orc-gallery-512.png
│ ├── orcs-gallery-256.png │ ├── orc-gallery-256.png
│ ├── orcs-gallery-128.png │ ├── orc-gallery-128.png
│ └── orcs-gallery-64.png │ └── orc-gallery-64.png
└── build/ # CMake build output (git-ignored) └── build/ # CMake build output (git-ignored)
└── screenshot-gallery # Compiled executable └── orc-gallery # Compiled executable
``` ```
--- ---
@@ -86,20 +86,20 @@ make -j$(nproc)
### Run ### Run
```bash ```bash
./build/screenshot-gallery ./build/orc-gallery
``` ```
### Install system-wide ### Install system-wide
```bash ```bash
cd build && sudo make install cd build && sudo make install
screenshot-gallery # from anywhere orc-gallery # from anywhere
``` ```
Installation puts: Installation puts:
- Binary → `${CMAKE_INSTALL_BINDIR}` (typically `/usr/local/bin/`) - Binary → `${CMAKE_INSTALL_BINDIR}` (typically `/usr/local/bin/`)
- Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orcs-gallery.png` - Icons → `/share/icons/hicolor/{64x64,128x128,256x256,512x512}/apps/orc-gallery.png`
- Desktop entry → `/share/applications/screenshot-gallery.desktop` - Desktop entry → `/share/applications/orc-gallery.desktop`
--- ---
@@ -107,9 +107,9 @@ Installation puts:
### Location ### 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 ### 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 - 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`) - Animated "Searching…" status bar indicator (`m_searchingAnimationTimer`)
- Instantiates and owns `DatabaseManager` and `ImageGallery` - 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 search complete: updates status bar with result count
- On clear search: reloads all images (first page) - 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 ```bash
python3 OCR-scripts/ocr_screenshots.py \ 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 --screenshots-dir ~/Screenshots
``` ```
@@ -307,11 +307,11 @@ These scripts hook OCR-screenshot workflows into the [Rofi](https://github.com/d
## Settings & Configuration ## Settings & Configuration
**Settings file:** `~/.config/ScreenshotOCRGallery/settings.ini` **Settings file:** `~/.config/OrcGallery/settings.ini`
```ini ```ini
[General] [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 screenshotsDir=/home/<user>/Screenshots
imagePreloadCount=20 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` | | Repeated searches | 5-minute LRU cache in `DatabaseManager` |
| Large galleries | Explicit pagination (default 20, user-controlled via Load More) | | Large galleries | Explicit pagination (default 20, user-controlled via Load More) |
| Column reflow cost | 300 ms debounce on resize events | | 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 | | Thread-safe DB access | Per-thread `QSqlDatabase` connections + `QMutex` guards |
| Missing files | Placeholder thumbnail rendered inline; no crash | | 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 ## Linux Desktop Integration
**`screenshot-gallery.desktop`** **`orc-gallery.desktop`**
```ini ```ini
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Screenshot OCR Gallery Name=ORC Gallery
Icon=orcs-gallery Icon=orc-gallery
Exec=screenshot-gallery Exec=orc-gallery
Categories=Graphics;Utility;Viewer; Categories=Graphics;Utility;Viewer;
Keywords=screenshots;ocr;gallery;search;images; Keywords=screenshots;ocr;gallery;search;images;
``` ```
XDG paths used by the application: XDG paths used by the application:
- Config: `QStandardPaths::AppConfigLocation``~/.config/ScreenshotOCRGallery/` - Config: `QStandardPaths::AppConfigLocation``~/.config/OrcGallery/`
- Data: `QStandardPaths::AppLocalDataLocation``~/.local/share/screenshot-gallery/` - 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. - 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`). - `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. - 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"> <!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/"> <qresource prefix="/">
<file>icons/orcs-gallery-64.png</file> <file>icons/orc-gallery-64.png</file>
<file>icons/orcs-gallery-128.png</file> <file>icons/orc-gallery-128.png</file>
<file>icons/orcs-gallery-256.png</file> <file>icons/orc-gallery-256.png</file>
<file>icons/orcs-gallery-512.png</file> <file>icons/orc-gallery-512.png</file>
</qresource> </qresource>
</RCC> </RCC>
+78 -127
View File
@@ -20,28 +20,8 @@ DatabaseManager::DatabaseManager(QObject *parent)
m_allImagesCache.clear(); m_allImagesCache.clear();
m_lastCacheUpdate = QDateTime::currentDateTime(); m_lastCacheUpdate = QDateTime::currentDateTime();
// Connect future watcher to handle search results // Watcher is used only for lifecycle tracking; results are emitted directly
connect(&m_searchWatcher, &QFutureWatcher<void>::finished, // from performSearchInBackground to avoid double-emit.
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 // Clean cache periodically
QTimer *cleanupTimer = new QTimer(this); QTimer *cleanupTimer = new QTimer(this);
@@ -51,9 +31,14 @@ DatabaseManager::DatabaseManager(QObject *parent)
DatabaseManager::~DatabaseManager() DatabaseManager::~DatabaseManager()
{ {
// Cancel any ongoing search and wait for it to finish // Signal cancellation and wait for the background thread to drain cleanly.
cancelSearch(); {
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
}
if (m_searchFuture.isRunning())
m_searchFuture.waitForFinished();
if (m_db.isOpen()) { if (m_db.isOpen()) {
m_db.close(); m_db.close();
} }
@@ -182,6 +167,17 @@ bool DatabaseManager::initialize(const QString &dbPath)
m_ftsEnabled = false; 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; m_initialized = true;
qDebug() << "Database initialized successfully."; qDebug() << "Database initialized successfully.";
return true; return true;
@@ -318,9 +314,12 @@ QSqlDatabase DatabaseManager::getDatabaseConnection()
if (!threadDb.open()) { if (!threadDb.open()) {
qDebug() << "Failed to open database in thread:" << threadDb.lastError().text(); qDebug() << "Failed to open database in thread:" << threadDb.lastError().text();
} else { } else {
// Enable foreign keys in this connection
QSqlQuery query(threadDb); 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; return threadDb;
@@ -397,16 +396,11 @@ void DatabaseManager::searchImages(const QString &searchText, int offset, int li
void DatabaseManager::cancelSearch() 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); QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true; m_searchCancelled = true;
m_currentSearchText.clear(); 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) void DatabaseManager::performSearchInBackground(const QString &searchText, int offset, int limit)
@@ -428,34 +422,8 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return; return;
} }
} }
// First, get total count for pagination info
QSqlQuery countQuery(threadDb);
int totalCount = 0; 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 // Check if search was cancelled before main query
{ {
QMutexLocker locker(&m_searchMutex); QMutexLocker locker(&m_searchMutex);
@@ -463,24 +431,24 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return; return;
} }
} }
// Start transaction to speed up query
threadDb.transaction();
// 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); QSqlQuery query(threadDb);
if (m_ftsEnabled) { if (m_ftsEnabled) {
// Use FTS5 virtual table for much faster text search
QString ftsQuery = prepareFTSQuery(searchText); QString ftsQuery = prepareFTSQuery(searchText);
QString queryStr = "SELECT r.id, r.full_path, r.ocr_text FROM ocr_results r " QString queryStr =
"JOIN ocr_fts f ON r.id = f.rowid " "SELECT r.id, r.full_path, r.ocr_text, COUNT(*) OVER() AS total_count "
"WHERE ocr_fts MATCH :query " "FROM ocr_results r "
"ORDER BY rank"; "JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query "
if (limit > 0) { "ORDER BY rank";
if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset"; queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr); query.prepare(queryStr);
query.bindValue(":query", ftsQuery); query.bindValue(":query", ftsQuery);
if (limit > 0) { if (limit > 0) {
@@ -488,57 +456,40 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
query.bindValue(":offset", offset); query.bindValue(":offset", offset);
} }
} else { } else {
// Fallback to LIKE queries if FTS is not available QString whereClause = (searchText.length() <= 3)
// Optimize the query based on length of search text ? "ocr_text LIKE :search"
if (searchText.length() <= 3) { : "ocr_text LIKE :search OR ocr_text LIKE :wordstart";
// 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 " QString queryStr =
"ORDER BY id"; "SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results WHERE " + whereClause + " ORDER BY id";
if (limit > 0) { if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset"; queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.prepare(queryStr); query.bindValue(":search", "%" + searchText + "%");
query.bindValue(":search", "%" + searchText + "%"); if (searchText.length() > 3)
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 + "%"); query.bindValue(":wordstart", "% " + searchText + "%");
if (limit > 0) { if (limit > 0) {
query.bindValue(":limit", limit); query.bindValue(":limit", limit);
query.bindValue(":offset", offset); query.bindValue(":offset", offset);
}
} }
} }
if (!query.exec()) { if (!query.exec()) {
qDebug() << "Failed to search images:" << query.lastError().text(); qDebug() << "Search query failed:" << query.lastError().text();
qDebug() << "Error details:" << query.lastError().databaseText();
threadDb.rollback(); threadDb.rollback();
// If FTS query failed, try fallback to LIKE // FTS failure: fall back to LIKE for this request
if (m_ftsEnabled) { if (m_ftsEnabled) {
qDebug() << "Trying fallback to LIKE query..."; qDebug() << "Falling back to LIKE query";
threadDb.transaction(); threadDb.transaction();
query.prepare(
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search"); "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 + "%"); query.bindValue(":search", "%" + searchText + "%");
if (!query.exec()) { if (!query.exec()) {
qDebug() << "Fallback query also failed:" << query.lastError().text(); qDebug() << "Fallback query failed:" << query.lastError().text();
threadDb.rollback(); threadDb.rollback();
return; 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); QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) { 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(100);
images.reserve(query.size() > 0 ? query.size() : 100);
while (query.next()) { while (query.next()) {
// Periodically check if search was cancelled
if (query.at() % 20 == 0) { if (query.at() % 20 == 0) {
QMutexLocker locker(&m_searchMutex); QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) { if (m_searchCancelled || m_currentSearchText != searchText) {
@@ -568,16 +517,18 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return; return;
} }
} }
// Read total count from the window function column (same on every row)
if (totalCount == 0)
totalCount = query.value(3).toInt();
ImageItem item; ImageItem item;
item.id = query.value(0).toInt(); item.id = query.value(0).toInt();
item.filePath = query.value(1).toString(); item.filePath = query.value(1).toString();
item.ocrText = query.value(2).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); images.append(item);
}
} }
threadDb.commit(); threadDb.commit();
+56 -104
View File
@@ -24,14 +24,33 @@ ImageThumbnail::ImageThumbnail(const QString &filePath, QWidget *parent)
setLineWidth(2); setLineWidth(2);
setScaledContents(false); setScaledContents(false);
setCursor(Qt::PointingHandCursor); 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); 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) void ImageThumbnail::mousePressEvent(QMouseEvent *event)
@@ -207,40 +226,10 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
continue; // Skip this image continue; // Skip this image
} }
// Create and add thumbnail
QPixmap thumbnail = createThumbnail(item.filePath, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
ImageThumbnail *thumbnailLabel = new ImageThumbnail(item.filePath, this); ImageThumbnail *thumbnailLabel = new ImageThumbnail(item.filePath, this);
thumbnailLabel->setPixmap(thumbnail); thumbnailLabel->setPixmap(createPlaceholderThumbnail(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, ""));
loadThumbnailAsync(thumbnailLabel, item.filePath);
// 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());
// Connect the thumbnail click signal // Connect the thumbnail click signal
connect(thumbnailLabel, &ImageThumbnail::thumbnailClicked, connect(thumbnailLabel, &ImageThumbnail::thumbnailClicked,
this, &ImageGallery::handleThumbnailClicked); this, &ImageGallery::handleThumbnailClicked);
@@ -546,26 +535,32 @@ void ImageGallery::handleThumbnailClicked(const QString &filePath)
QApplication::restoreOverrideCursor(); 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 // QImage loading and scaling are thread-safe; QPixmap creation must happen
QFileInfo fileInfo(filePath); // on the main thread. We do the heavy work in a background thread and marshal
if (!fileInfo.exists() || !fileInfo.isReadable()) { // the result back via invokeMethod so the main thread never stalls on disk I/O.
qDebug() << "Image file does not exist or is not readable:" << filePath; QPointer<ImageThumbnail> ptr(label);
return createPlaceholderThumbnail(width, height, "File not found");
} QThreadPool::globalInstance()->start([this, ptr, filePath]() {
QImage image;
// Try to load the image QFileInfo fi(filePath);
QPixmap pixmap(filePath); if (fi.exists() && fi.isReadable()) {
image = QImage(filePath);
// If loading failed, create a placeholder if (!image.isNull())
if (pixmap.isNull()) { image = image.scaled(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT,
qDebug() << "Failed to load image:" << filePath; Qt::KeepAspectRatio, Qt::SmoothTransformation);
return createPlaceholderThumbnail(width, height, "Failed to load"); }
}
QMetaObject::invokeMethod(this, [ptr, image]() {
// Scale pixmap while maintaining aspect ratio if (!ptr) return;
return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); if (image.isNull()) {
// File missing or unreadable — placeholder already shown, nothing to do
return;
}
ptr->setPixmap(QPixmap::fromImage(image));
}, Qt::QueuedConnection);
});
} }
void ImageGallery::resizeEvent(QResizeEvent *event) void ImageGallery::resizeEvent(QResizeEvent *event)
@@ -687,62 +682,19 @@ void ImageGallery::updateGridLayout()
m_containerWidget->adjustSize(); m_containerWidget->adjustSize();
m_containerWidget->updateGeometry(); m_containerWidget->updateGeometry();
// Handle single column mode specially
if (m_columnsCount == 1) { if (m_columnsCount == 1) {
// Center the thumbnails in the viewport int centeringMargin = std::max(THUMBNAIL_SPACING, (viewportWidth - THUMBNAIL_WIDTH) / 2);
int centeringMargin = (viewportWidth - THUMBNAIL_WIDTH) / 2;
centeringMargin = std::max(THUMBNAIL_SPACING, centeringMargin);
m_gridLayout->setContentsMargins(centeringMargin, THUMBNAIL_SPACING, centeringMargin, THUMBNAIL_SPACING); 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) { for (auto thumbnail : m_thumbnails) {
thumbnail->setMaximumWidth(THUMBNAIL_WIDTH); thumbnail->setMaximumWidth(THUMBNAIL_WIDTH);
thumbnail->setAlignment(Qt::AlignCenter); 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 { } else {
// For multi-column, use minimal margins
m_gridLayout->setContentsMargins(THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING); m_gridLayout->setContentsMargins(THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING);
for (auto thumbnail : m_thumbnails)
// Reset thumbnail constraints
for (auto thumbnail : m_thumbnails) {
thumbnail->setMaximumWidth(QWIDGETSIZE_MAX); 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); 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();
});
} }
} }
} }
+9 -5
View File
@@ -12,6 +12,8 @@
#include <QPushButton> #include <QPushButton>
#include <QResizeEvent> #include <QResizeEvent>
#include <QPushButton> #include <QPushButton>
#include <QPointer>
#include <QThreadPool>
#include "databasemanager.h" #include "databasemanager.h"
// Global constants // Global constants
@@ -22,20 +24,22 @@ static const int THUMBNAIL_SPACING = 2; // Reduced spacing between thumbnails
class ImageThumbnail : public QLabel class ImageThumbnail : public QLabel
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ImageThumbnail(const QString &filePath, QWidget *parent = nullptr); explicit ImageThumbnail(const QString &filePath, QWidget *parent = nullptr);
QString getFilePath() const { return m_filePath; } QString getFilePath() const { return m_filePath; }
protected: protected:
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *event) override;
signals: signals:
void thumbnailClicked(const QString &filePath); void thumbnailClicked(const QString &filePath);
private: private:
QString m_filePath; QString m_filePath;
QString m_displayName; // pre-truncated filename for painting
}; };
class ImageGallery : public QWidget class ImageGallery : public QWidget
@@ -88,7 +92,7 @@ private:
QLabel *m_loadingIndicator; QLabel *m_loadingIndicator;
QPushButton *m_loadMoreButton; 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); QPixmap createPlaceholderThumbnail(int width, int height, const QString &message);
QString m_lastSearchQuery; // Track the last search text 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); QApplication app(argc, argv);
// Set application properties // Set application properties
app.setApplicationName("screenshot-gallery"); app.setApplicationName("orc-gallery");
app.setApplicationVersion("1.0.0"); app.setApplicationVersion("1.0.0");
// Set application icon // Set application icon
QIcon appIcon; QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64)); appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128)); appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256)); appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512)); appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
app.setWindowIcon(appIcon); app.setWindowIcon(appIcon);
// Create and show main window // Create and show main window
+7 -7
View File
@@ -17,7 +17,7 @@
#include <stdexcept> #include <stdexcept>
// Define settings file path // 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) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
@@ -32,15 +32,15 @@ MainWindow::MainWindow(QWidget *parent)
, m_imagePreloadCount(DEFAULT_PRELOAD_COUNT) , m_imagePreloadCount(DEFAULT_PRELOAD_COUNT)
{ {
// Set window title and size // Set window title and size
setWindowTitle(tr("Screenshot OCR Gallery")); setWindowTitle(tr("ORC Gallery"));
resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT); resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
// Set window icon // Set window icon
QIcon appIcon; QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64)); appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128)); appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256)); appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512)); appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
setWindowIcon(appIcon); setWindowIcon(appIcon);
// Remove fixed minimum size to allow for single column layout at any width // 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 // Initialize typing inactivity timer
m_typingTimer->setSingleShot(true); 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); connect(m_typingTimer, &QTimer::timeout, this, &MainWindow::handleTypingInactivityTimeout);
// Only create animation timer for searching visual feedback // Only create animation timer for searching visual feedback
+1 -1
View File
@@ -8,7 +8,7 @@
// Define static constants // Define static constants
const QString SettingsDialog::DEFAULT_SCREENSHOTS_DIR = QDir::homePath() + "/Screenshots"; const QString SettingsDialog::DEFAULT_SCREENSHOTS_DIR = QDir::homePath() + "/Screenshots";
const QString SettingsDialog::DEFAULT_DATABASE_FILENAME = "screenshot_ocr.db"; 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) SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent) : QDialog(parent)