feat(sidebar): pause/resume all background work from folder context menu

The onboarding tour tells users they can right-click a folder to pause its
background work, but only the per-worker buttons in the BackgroundTasks bar
existed. Add a 'Pause/Resume background work' context-menu item that toggles
all four workers (thumbnail/metadata/embedding/tagging) for the folder.

To keep the bar and the menu in sync, worker-pause state moves from
BackgroundTasks' local state into the store (the canonical owner of app
state), so pausing from either surface reflects in both. Reuses the existing
set_worker_paused / get_worker_states commands; no backend change.
This commit is contained in:
2026-06-13 09:42:51 +01:00
parent 2f66b0bdb8
commit 075c7e4cfb
3 changed files with 96 additions and 52 deletions
+9 -52
View File
@@ -1,8 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useGalleryStore } from "../store";
type WorkerKey = "thumbnail" | "metadata" | "embedding" | "tagging";
import { useGalleryStore, WorkerKey } from "../store";
const WORKER_FOR_STAGE: Record<string, WorkerKey> = {
Thumbnails: "thumbnail",
@@ -38,21 +36,6 @@ interface FailedEmbeddingItem {
error: string | null;
}
interface FolderWorkerStates {
folder_id: number;
thumbnail_paused: boolean;
metadata_paused: boolean;
embedding_paused: boolean;
tagging_paused: boolean;
}
const DEFAULT_PAUSED_STATE: Record<WorkerKey, boolean> = {
thumbnail: false,
metadata: false,
embedding: false,
tagging: false,
};
export function BackgroundTasks() {
const folders = useGalleryStore((state) => state.folders);
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
@@ -63,32 +46,15 @@ export function BackgroundTasks() {
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
const [expanded, setExpanded] = useState(false);
const [dismissed, setDismissed] = useState<Record<number, string>>({});
const [paused, setPaused] = useState<Record<number, Record<WorkerKey, boolean>>>({});
const [failedItems, setFailedItems] = useState<Record<number, FailedEmbeddingItem[]>>({});
useEffect(() => {
const folderIds = folders.map((folder) => folder.id);
if (folderIds.length === 0) {
setPaused({});
return;
}
const workerPaused = useGalleryStore((state) => state.workerPaused);
const loadWorkerStates = useGalleryStore((state) => state.loadWorkerStates);
const setWorkerPaused = useGalleryStore((state) => state.setWorkerPaused);
invoke<FolderWorkerStates[]>("get_worker_states", { folderIds }).then((states) => {
setPaused(
Object.fromEntries(
states.map((state) => [
state.folder_id,
{
thumbnail: state.thumbnail_paused,
metadata: state.metadata_paused,
embedding: state.embedding_paused,
tagging: state.tagging_paused,
},
]),
),
);
});
}, [folders]);
useEffect(() => {
void loadWorkerStates();
}, [folders, loadWorkerStates]);
// Fetch failed embedding filenames whenever the expanded panel opens or failure counts change.
const failedCounts = useMemo(
@@ -113,20 +79,11 @@ export function BackgroundTasks() {
}, [expanded, failedCounts]);
const isWorkerPaused = (folderId: number, worker: WorkerKey) => {
return paused[folderId]?.[worker] ?? DEFAULT_PAUSED_STATE[worker];
return workerPaused[folderId]?.[worker] ?? false;
};
const toggleWorker = (folderId: number, worker: WorkerKey) => {
const next = !isWorkerPaused(folderId, worker);
setPaused((prev) => ({
...prev,
[folderId]: {
...DEFAULT_PAUSED_STATE,
...prev[folderId],
[worker]: next,
},
}));
void invoke("set_worker_paused", { worker, folderId, paused: next });
setWorkerPaused(folderId, worker, !isWorkerPaused(folderId, worker));
};
const dismissTask = (id: number, snapshot: string) => {