WIP gallery for linux
  • QML 48.7%
  • C++ 29.9%
  • JavaScript 19.8%
  • GLSL 0.7%
  • CMake 0.7%
  • Other 0.2%
Find a file
Alex ab99607936 perf: keep the UI responsive while a scan runs
Startup froze the window because the scan triggered a chain of blocking
work on the GUI thread rather than because the scan itself was slow.

Unblock the GUI thread:

- MediaModel/AlbumModel data() returned a synchronously generated
  thumbnail, decoding via libvips/LibRaw (or an ffmpegthumbnailer
  subprocess) inline, behind the same generator mutex the background
  cache builder holds. Both now return a ThumbnailProvider URL, which
  decodes on the pixmap-reader thread. This was the hard freeze.
- startCacheBuilding() waited on the previous QFuture and then stat()ed
  every path in the library, both on the caller's thread. Replace the
  cancel flag with a generation counter so a superseded run drops out on
  its own, and move the stat pass onto the worker.
- main.cpp ran three full-table queries plus an O(n) mosaic relayout per
  finished scan. Funnel every refresh through one 500 ms single-shot
  timer so a burst of scans collapses into one refresh, and move
  getAllMediaPaths() and pruneOrphanedMedia() off-thread.
- FileScanner::startScan() read the scan exclusions over SQL before
  dispatching; do it on the worker instead.

Cut redundant work:

- main.qml built the timeline grid a second time and replace()d it into
  the StackView; adopt the initialItem instead. The throwaway laid out
  at its 800px implicitWidth first, forcing a third TimelineModel pack.
- SettingsView duplicated the model refreshes main.cpp already does, so
  every scan cost two library re-queries once Settings had been opened.
- DashboardView ran MediaModel.refresh() from Component.onCompleted as
  the Popup's contentItem, i.e. on every cold start for a modal nobody
  had opened. Defer it behind a Loader.
- TimelineModel::refresh() re-ran the library SELECT for geometry-only
  changes; split the packing into repack() and call that from
  setNumColumns()/setContentWidth().
- scanFinished now carries a file count instead of the QStringList; the
  only consumers used its size, and the QML handler converted every
  entry to a JS string on the GUI thread.

Yield to the UI:

- Nice the scan's traversal workers and its EXIF/ffprobe pass to +10.
  Nice values are per-thread on Linux, so this only touches the scan;
  runScan() restores the caller's priority since it runs on a shared
  QtConcurrent pool thread.

Also fixes a ReferenceError in main.qml's scroll-restore path, which
called a restoreTimer that does not exist and aborted the restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 20:27:08 +02:00
assets Fix zoom to target mouse cursor position 2026-08-24 22:16:22 +02:00
include perf: keep the UI responsive while a scan runs 2026-08-25 20:27:08 +02:00
qml perf: keep the UI responsive while a scan runs 2026-08-25 20:27:08 +02:00
src perf: keep the UI responsive while a scan runs 2026-08-25 20:27:08 +02:00
.gitignore chore: gitignore .serena/ and src/build-pkg/ tool dirs 2026-07-02 11:22:52 +02:00
CMakeLists.txt Fix zoom to target mouse cursor position 2026-08-24 22:16:22 +02:00
kader.desktop feat: trash management, ignored folders management, and UI polish 2026-05-23 19:37:36 +02:00
PKGBUILD Fix zoom jitter by animating pan and zoom synchronously 2026-08-24 22:19:28 +02:00
README.md feat: implement dynamic coloring support via material_colors.scss and Matugen 2026-08-10 15:58:03 +02:00
search-in-video.txt feat: implement dynamic coloring support via material_colors.scss and Matugen 2026-08-10 15:58:03 +02:00
SUMMARY.md Fix zoom jitter and dizzying panning behavior in ViewerOverlay 2026-08-24 22:33:04 +02:00

Kader icon

Kader

A fast, modern photo gallery for Linux — built with Qt 6, QML Material 3, and C++.


Features

  • Timeline — mosaic grid with automatic monthly separators and variable density
  • Albums — folder-based with pinning, custom covers, and virtual album creation
  • Videos — dedicated view with an integrated player (FFmpeg backend), looping, volume, seeking
  • Places / Map — geotagged photos plotted on an OpenStreetMap with reverse-geocoded pin cards
  • Favorites, Hidden, Trash — smart views; hidden photos are password-protected
  • Viewer — zoom/pan, EXIF info panel, keyboard navigation, fullscreen, copy to clipboard
  • RAW camera support — thumbnail extraction via LibRaw for NEF, CR2, CR3, ARW, DNG, RAF and more
  • Encrypted thumbnails — AES-256-CBC per-machine key, cached on disk
  • Fast scanning — parallel directory walk using Linux getdents64, Exiv2 EXIF parsing, batch SQLite writes in WAL transactions
  • Storage overview — sidebar card showing photos, video, and other disk usage

Tech Stack

Layer Technology
UI QML + Qt 6 Quick + QmlMaterial
Backend C++17, Qt 6 Core / Sql / Concurrent / Multimedia
Thumbnails libvips (images), ffmpegthumbnailer (video), LibRaw (RAW)
Metadata Exiv2
Database SQLite 3 (WAL mode)
Encryption OpenSSL AES-256-CBC

Building

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/kader

Dependencies: Qt 6 (Core Gui Qml Quick Sql Positioning Location Concurrent Multimedia), libvips, Exiv2, LibRaw, OpenSSL, ffmpegthumbnailer

On Arch Linux:

makepkg -si

Usage

kader                        # open gallery
kader /path/to/photo.jpg     # open a single file directly
kader file:///path/to/photo  # XDG / .desktop %U handler

Dynamic Colors & Matugen

Kader supports dynamic theming out of the box. By toggling "Dynamic color" in the app's settings, Kader will automatically read Material 3 colors from ~/.local/state/quickshell/user/generated/material_colors.scss.

Using with Matugen

To sync Kader's colors with your system wallpaper using Matugen, you can create an SCSS template in your Matugen templates directory (e.g., ~/.config/matugen/templates/kader-colors.scss) with the following variables:

$darkmode: true;
$primary: {{colors.primary.default.hex}};
$onPrimary: {{colors.on_primary.default.hex}};
$primaryContainer: {{colors.primary_container.default.hex}};
$onPrimaryContainer: {{colors.on_primary_container.default.hex}};
$secondary: {{colors.secondary.default.hex}};
$onSecondary: {{colors.on_secondary.default.hex}};
$secondaryContainer: {{colors.secondary_container.default.hex}};
$onSecondaryContainer: {{colors.on_secondary_container.default.hex}};
$tertiary: {{colors.tertiary.default.hex}};
$onTertiary: {{colors.on_tertiary.default.hex}};
$tertiaryContainer: {{colors.tertiary_container.default.hex}};
$onTertiaryContainer: {{colors.on_tertiary_container.default.hex}};
$error: {{colors.error.default.hex}};
$onError: {{colors.on_error.default.hex}};
$errorContainer: {{colors.error_container.default.hex}};
$onErrorContainer: {{colors.on_error_container.default.hex}};
$surface: {{colors.surface.default.hex}};
$surfaceDim: {{colors.surface_dim.default.hex}};
$surfaceBright: {{colors.surface_bright.default.hex}};
$surfaceContainerLowest: {{colors.surface_container_lowest.default.hex}};
$surfaceContainerLow: {{colors.surface_container_low.default.hex}};
$surfaceContainer: {{colors.surface_container.default.hex}};
$surfaceContainerHigh: {{colors.surface_container_high.default.hex}};
$surfaceContainerHighest: {{colors.surface_container_highest.default.hex}};
$onSurface: {{colors.on_surface.default.hex}};
$onSurfaceVariant: {{colors.on_surface_variant.default.hex}};
$outline: {{colors.outline.default.hex}};
$outlineVariant: {{colors.outline_variant.default.hex}};
$inverseSurface: {{colors.inverse_surface.default.hex}};
$inverseOnSurface: {{colors.inverse_on_surface.default.hex}};
$inversePrimary: {{colors.inverse_primary.default.hex}};

Then configure your matugen.toml to output it to Kader's watch path:

[templates.kader_gallery]
input_path = '~/.config/matugen/templates/kader-colors.scss'
output_path = '~/.local/state/quickshell/user/generated/material_colors.scss'

Kader parses both standard camelCase and snake_case tokens and will seamlessly update at runtime when Matugen generates new colors!