- QML 48.7%
- C++ 29.9%
- JavaScript 19.8%
- GLSL 0.7%
- CMake 0.7%
- Other 0.2%
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> |
||
|---|---|---|
| assets | ||
| include | ||
| qml | ||
| src | ||
| .gitignore | ||
| CMakeLists.txt | ||
| kader.desktop | ||
| PKGBUILD | ||
| README.md | ||
| search-in-video.txt | ||
| SUMMARY.md | ||
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!