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:
@@ -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) => {
|
||||
|
||||
@@ -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<HTMLDivElement>(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)}
|
||||
<div className="my-1 border-t border-white/[0.06]" />
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<number, Record<WorkerKey, boolean>>;
|
||||
|
||||
appVersion: string | null;
|
||||
updateStatus: UpdateStatus;
|
||||
@@ -421,6 +436,9 @@ interface GalleryState {
|
||||
toggleMutedFolder: (folderId: number) => void;
|
||||
loadNotificationsPaused: () => Promise<void>;
|
||||
setNotificationsPaused: (paused: boolean) => void;
|
||||
loadWorkerStates: () => Promise<void>;
|
||||
setWorkerPaused: (folderId: number, worker: WorkerKey, paused: boolean) => void;
|
||||
setAllWorkersPaused: (folderId: number, paused: boolean) => void;
|
||||
loadAppVersion: () => Promise<void>;
|
||||
checkForUpdates: (options?: { quiet?: boolean }) => Promise<void>;
|
||||
installUpdate: () => Promise<void>;
|
||||
@@ -708,6 +726,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
taggingQueueFolderIds: [],
|
||||
mutedFolderIds: [],
|
||||
notificationsPaused: false,
|
||||
workerPaused: {},
|
||||
|
||||
appVersion: null,
|
||||
updateStatus: "idle",
|
||||
@@ -1526,6 +1545,62 @@ export const useGalleryStore = create<GalleryState>((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<FolderWorkerStates[]>("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() });
|
||||
|
||||
Reference in New Issue
Block a user