feat: notification batching, per-folder mute, and global pause

Debounce embedding/tagging completion notifications per folder (6s window)
so rapid file additions from downloaders fire one notification instead of
one per file. Add per-folder mute via the sidebar context menu and a global
pause toggle in Settings > General, both persisted across restarts.
This commit is contained in:
2026-06-11 06:28:16 +01:00
parent b89e7406e9
commit a34d38d9d3
6 changed files with 200 additions and 27 deletions
+22
View File
@@ -162,6 +162,8 @@ export function SettingsModal() {
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders);
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
const notificationsPaused = useGalleryStore((state) => state.notificationsPaused);
const setNotificationsPaused = useGalleryStore((state) => state.setNotificationsPaused);
const getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo);
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
const getOrphanedThumbnailsInfo = useGalleryStore((state) => state.getOrphanedThumbnailsInfo);
@@ -601,6 +603,26 @@ export function SettingsModal() {
</div>
</SettingsCard>
<SettingsCard
title="Notifications"
description="Notifications are batched per folder — a single alert fires once activity settles. Mute individual folders from their right-click menu."
>
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
<div>
<p className="text-sm font-medium text-white">Pause all notifications</p>
<p className="mt-0.5 text-xs text-gray-500">Suppress all indexing notifications until re-enabled.</p>
</div>
<button
role="switch"
aria-checked={notificationsPaused}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${notificationsPaused ? "bg-sky-500" : "bg-white/15"}`}
onClick={() => setNotificationsPaused(!notificationsPaused)}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${notificationsPaused ? "translate-x-4" : "translate-x-0"}`} />
</button>
</div>
</SettingsCard>
<SettingsCard
title="Compact database"
description="Reclaims wasted space left behind when images or tags are deleted. Safe to run at any time."
+10 -1
View File
@@ -11,18 +11,22 @@ interface ContextMenuState {
function FolderContextMenu({
menu,
folder,
isMuted,
onClose,
onRename,
onReindex,
onLocate,
onToggleMute,
onRemove,
}: {
menu: ContextMenuState;
folder: Folder;
isMuted: boolean;
onClose: () => void;
onRename: () => void;
onReindex: () => void;
onLocate: () => void;
onToggleMute: () => void;
onRemove: () => void;
}) {
const ref = useRef<HTMLDivElement>(null);
@@ -61,6 +65,7 @@ function FolderContextMenu({
>
{item("Reindex", onReindex)}
{item("Rename", onRename)}
{item(isMuted ? "Unmute notifications" : "Mute notifications", onToggleMute)}
{folder.scan_error && item("Locate Folder", onLocate)}
<div className="my-1 border-t border-white/[0.06]" />
{item("Remove from app", onRemove, true)}
@@ -77,7 +82,9 @@ function FolderItem({
selected: boolean;
progress: IndexProgress | undefined;
}) {
const { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath } = useGalleryStore();
const { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath, toggleMutedFolder } = useGalleryStore();
const mutedFolderIds = useGalleryStore((state) => state.mutedFolderIds);
const isMuted = mutedFolderIds.includes(folder.id);
const isIndexing = progress && !progress.done;
const isMissing = !!folder.scan_error && !isIndexing;
@@ -250,10 +257,12 @@ function FolderItem({
<FolderContextMenu
menu={contextMenu}
folder={folder}
isMuted={isMuted}
onClose={() => setContextMenu(null)}
onRename={() => setRenaming(true)}
onReindex={() => void reindexFolder(folder.id)}
onLocate={() => void handleLocateFolder()}
onToggleMute={() => toggleMutedFolder(folder.id)}
onRemove={() => setConfirmingRemoval(true)}
/>
)}