From cd7dd89f003c8cfe58185f3f260c49bd90809354 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Fri, 12 Jun 2026 12:07:52 +0100 Subject: [PATCH] feat(settings): flat desktop redesign with proportional sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete presentation rework of the Settings modal away from the card-based tablet look: - Dialog now sizes proportionally (85vw x 85vh, capped at 1400x900) and resizes with the app window instead of a fixed centred square. - Content rebuilt as flat form rows in the VS Code preferences style: uppercase group headings with hairline-divided label/control rows — no nested cards, panels, or boxed strips. Sidebar retained. - Duplicated content header removed; close button floats top-right. - Folder selection restyled from bordered chips to flat divider rows; model path and runtime check render as plain monospace text. Also fixes two maintenance-section bugs: thumbnail-cache stats no longer blank to em-dashes while a cleanup is running, and the Compact now button disables immediately after compaction instead of allowing pointless re-runs until the section remounted. --- src/components/SettingsModal.tsx | 841 +++++++++++++++---------------- 1 file changed, 413 insertions(+), 428 deletions(-) diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 190344f..14ec1da 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -16,46 +16,56 @@ function StatusPill({ children, tone }: { children: React.ReactNode; tone: "read ? "border-sky-400/25 bg-sky-500/10 text-sky-300" : "border-white/10 bg-white/[0.04] text-gray-500"; - return {children}; + return {children}; } -function SectionShell({ eyebrow, title, description, children }: { - eyebrow: string; +function SettingsGroup({ title, description, children }: { title: string; description?: string; children: React.ReactNode; }) { return (
-

{eyebrow}

-

{title}

- {description ?

{description}

: null} -
{children}
+

{title}

+ {description ?

{description}

: null} +
{children}
); } -function SettingsCard({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) { - return ( -
-
-

{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()} >
-
-
-
-

{activeSection === "workspace" ? "AI Workspace" : "General"}

-

{activeSection === "workspace" ? "Model setup and queue targets" : "App data and diagnostics"}

-
- -
+ + +
+
+

{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 ? ( + <> - {taggerReady ? ( - <> - - - - ) : 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} -
-
+ + + ) : ( + + )}
- + - - -
- All media - Selected folders -
-
- -
-
-
-

Folder selection

-

Current target: {queueScopeLabel}.

-
-
- - -
+ {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 ( - - ); - })} - {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}.

+
+
+ +
- -
- - -
-
+
+ {folders.map((folder) => { + const active = taggingQueueFolderIds.includes(folder.id); + const progress = mediaJobProgress[folder.id]; + return ( + + ); + })} + {folders.length === 0 ?

Add a folder first to enable targeted tagging queues.

: null} +
+
- {taggerQueueStatus ?

{taggerQueueStatus}

: null} - - + +
+ + +
+
+ + {taggerQueueStatus ?

{taggerQueueStatus}

: null} +
) : ( -
- - -
-

Open the app data folder in Explorer to inspect or back up files.

- -
-
- - + + + + + -
-
-

Pause all notifications

-

Suppress all indexing notifications until re-enabled.

-
- -
-
+ + + - -
-
-
-

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."} -

- -
-
-
- - + + } > -
-
-
-

Orphaned files

-

- {cleaningThumbnails - ? "—" - : thumbnailCleanupResult + + + + + 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."} -

- -
-
-
-
+ + + } + > + + +
)}