-
{title}
- {description ?
{description}
: null}
+function SettingsItem({ label, description, children, vertical = false }: {
+ label: React.ReactNode;
+ description?: React.ReactNode;
+ children?: React.ReactNode;
+ vertical?: boolean;
+}) {
+ if (vertical) {
+ return (
+
+
{label}
+ {description ?
{description}
: null}
+ {children ?
{children}
: null}
-
{children}
+ );
+ }
+
+ return (
+
+
+
{label}
+ {description ?
{description}
: null}
+
+
{children}
);
}
-function SettingsRow({ title, description, children }: { title: string; description: string; children: React.ReactNode }) {
+function StatPair({ label, value, accent = false }: { label: string; value: string; accent?: boolean }) {
return (
-
-
-
{title}
-
{description}
-
-
{children}
-
+
+ {label}
+ {value}
+
);
}
@@ -72,7 +82,7 @@ function ScopeButton({ scope, current, onSelect, children }: {
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
active
? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200"
- : "border-white/10 bg-white/[0.045] text-gray-500 hover:bg-white/[0.075] hover:text-gray-200"
+ : "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200"
}`}
onClick={() => onSelect(scope)}
>
@@ -94,7 +104,7 @@ function TaggerAccelerationButton({ acceleration, current, onSelect, children }:
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
active
? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200"
- : "border-white/10 bg-white/[0.045] text-gray-500 hover:bg-white/[0.075] hover:text-gray-200"
+ : "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200"
}`}
onClick={() => onSelect(acceleration)}
>
@@ -277,7 +287,7 @@ export function SettingsModal() {
return (
setSettingsOpen(false)}>
event.stopPropagation()}
>
@@ -301,417 +311,391 @@ export function SettingsModal() {
-
-
-
-
{activeSection === "workspace" ? "AI Workspace" : "General"}
-
{activeSection === "workspace" ? "Model setup and queue targets" : "App data and diagnostics"}
-
-
setSettingsOpen(false)}
- title="Close settings"
- >
-
-
-
-
-
+ setSettingsOpen(false)}
+ title="Close settings"
+ >
+
+
+
+
+
+
+
+
{activeSection === "workspace" ? "AI Workspace" : "General"}
+
{activeSection === "workspace" ? "Model setup and queue targets" : "App data and diagnostics"}
-
{activeSection === "workspace" ? (
-
-
-
-
-
-
-
-
WD SwinV2 Tagger v3
-
- {taggerReady ? "Installed" : taggerModelPreparing ? "Preparing" : "Not installed"}
-
-
-
- Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds.
-
-
-
+
+
+
+ WD SwinV2 Tagger v3{" "}
+
+
+ {taggerReady ? "Installed" : taggerModelPreparing ? "Preparing" : "Not installed"}
+
+
+ >
+ }
+ description="Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds."
+ >
+
+ {taggerReady ? (
+ <>
void prepareTaggerModel()}
- disabled={taggerModelPreparing || taggerReady}
+ className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
+ onClick={() => void probeTaggerRuntime()}
+ disabled={taggerRuntimeChecking}
>
- {taggerModelProgress ? : null}
- {taggerDownloadLabel}
+ {taggerRuntimeChecking ? "Checking runtime..." : "Check runtime"}
- {taggerReady ? (
- <>
- void probeTaggerRuntime()}
- disabled={taggerRuntimeChecking}
- >
- {taggerRuntimeChecking ? "Checking runtime..." : "Check runtime"}
-
- void deleteTaggerModel()}
- disabled={taggerModelPreparing}
- >
- Delete model files
-
- >
- ) : null}
-
-
-
-
-
-
-
- {(["auto", "directml", "cpu"] as const).map((acceleration) => (
- {
- setTaggerAccelerationSaving(true);
- setTaggerAccelerationError(null);
- void setTaggerAcceleration(nextAcceleration)
- .catch((error: unknown) => setTaggerAccelerationError(String(error)))
- .finally(() => setTaggerAccelerationSaving(false));
- }}
- >
- {acceleration === "directml" ? "DirectML" : acceleration === "cpu" ? "CPU" : "Auto"}
-
- ))}
-
- {taggerAccelerationError ? (
-
{taggerAccelerationError}
- ) : (
-
{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}
- )}
-
-
-
-
-
-
setTaggerThresholdDraft(event.target.value)}
- onBlur={() => {
- const value = parseFloat(thresholdDisplay);
- if (!isNaN(value) && value >= 0.05 && value <= 0.99) {
- setTaggerThresholdError(null);
- setTaggerThresholdSaving(true);
- void setTaggerThreshold(value)
- .catch((error: unknown) => setTaggerThresholdError(String(error)))
- .finally(() => {
- setTaggerThresholdDraft(null);
- setTaggerThresholdSaving(false);
- });
- } else {
- setTaggerThresholdDraft(null);
- setTaggerThresholdError("Must be 0.05 – 0.99");
- if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current);
- thresholdErrorTimerRef.current = setTimeout(() => setTaggerThresholdError(null), 2000);
- }
- }}
- />
- {taggerThresholdError ? (
-
{taggerThresholdError}
- ) : (
-
{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}
- )}
-
-
-
-
-
-
setTaggerBatchSizeDraft(event.target.value)}
- onBlur={() => {
- const value = parseInt(batchSizeDisplay, 10);
- if (!isNaN(value) && value >= 1 && value <= 100) {
- setTaggerBatchSizeError(null);
- setTaggerBatchSizeSaving(true);
- void setTaggerBatchSize(value)
- .catch((error: unknown) => setTaggerQueueStatus(String(error)))
- .finally(() => {
- setTaggerBatchSizeDraft(null);
- setTaggerBatchSizeSaving(false);
- });
- } else {
- setTaggerBatchSizeDraft(null);
- setTaggerBatchSizeError("Must be 1 – 100");
- if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current);
- batchSizeErrorTimerRef.current = setTimeout(() => setTaggerBatchSizeError(null), 2000);
- }
- }}
- />
- {taggerBatchSizeError ? (
-
{taggerBatchSizeError}
- ) : (
-
{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}
- )}
-
-
-
-
-
Model location
-
- {taggerReady ? taggerModelStatus?.local_dir : "Not downloaded"}
-
- {taggerModelProgress?.current_file ?
{taggerModelProgress.current_file}
: null}
- {taggerModelError ?
{taggerModelError}
: null}
- {taggerRuntimeProbe ? (
-
-
-
Runtime check
-
Ready
-
-
Tagger acceleration: {taggerRuntimeProbe.acceleration}
-
{taggerRuntimeProbe.session.file}
-
- ) : null}
-
-
+
void deleteTaggerModel()}
+ disabled={taggerModelPreparing}
+ >
+ Delete model files
+
+ >
+ ) : (
+
void prepareTaggerModel()}
+ disabled={taggerModelPreparing}
+ >
+ {taggerModelProgress ? : null}
+ {taggerDownloadLabel}
+
+ )}
-
+
-
-
-
- All media
- Selected folders
-
-
-
-
-
-
-
Folder selection
-
Current target: {queueScopeLabel}.
-
-
-
setTaggingQueueFolderIds(folders.map((folder) => folder.id))}
- disabled={taggingQueueScope === "all" || folders.length === 0}
+
+
+
+ {(["auto", "directml", "cpu"] as const).map((acceleration) => (
+ {
+ setTaggerAccelerationSaving(true);
+ setTaggerAccelerationError(null);
+ void setTaggerAcceleration(nextAcceleration)
+ .catch((error: unknown) => setTaggerAccelerationError(String(error)))
+ .finally(() => setTaggerAccelerationSaving(false));
+ }}
>
- Select all
-
- setTaggingQueueFolderIds([])}
- disabled={taggingQueueScope === "all" || taggingQueueFolderIds.length === 0}
- >
- Clear
-
-
+ {acceleration === "directml" ? "DirectML" : acceleration === "cpu" ? "CPU" : "Auto"}
+
+ ))}
+ {taggerAccelerationError ? (
+ {taggerAccelerationError}
+ ) : (
+ {taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}
+ )}
+
+
-
- {folders.map((folder) => {
- const active = taggingQueueFolderIds.includes(folder.id);
- const progress = mediaJobProgress[folder.id];
- return (
-
toggleTaggingQueueFolder(folder.id)}
- disabled={taggingQueueScope === "all"}
- >
-
-
{folder.name}
-
{folder.image_count.toLocaleString()} items
-
-
- {(progress?.tagging_pending ?? 0) > 0 ? {progress?.tagging_pending} queued : null}
-
-
-
- );
- })}
- {folders.length === 0 ?
Add a folder first to enable targeted tagging queues.
: null}
+
+
+
setTaggerThresholdDraft(event.target.value)}
+ onBlur={() => {
+ const value = parseFloat(thresholdDisplay);
+ if (!isNaN(value) && value >= 0.05 && value <= 0.99) {
+ setTaggerThresholdError(null);
+ setTaggerThresholdSaving(true);
+ void setTaggerThreshold(value)
+ .catch((error: unknown) => setTaggerThresholdError(String(error)))
+ .finally(() => {
+ setTaggerThresholdDraft(null);
+ setTaggerThresholdSaving(false);
+ });
+ } else {
+ setTaggerThresholdDraft(null);
+ setTaggerThresholdError("Must be 0.05 – 0.99");
+ if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current);
+ thresholdErrorTimerRef.current = setTimeout(() => setTaggerThresholdError(null), 2000);
+ }
+ }}
+ />
+ {taggerThresholdError ? (
+
{taggerThresholdError}
+ ) : (
+
{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}
+ )}
+
+
+
+
+
+
setTaggerBatchSizeDraft(event.target.value)}
+ onBlur={() => {
+ const value = parseInt(batchSizeDisplay, 10);
+ if (!isNaN(value) && value >= 1 && value <= 100) {
+ setTaggerBatchSizeError(null);
+ setTaggerBatchSizeSaving(true);
+ void setTaggerBatchSize(value)
+ .catch((error: unknown) => setTaggerQueueStatus(String(error)))
+ .finally(() => {
+ setTaggerBatchSizeDraft(null);
+ setTaggerBatchSizeSaving(false);
+ });
+ } else {
+ setTaggerBatchSizeDraft(null);
+ setTaggerBatchSizeError("Must be 1 – 100");
+ if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current);
+ batchSizeErrorTimerRef.current = setTimeout(() => setTaggerBatchSizeError(null), 2000);
+ }
+ }}
+ />
+ {taggerBatchSizeError ? (
+
{taggerBatchSizeError}
+ ) : (
+
{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}
+ )}
+
+
+
+
+
+
+ {taggerReady ? taggerModelStatus?.local_dir : "Not downloaded"}
+
+ {taggerModelProgress?.current_file ?
{taggerModelProgress.current_file}
: null}
+ {taggerModelError ?
{taggerModelError}
: null}
+ {taggerRuntimeProbe ? (
+
+
+ Runtime check Ready
+ acceleration: {taggerRuntimeProbe.acceleration}
+
+
{taggerRuntimeProbe.session.file}
+
+ ) : null}
+
+
+
+
+
+
+
+ All media
+ Selected folders
+
+
+
+
+
+
+
Folder selection
+
Current target: {queueScopeLabel}.
+
+
+ setTaggingQueueFolderIds(folders.map((folder) => folder.id))}
+ disabled={taggingQueueScope === "all" || folders.length === 0}
+ >
+ Select all
+
+ setTaggingQueueFolderIds([])}
+ disabled={taggingQueueScope === "all" || taggingQueueFolderIds.length === 0}
+ >
+ Clear
+
-
-
- runQueueAction("queue")}
- disabled={!taggerReady || taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)}
- >
- {taggerQueueing ? "Queueing..." : "Queue tagging"}
-
- runQueueAction("clear")}
- disabled={taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)}
- >
- {taggerClearing ? "Clearing..." : "Clear queued jobs"}
-
-
-
+
+ {folders.map((folder) => {
+ const active = taggingQueueFolderIds.includes(folder.id);
+ const progress = mediaJobProgress[folder.id];
+ return (
+
toggleTaggingQueueFolder(folder.id)}
+ disabled={taggingQueueScope === "all"}
+ >
+ {folder.name}
+
+ {(progress?.tagging_pending ?? 0) > 0 ? {progress?.tagging_pending} queued : null}
+ {folder.image_count.toLocaleString()} items
+
+
+
+ );
+ })}
+ {folders.length === 0 ?
Add a folder first to enable targeted tagging queues.
: null}
+
+
- {taggerQueueStatus ? {taggerQueueStatus}
: null}
-
-
+
+
+ runQueueAction("queue")}
+ disabled={!taggerReady || taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)}
+ >
+ {taggerQueueing ? "Queueing..." : "Queue tagging"}
+
+ runQueueAction("clear")}
+ disabled={taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)}
+ >
+ {taggerClearing ? "Clearing..." : "Clear queued jobs"}
+
+
+
+
+ {taggerQueueStatus ? {taggerQueueStatus}
: null}
+
) : (
-
-
-
-
-
Open the app data folder in Explorer to inspect or back up files.
-
{
- setOpeningDataFolder(true);
- void openAppDataFolder().finally(() => setOpeningDataFolder(false));
- }}
- disabled={openingDataFolder}
- >
- {openingDataFolder ? "Opening..." : "Open data folder"}
-
-
-
-
-
+
+
+ {
+ setOpeningDataFolder(true);
+ void openAppDataFolder().finally(() => setOpeningDataFolder(false));
+ }}
+ disabled={openingDataFolder}
+ >
+ {openingDataFolder ? "Opening..." : "Open data folder"}
+
+
+
-
-
-
Pause all notifications
-
Suppress all indexing notifications until re-enabled.
-
-
setNotificationsPaused(!notificationsPaused)}
- >
-
-
-
-
+ setNotificationsPaused(!notificationsPaused)}
+ >
+
+
+
+
-
-
-
-
-
Database size
-
- {vacuumResult
- ? `${vacuumResult.after_mb.toFixed(1)} MB`
- : dbInfo
- ? `${dbInfo.size_mb.toFixed(1)} MB`
- : "—"}
-
-
-
-
Reclaimable
-
- {vacuumResult
- ? `−${vacuumResult.freed_mb.toFixed(1)} MB freed`
- : dbInfo
- ? `${dbInfo.reclaimable_mb.toFixed(1)} MB`
- : "—"}
-
-
-
-
-
+
+
+ Reclaims wasted space left behind when images or tags are deleted. Safe to run at any time.
+
+
+
+
+
{vacuumResult
? `Compacted from ${vacuumResult.before_mb.toFixed(1)} MB to ${vacuumResult.after_mb.toFixed(1)} MB.`
: dbInfo && dbInfo.reclaimable_mb < 0.5
? "Database is already compact."
: "Run this after removing folders or bulk-deleting images."}
-
-
{
- setVacuuming(true);
- setVacuumResult(null);
- void vacuumDatabase()
- .then((result) => {
- setVacuumResult(result);
- setDbInfo({ size_mb: result.after_mb, reclaimable_mb: 0 });
- })
- .catch(() => {})
- .finally(() => setVacuuming(false));
- }}
- disabled={vacuuming || (dbInfo !== null && dbInfo.reclaimable_mb < 0.5 && vacuumResult === null)}
- >
- {vacuuming ? "Compacting..." : "Compact now"}
-
-
-
-
-
-
+ >
+ }
>
-
-
-
-
Orphaned files
-
- {cleaningThumbnails
- ? "—"
- : thumbnailCleanupResult
+ {
+ setVacuuming(true);
+ setVacuumResult(null);
+ void vacuumDatabase()
+ .then((result) => {
+ setVacuumResult(result);
+ setDbInfo({ size_mb: result.after_mb, reclaimable_mb: 0 });
+ })
+ .catch(() => {})
+ .finally(() => setVacuuming(false));
+ }}
+ disabled={vacuuming || (dbInfo !== null && dbInfo.reclaimable_mb < 0.5)}
+ >
+ {vacuuming ? "Compacting..." : "Compact now"}
+
+
+
+
+ Thumbnails left behind when folders or images are removed. Safe to delete — they are regenerated if the originals are re-indexed.
+
+
-
-
-
Reclaimable
-
- {cleaningThumbnails
- ? "—"
- : thumbnailCleanupResult
- ? "0 MB"
+ : "—"
+ }
+ />
+
-
-
-
-
+ : "—"
+ }
+ />
+
+
{cleaningThumbnails
? "Scanning and removing orphaned thumbnails…"
: thumbnailCleanupResult
@@ -719,29 +703,30 @@ export function SettingsModal() {
: thumbnailInfo && thumbnailInfo.count === 0
? "No orphaned thumbnails found."
: thumbnailInfo && thumbnailInfo.count > 1000
- ? "Remove thumbnails no longer associated with any indexed image. This may take a few minutes for large collections."
+ ? "May take a few minutes for large collections."
: "Remove thumbnails no longer associated with any indexed image."}
-
-
{
- setCleaningThumbnails(true);
- cleanupOrphanedThumbnails()
- .then((result) => {
- setThumbnailCleanupResult(result);
- setThumbnailInfo(null);
- })
- .catch(() => {})
- .finally(() => setCleaningThumbnails(false));
- }}
- disabled={cleaningThumbnails || thumbnailCleanupResult !== null || (thumbnailInfo !== null && thumbnailInfo.count === 0)}
- >
- {cleaningThumbnails ? "Cleaning…" : "Clean up"}
-
-
-
-
-
+
+ >
+ }
+ >
+
{
+ setCleaningThumbnails(true);
+ cleanupOrphanedThumbnails()
+ .then((result) => {
+ setThumbnailCleanupResult(result);
+ setThumbnailInfo(null);
+ })
+ .catch(() => {})
+ .finally(() => setCleaningThumbnails(false));
+ }}
+ disabled={cleaningThumbnails || thumbnailCleanupResult !== null || (thumbnailInfo !== null && thumbnailInfo.count === 0)}
+ >
+ {cleaningThumbnails ? "Cleaning…" : "Clean up"}
+
+
+
)}