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>
@@ -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")
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,8 +31,13 @@ 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);
|
||||||
@@ -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();
|
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 =
|
||||||
|
"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 "
|
"JOIN ocr_fts f ON r.id = f.rowid "
|
||||||
"WHERE ocr_fts MATCH :query "
|
"WHERE ocr_fts MATCH :query "
|
||||||
"ORDER BY rank";
|
"ORDER BY rank";
|
||||||
|
if (limit > 0)
|
||||||
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);
|
||||||
@@ -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 "
|
|
||||||
"ORDER BY id";
|
|
||||||
|
|
||||||
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";
|
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.prepare(queryStr);
|
||||||
query.bindValue(":search", "%" + searchText + "%");
|
query.bindValue(":search", "%" + searchText + "%");
|
||||||
|
if (searchText.length() > 3)
|
||||||
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) {
|
||||||
@@ -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;
|
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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,39 +226,9 @@ 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,
|
||||||
@@ -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;
|
||||||
|
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
|
QMetaObject::invokeMethod(this, [ptr, image]() {
|
||||||
QPixmap pixmap(filePath);
|
if (!ptr) return;
|
||||||
|
if (image.isNull()) {
|
||||||
// If loading failed, create a placeholder
|
// File missing or unreadable — placeholder already shown, nothing to do
|
||||||
if (pixmap.isNull()) {
|
return;
|
||||||
qDebug() << "Failed to load image:" << filePath;
|
|
||||||
return createPlaceholderThumbnail(width, height, "Failed to load");
|
|
||||||
}
|
}
|
||||||
|
ptr->setPixmap(QPixmap::fromImage(image));
|
||||||
// Scale pixmap while maintaining aspect ratio
|
}, Qt::QueuedConnection);
|
||||||
return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -30,12 +32,14 @@ public:
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||