diff --git a/src/components/BackgroundTasks.tsx b/src/components/BackgroundTasks.tsx index 8930943..e0cd20a 100644 --- a/src/components/BackgroundTasks.tsx +++ b/src/components/BackgroundTasks.tsx @@ -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 = { 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 = { - 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>({}); - const [paused, setPaused] = useState>>({}); const [failedItems, setFailedItems] = useState>({}); - 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("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) => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ba47dd6..ae67c0d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,21 +12,25 @@ function FolderContextMenu({ menu, folder, isMuted, + isPausedAll, onClose, onRename, onReindex, onLocate, onToggleMute, + onTogglePauseAll, onRemove, }: { menu: ContextMenuState; folder: Folder; isMuted: boolean; + isPausedAll: boolean; onClose: () => void; onRename: () => void; onReindex: () => void; onLocate: () => void; onToggleMute: () => void; + onTogglePauseAll: () => void; onRemove: () => void; }) { const ref = useRef(null); @@ -65,6 +69,7 @@ function FolderContextMenu({ > {item("Reindex", onReindex)} {item("Rename", onRename)} + {item(isPausedAll ? "Resume background work" : "Pause background work", onTogglePauseAll)} {item(isMuted ? "Unmute notifications" : "Mute notifications", onToggleMute)} {folder.scan_error && item("Locate Folder", onLocate)}
@@ -84,7 +89,12 @@ function FolderItem({ }) { const { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath, toggleMutedFolder } = useGalleryStore(); const mutedFolderIds = useGalleryStore((state) => state.mutedFolderIds); + const setAllWorkersPaused = useGalleryStore((state) => state.setAllWorkersPaused); + const folderWorkers = useGalleryStore((state) => state.workerPaused[folder.id]); const isMuted = mutedFolderIds.includes(folder.id); + // "Fully paused" only when every worker for this folder is paused. + const isPausedAll = !!folderWorkers && + folderWorkers.thumbnail && folderWorkers.metadata && folderWorkers.embedding && folderWorkers.tagging; const isIndexing = progress && !progress.done; const isMissing = !!folder.scan_error && !isIndexing; @@ -258,11 +268,13 @@ function FolderItem({ menu={contextMenu} folder={folder} isMuted={isMuted} + isPausedAll={isPausedAll} onClose={() => setContextMenu(null)} onRename={() => setRenaming(true)} onReindex={() => void reindexFolder(folder.id)} onLocate={() => void handleLocateFolder()} onToggleMute={() => toggleMutedFolder(folder.id)} + onTogglePauseAll={() => setAllWorkersPaused(folder.id, !isPausedAll)} onRemove={() => setConfirmingRemoval(true)} /> )} diff --git a/src/store.ts b/src/store.ts index 8a58414..5177193 100644 --- a/src/store.ts +++ b/src/store.ts @@ -266,6 +266,18 @@ export type SortOrder = export type UpdateStatus = "idle" | "checking" | "upToDate" | "available" | "downloading" | "installing" | "error"; +export type WorkerKey = "thumbnail" | "metadata" | "embedding" | "tagging"; + +const WORKER_KEYS: WorkerKey[] = ["thumbnail", "metadata", "embedding", "tagging"]; + +interface FolderWorkerStates { + folder_id: number; + thumbnail_paused: boolean; + metadata_paused: boolean; + embedding_paused: boolean; + tagging_paused: boolean; +} + export type FfmpegStatus = "unknown" | "starting" | "downloading" | "unpacking" | "installed" | "error"; interface FfmpegProgressEvent { @@ -329,6 +341,9 @@ interface GalleryState { taggingQueueFolderIds: number[]; mutedFolderIds: number[]; notificationsPaused: boolean; + // Per-folder background-worker pause flags, shared by the BackgroundTasks + // bar and the sidebar folder context menu. + workerPaused: Record>; appVersion: string | null; updateStatus: UpdateStatus; @@ -421,6 +436,9 @@ interface GalleryState { toggleMutedFolder: (folderId: number) => void; loadNotificationsPaused: () => Promise; setNotificationsPaused: (paused: boolean) => void; + loadWorkerStates: () => Promise; + setWorkerPaused: (folderId: number, worker: WorkerKey, paused: boolean) => void; + setAllWorkersPaused: (folderId: number, paused: boolean) => void; loadAppVersion: () => Promise; checkForUpdates: (options?: { quiet?: boolean }) => Promise; installUpdate: () => Promise; @@ -708,6 +726,7 @@ export const useGalleryStore = create((set, get) => ({ taggingQueueFolderIds: [], mutedFolderIds: [], notificationsPaused: false, + workerPaused: {}, appVersion: null, updateStatus: "idle", @@ -1526,6 +1545,62 @@ export const useGalleryStore = create((set, get) => ({ void invoke("set_notifications_paused", { paused }).catch(() => {}); }, + loadWorkerStates: async () => { + const folderIds = get().folders.map((folder) => folder.id); + if (folderIds.length === 0) { + set({ workerPaused: {} }); + return; + } + try { + const states = await invoke("get_worker_states", { folderIds }); + set({ + workerPaused: Object.fromEntries( + states.map((state) => [ + state.folder_id, + { + thumbnail: state.thumbnail_paused, + metadata: state.metadata_paused, + embedding: state.embedding_paused, + tagging: state.tagging_paused, + }, + ]), + ), + }); + } catch { + // leave the existing snapshot in place + } + }, + + setWorkerPaused: (folderId, worker, paused) => { + set((state) => { + const current = state.workerPaused[folderId] ?? { + thumbnail: false, + metadata: false, + embedding: false, + tagging: false, + }; + return { + workerPaused: { + ...state.workerPaused, + [folderId]: { ...current, [worker]: paused }, + }, + }; + }); + void invoke("set_worker_paused", { worker, folderId, paused }).catch(() => {}); + }, + + setAllWorkersPaused: (folderId, paused) => { + set((state) => ({ + workerPaused: { + ...state.workerPaused, + [folderId]: { thumbnail: paused, metadata: paused, embedding: paused, tagging: paused }, + }, + })); + for (const worker of WORKER_KEYS) { + void invoke("set_worker_paused", { worker, folderId, paused }).catch(() => {}); + } + }, + loadAppVersion: async () => { try { set({ appVersion: await getVersion() });