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 { useEffect, useMemo, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useGalleryStore } from "../store";
|
import { useGalleryStore, WorkerKey } from "../store";
|
||||||
|
|
||||||
type WorkerKey = "thumbnail" | "metadata" | "embedding" | "tagging";
|
|
||||||
|
|
||||||
const WORKER_FOR_STAGE: Record<string, WorkerKey> = {
|
const WORKER_FOR_STAGE: Record<string, WorkerKey> = {
|
||||||
Thumbnails: "thumbnail",
|
Thumbnails: "thumbnail",
|
||||||
@@ -38,21 +36,6 @@ interface FailedEmbeddingItem {
|
|||||||
error: string | null;
|
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() {
|
export function BackgroundTasks() {
|
||||||
const folders = useGalleryStore((state) => state.folders);
|
const folders = useGalleryStore((state) => state.folders);
|
||||||
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
|
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
|
||||||
@@ -63,32 +46,15 @@ export function BackgroundTasks() {
|
|||||||
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
|
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [dismissed, setDismissed] = useState<Record<number, string>>({});
|
const [dismissed, setDismissed] = useState<Record<number, string>>({});
|
||||||
const [paused, setPaused] = useState<Record<number, Record<WorkerKey, boolean>>>({});
|
|
||||||
const [failedItems, setFailedItems] = useState<Record<number, FailedEmbeddingItem[]>>({});
|
const [failedItems, setFailedItems] = useState<Record<number, FailedEmbeddingItem[]>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
const workerPaused = useGalleryStore((state) => state.workerPaused);
|
||||||
const folderIds = folders.map((folder) => folder.id);
|
const loadWorkerStates = useGalleryStore((state) => state.loadWorkerStates);
|
||||||
if (folderIds.length === 0) {
|
const setWorkerPaused = useGalleryStore((state) => state.setWorkerPaused);
|
||||||
setPaused({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
invoke<FolderWorkerStates[]>("get_worker_states", { folderIds }).then((states) => {
|
useEffect(() => {
|
||||||
setPaused(
|
void loadWorkerStates();
|
||||||
Object.fromEntries(
|
}, [folders, loadWorkerStates]);
|
||||||
states.map((state) => [
|
|
||||||
state.folder_id,
|
|
||||||
{
|
|
||||||
thumbnail: state.thumbnail_paused,
|
|
||||||
metadata: state.metadata_paused,
|
|
||||||
embedding: state.embedding_paused,
|
|
||||||
tagging: state.tagging_paused,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [folders]);
|
|
||||||
|
|
||||||
// Fetch failed embedding filenames whenever the expanded panel opens or failure counts change.
|
// Fetch failed embedding filenames whenever the expanded panel opens or failure counts change.
|
||||||
const failedCounts = useMemo(
|
const failedCounts = useMemo(
|
||||||
@@ -113,20 +79,11 @@ export function BackgroundTasks() {
|
|||||||
}, [expanded, failedCounts]);
|
}, [expanded, failedCounts]);
|
||||||
|
|
||||||
const isWorkerPaused = (folderId: number, worker: WorkerKey) => {
|
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 toggleWorker = (folderId: number, worker: WorkerKey) => {
|
||||||
const next = !isWorkerPaused(folderId, worker);
|
setWorkerPaused(folderId, worker, !isWorkerPaused(folderId, worker));
|
||||||
setPaused((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[folderId]: {
|
|
||||||
...DEFAULT_PAUSED_STATE,
|
|
||||||
...prev[folderId],
|
|
||||||
[worker]: next,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
void invoke("set_worker_paused", { worker, folderId, paused: next });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismissTask = (id: number, snapshot: string) => {
|
const dismissTask = (id: number, snapshot: string) => {
|
||||||
|
|||||||
@@ -12,21 +12,25 @@ function FolderContextMenu({
|
|||||||
menu,
|
menu,
|
||||||
folder,
|
folder,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isPausedAll,
|
||||||
onClose,
|
onClose,
|
||||||
onRename,
|
onRename,
|
||||||
onReindex,
|
onReindex,
|
||||||
onLocate,
|
onLocate,
|
||||||
onToggleMute,
|
onToggleMute,
|
||||||
|
onTogglePauseAll,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
menu: ContextMenuState;
|
menu: ContextMenuState;
|
||||||
folder: Folder;
|
folder: Folder;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
isPausedAll: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRename: () => void;
|
onRename: () => void;
|
||||||
onReindex: () => void;
|
onReindex: () => void;
|
||||||
onLocate: () => void;
|
onLocate: () => void;
|
||||||
onToggleMute: () => void;
|
onToggleMute: () => void;
|
||||||
|
onTogglePauseAll: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -65,6 +69,7 @@ function FolderContextMenu({
|
|||||||
>
|
>
|
||||||
{item("Reindex", onReindex)}
|
{item("Reindex", onReindex)}
|
||||||
{item("Rename", onRename)}
|
{item("Rename", onRename)}
|
||||||
|
{item(isPausedAll ? "Resume background work" : "Pause background work", onTogglePauseAll)}
|
||||||
{item(isMuted ? "Unmute notifications" : "Mute notifications", onToggleMute)}
|
{item(isMuted ? "Unmute notifications" : "Mute notifications", onToggleMute)}
|
||||||
{folder.scan_error && item("Locate Folder", onLocate)}
|
{folder.scan_error && item("Locate Folder", onLocate)}
|
||||||
<div className="my-1 border-t border-white/[0.06]" />
|
<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 { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath, toggleMutedFolder } = useGalleryStore();
|
||||||
const mutedFolderIds = useGalleryStore((state) => state.mutedFolderIds);
|
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);
|
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 isIndexing = progress && !progress.done;
|
||||||
const isMissing = !!folder.scan_error && !isIndexing;
|
const isMissing = !!folder.scan_error && !isIndexing;
|
||||||
|
|
||||||
@@ -258,11 +268,13 @@ function FolderItem({
|
|||||||
menu={contextMenu}
|
menu={contextMenu}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
|
isPausedAll={isPausedAll}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={() => setContextMenu(null)}
|
||||||
onRename={() => setRenaming(true)}
|
onRename={() => setRenaming(true)}
|
||||||
onReindex={() => void reindexFolder(folder.id)}
|
onReindex={() => void reindexFolder(folder.id)}
|
||||||
onLocate={() => void handleLocateFolder()}
|
onLocate={() => void handleLocateFolder()}
|
||||||
onToggleMute={() => toggleMutedFolder(folder.id)}
|
onToggleMute={() => toggleMutedFolder(folder.id)}
|
||||||
|
onTogglePauseAll={() => setAllWorkersPaused(folder.id, !isPausedAll)}
|
||||||
onRemove={() => setConfirmingRemoval(true)}
|
onRemove={() => setConfirmingRemoval(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -266,6 +266,18 @@ export type SortOrder =
|
|||||||
|
|
||||||
export type UpdateStatus = "idle" | "checking" | "upToDate" | "available" | "downloading" | "installing" | "error";
|
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";
|
export type FfmpegStatus = "unknown" | "starting" | "downloading" | "unpacking" | "installed" | "error";
|
||||||
|
|
||||||
interface FfmpegProgressEvent {
|
interface FfmpegProgressEvent {
|
||||||
@@ -329,6 +341,9 @@ interface GalleryState {
|
|||||||
taggingQueueFolderIds: number[];
|
taggingQueueFolderIds: number[];
|
||||||
mutedFolderIds: number[];
|
mutedFolderIds: number[];
|
||||||
notificationsPaused: boolean;
|
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;
|
appVersion: string | null;
|
||||||
updateStatus: UpdateStatus;
|
updateStatus: UpdateStatus;
|
||||||
@@ -421,6 +436,9 @@ interface GalleryState {
|
|||||||
toggleMutedFolder: (folderId: number) => void;
|
toggleMutedFolder: (folderId: number) => void;
|
||||||
loadNotificationsPaused: () => Promise<void>;
|
loadNotificationsPaused: () => Promise<void>;
|
||||||
setNotificationsPaused: (paused: boolean) => 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>;
|
loadAppVersion: () => Promise<void>;
|
||||||
checkForUpdates: (options?: { quiet?: boolean }) => Promise<void>;
|
checkForUpdates: (options?: { quiet?: boolean }) => Promise<void>;
|
||||||
installUpdate: () => Promise<void>;
|
installUpdate: () => Promise<void>;
|
||||||
@@ -708,6 +726,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
taggingQueueFolderIds: [],
|
taggingQueueFolderIds: [],
|
||||||
mutedFolderIds: [],
|
mutedFolderIds: [],
|
||||||
notificationsPaused: false,
|
notificationsPaused: false,
|
||||||
|
workerPaused: {},
|
||||||
|
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
updateStatus: "idle",
|
updateStatus: "idle",
|
||||||
@@ -1526,6 +1545,62 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
void invoke("set_notifications_paused", { paused }).catch(() => {});
|
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 () => {
|
loadAppVersion: async () => {
|
||||||
try {
|
try {
|
||||||
set({ appVersion: await getVersion() });
|
set({ appVersion: await getVersion() });
|
||||||
|
|||||||
Reference in New Issue
Block a user