feat: 0.1.1 — timeline scrubber, gallery virtualisation, folder reorder, QoL polish

Timeline:
- Add a right-edge scrubber (year labels + month dots) that jumps to any
  period; runs in the same direction as the scrolled content
- Load the full filtered set in Timeline view so the scrubber spans the whole
  library instead of just the first page

Gallery & UI:
- Virtualise the gallery grid
- Folder reordering in the sidebar (drag + persisted sort_order) and a themed
  dropdown component
- Subtle Light theme contrast fixes for toggles, secondary buttons and the
  update toast

Embedding workers now defer video jobs without a thumbnail at claim time and
requeue any previously-failed deferred jobs on startup, so videos no longer
churn through failed embeddings.

QoL polish across BackgroundTasks, DuplicateFinder, Lightbox and VideoPlayer.
This commit is contained in:
2026-06-17 18:18:35 +01:00
parent f049f8c997
commit 9047c8053a
18 changed files with 868 additions and 196 deletions
+7 -5
View File
@@ -18,7 +18,7 @@ use tauri::{AppHandle, Emitter};
use walkdir::WalkDir;
const IMAGE_EXTENSIONS: &[&str] = &[
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "avif", "heic", "heif",
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "avif",
];
const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "m4v", "webm"];
@@ -550,6 +550,9 @@ fn build_record(
let filename = path.file_name()?.to_string_lossy().to_string();
let metadata = std::fs::metadata(path).ok()?;
let file_size = metadata.len() as i64;
if file_size == 0 {
return None;
}
let modified_at = metadata.modified().ok().map(|time| {
let date_time: chrono::DateTime<chrono::Utc> = time.into();
date_time.to_rfc3339()
@@ -869,14 +872,14 @@ fn process_embedding_batch(
let embedder = embedder.as_ref().expect("embedder should be initialized");
let infer_started_at = Instant::now();
// Resolve the source path for each job. Videos without a thumbnail produce an Err
// here — those jobs are marked failed immediately without going to the embedder.
// Resolve each source path. Video jobs without thumbnails are not claimable, so an
// error here represents a real race or missing thumbnail rather than normal deferral.
let source_results: Vec<Result<PathBuf>> = jobs
.iter()
.map(|job| embedding_source_path(&job.path, job.thumbnail_path.as_deref(), &job.media_kind))
.collect();
// Separate jobs with a valid source from those that fail early (e.g. video with no thumbnail).
// Separate jobs with a valid source from genuine early failures.
let mut embeddable_indices: Vec<usize> = Vec::new();
let mut embeddable_paths: Vec<PathBuf> = Vec::new();
// image_id -> early error message for jobs that cannot be embedded yet
@@ -1372,7 +1375,6 @@ fn mime_for_ext(ext: &str) -> &'static str {
"webp" => "image/webp",
"tiff" | "tif" => "image/tiff",
"avif" => "image/avif",
"heic" | "heif" => "image/heif",
"mp4" | "m4v" => "video/mp4",
"mov" => "video/quicktime",
"webm" => "video/webm",