From 33fb3c6c7736db8bf13bbea29b8b18624c5bcaaa Mon Sep 17 00:00:00 2001 From: LyAhn Date: Tue, 9 Jun 2026 00:14:49 +0100 Subject: [PATCH] feat(settings): overhaul Settings modal with improvements and General section - Remove Workers section (read-only status, belongs in background tasks panel) - Remove Captioning coming-soon placeholder - Add General section with Open data folder button (tauri-plugin-opener) - Persist queue scope and folder selection across sessions via settings files - Show inline validation errors on threshold/batch size inputs instead of silent revert - Fix acceleration save errors appearing in wrong card - Disable folder selection controls when target scope is All media --- src-tauri/src/commands.rs | 82 +++++++++++++++++++++ src-tauri/src/lib.rs | 5 ++ src/components/SettingsModal.tsx | 123 +++++++++++++++++++------------ src/store.ts | 32 +++++++- 4 files changed, 192 insertions(+), 50 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1eabb3b..81bca58 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1614,3 +1614,85 @@ pub async fn remove_tag(db: State<'_, DbState>, params: RemoveTagParams) -> Resu let conn = db.get().map_err(|e| e.to_string())?; db::remove_tag(&conn, params.tag_id).map_err(|e| e.to_string()) } + +// --------------------------------------------------------------------------- +// Queue scope / folder-id persistence +// --------------------------------------------------------------------------- + +const TAGGING_QUEUE_SCOPE_FILE: &str = "settings/tagging_queue_scope.txt"; +const TAGGING_QUEUE_FOLDER_IDS_FILE: &str = "settings/tagging_queue_folder_ids.txt"; + +#[derive(Deserialize)] +pub struct SetTaggingQueueScopeParams { + pub scope: String, +} + +#[derive(Deserialize)] +pub struct SetTaggingQueueFolderIdsParams { + pub folder_ids: Vec, +} + +#[tauri::command] +pub async fn get_tagging_queue_scope(app: AppHandle) -> Result { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let path = app_dir.join(TAGGING_QUEUE_SCOPE_FILE); + let value = std::fs::read_to_string(path).unwrap_or_default(); + Ok(if value.trim() == "selected" { "selected".to_string() } else { "all".to_string() }) +} + +#[tauri::command] +pub async fn set_tagging_queue_scope( + app: AppHandle, + params: SetTaggingQueueScopeParams, +) -> Result { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let path = app_dir.join(TAGGING_QUEUE_SCOPE_FILE); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let value = if params.scope == "selected" { "selected" } else { "all" }; + std::fs::write(path, value).map_err(|e| e.to_string())?; + Ok(value.to_string()) +} + +#[tauri::command] +pub async fn get_tagging_queue_folder_ids(app: AppHandle) -> Result, String> { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let path = app_dir.join(TAGGING_QUEUE_FOLDER_IDS_FILE); + let Ok(content) = std::fs::read_to_string(path) else { + return Ok(vec![]); + }; + let ids = content + .split(',') + .filter_map(|s| s.trim().parse::().ok()) + .collect(); + Ok(ids) +} + +#[tauri::command] +pub async fn set_tagging_queue_folder_ids( + app: AppHandle, + params: SetTaggingQueueFolderIdsParams, +) -> Result<(), String> { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let path = app_dir.join(TAGGING_QUEUE_FOLDER_IDS_FILE); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let content: Vec = params.folder_ids.iter().map(|id| id.to_string()).collect(); + std::fs::write(path, content.join(",")).map_err(|e| e.to_string())?; + Ok(()) +} + +// --------------------------------------------------------------------------- +// App data folder +// --------------------------------------------------------------------------- + +#[tauri::command] +pub async fn open_app_data_folder(app: AppHandle) -> Result<(), String> { + use tauri_plugin_opener::OpenerExt; + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + app.opener() + .open_path(app_dir.to_string_lossy().as_ref(), None::<&str>) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bb8a736..38fdd8f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -139,6 +139,11 @@ pub fn run() { commands::delete_images_from_disk, commands::rename_folder, commands::update_folder_path, + commands::get_tagging_queue_scope, + commands::set_tagging_queue_scope, + commands::get_tagging_queue_folder_ids, + commands::set_tagging_queue_folder_ids, + commands::open_app_data_folder, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index d328ac6..2ee06f6 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,11 +1,11 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { TaggerAcceleration, TaggingQueueScope, useGalleryStore } from "../store"; -type SettingsSection = "workspace" | "workers"; +type SettingsSection = "workspace" | "general"; const SECTIONS: { id: SettingsSection; label: string; detail: string }[] = [ { id: "workspace", label: "AI Workspace", detail: "Tagging models and queue targets" }, - { id: "workers", label: "Workers", detail: "Queue activity and background processing" }, + { id: "general", label: "General", detail: "App data and diagnostics" }, ]; function StatusPill({ children, tone }: { children: React.ReactNode; tone: "ready" | "muted" | "busy" }) { @@ -109,19 +109,29 @@ export function SettingsModal() { const [taggerQueueing, setTaggerQueueing] = useState(false); const [taggerClearing, setTaggerClearing] = useState(false); const [taggerAccelerationSaving, setTaggerAccelerationSaving] = useState(false); + const [taggerAccelerationError, setTaggerAccelerationError] = useState(null); const [taggerThresholdDraft, setTaggerThresholdDraft] = useState(null); const [taggerThresholdSaving, setTaggerThresholdSaving] = useState(false); + const [taggerThresholdError, setTaggerThresholdError] = useState(null); const [taggerBatchSizeDraft, setTaggerBatchSizeDraft] = useState(null); const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false); + const [taggerBatchSizeError, setTaggerBatchSizeError] = useState(null); + const [openingDataFolder, setOpeningDataFolder] = useState(false); + + const thresholdErrorTimerRef = useRef | null>(null); + const batchSizeErrorTimerRef = useRef | null>(null); + const settingsOpen = useGalleryStore((state) => state.settingsOpen); const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen); const folders = useGalleryStore((state) => state.folders); const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress); const taggingQueueScope = useGalleryStore((state) => state.taggingQueueScope); const taggingQueueFolderIds = useGalleryStore((state) => state.taggingQueueFolderIds); + const loadTaggingQueueScope = useGalleryStore((state) => state.loadTaggingQueueScope); const setTaggingQueueScope = useGalleryStore((state) => state.setTaggingQueueScope); const toggleTaggingQueueFolder = useGalleryStore((state) => state.toggleTaggingQueueFolder); const setTaggingQueueFolderIds = useGalleryStore((state) => state.setTaggingQueueFolderIds); + const loadTaggingQueueFolderIds = useGalleryStore((state) => state.loadTaggingQueueFolderIds); const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus); const taggerModelPreparing = useGalleryStore((state) => state.taggerModelPreparing); const taggerModelProgress = useGalleryStore((state) => state.taggerModelProgress); @@ -145,6 +155,7 @@ export function SettingsModal() { const queueTaggingJobsForFolders = useGalleryStore((state) => state.queueTaggingJobsForFolders); const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs); const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders); + const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder); useEffect(() => { if (!settingsOpen) return; @@ -152,24 +163,29 @@ export function SettingsModal() { void loadTaggerAcceleration(); void loadTaggerThreshold(); void loadTaggerBatchSize(); + void loadTaggingQueueScope(); + void loadTaggingQueueFolderIds(); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setSettingsOpen(false); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, setSettingsOpen]); + }, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, loadTaggingQueueScope, loadTaggingQueueFolderIds, setSettingsOpen]); + + // Clean up error timers on unmount + useEffect(() => { + return () => { + if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current); + if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current); + }; + }, []); const selectedFolders = useMemo( () => folders.filter((folder) => taggingQueueFolderIds.includes(folder.id)), [folders, taggingQueueFolderIds], ); - const totalQueuedJobs = useMemo( - () => Object.values(mediaJobProgress).reduce((sum, progress) => sum + (progress?.tagging_pending ?? 0), 0), - [mediaJobProgress], - ); - if (!settingsOpen) return null; const taggerReady = taggerModelStatus?.ready ?? false; @@ -268,8 +284,8 @@ export function SettingsModal() {
-

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

-

{activeSection === "workspace" ? "Model setup and queue targets" : "Background processing status"}

+

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

+

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

-

{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}

+ {taggerAccelerationError ? ( +

{taggerAccelerationError}

+ ) : ( +

{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}

+ )} @@ -370,19 +391,27 @@ export function SettingsModal() { onBlur={() => { const value = parseFloat(thresholdDisplay); if (!isNaN(value) && value >= 0.05 && value <= 0.99) { + setTaggerThresholdError(null); setTaggerThresholdSaving(true); void setTaggerThreshold(value) - .catch((error) => setTaggerQueueStatus(String(error))) + .catch((error: unknown) => setTaggerQueueStatus(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); } }} /> -

{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}

+ {taggerThresholdError ? ( +

{taggerThresholdError}

+ ) : ( +

{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}

+ )} @@ -399,6 +428,7 @@ export function SettingsModal() { 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))) @@ -408,10 +438,17 @@ export function SettingsModal() { }); } else { setTaggerBatchSizeDraft(null); + setTaggerBatchSizeError("Must be 1 – 100"); + if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current); + batchSizeErrorTimerRef.current = setTimeout(() => setTaggerBatchSizeError(null), 2000); } }} /> -

{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}

+ {taggerBatchSizeError ? ( +

{taggerBatchSizeError}

+ ) : ( +

{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}

+ )} @@ -453,16 +490,16 @@ export function SettingsModal() {
@@ -477,12 +514,13 @@ export function SettingsModal() {
diff --git a/src/store.ts b/src/store.ts index c82494d..85cf5dd 100644 --- a/src/store.ts +++ b/src/store.ts @@ -334,9 +334,12 @@ interface GalleryState { setCaptionDetail: (detail: CaptionDetail) => Promise; setAiCaptionsEnabled: (enabled: boolean) => void; setSettingsOpen: (open: boolean) => void; + loadTaggingQueueScope: () => Promise; setTaggingQueueScope: (scope: TaggingQueueScope) => void; + loadTaggingQueueFolderIds: () => Promise; toggleTaggingQueueFolder: (folderId: number) => void; setTaggingQueueFolderIds: (folderIds: number[]) => void; + openAppDataFolder: () => Promise; retryFailedEmbeddings: (folderId: number) => Promise; updateImageDetails: (imageId: number, updates: { favorite?: boolean; rating?: number }) => Promise; setCacheDir: (dir: string) => void; @@ -1299,6 +1302,15 @@ export const useGalleryStore = create((set, get) => ({ setSettingsOpen: (settingsOpen) => set({ settingsOpen }), + loadTaggingQueueScope: async () => { + try { + const scope = await invoke("get_tagging_queue_scope"); + set({ taggingQueueScope: scope }); + } catch { + // silently fall back to in-memory default + } + }, + setTaggingQueueScope: (taggingQueueScope) => { set((state) => ({ taggingQueueScope, @@ -1307,6 +1319,16 @@ export const useGalleryStore = create((set, get) => ({ ? [state.folders[0].id] : state.taggingQueueFolderIds, })); + void invoke("set_tagging_queue_scope", { scope: taggingQueueScope }).catch(() => {}); + }, + + loadTaggingQueueFolderIds: async () => { + try { + const folderIds = await invoke("get_tagging_queue_folder_ids"); + set({ taggingQueueFolderIds: folderIds }); + } catch { + // silently fall back to in-memory default + } }, toggleTaggingQueueFolder: (folderId) => { @@ -1314,11 +1336,19 @@ export const useGalleryStore = create((set, get) => ({ const next = state.taggingQueueFolderIds.includes(folderId) ? state.taggingQueueFolderIds.filter((id) => id !== folderId) : [...state.taggingQueueFolderIds, folderId].sort((a, b) => a - b); + void invoke("set_tagging_queue_folder_ids", { folderIds: next }).catch(() => {}); return { taggingQueueFolderIds: next }; }); }, - setTaggingQueueFolderIds: (taggingQueueFolderIds) => set({ taggingQueueFolderIds }), + setTaggingQueueFolderIds: (taggingQueueFolderIds) => { + set({ taggingQueueFolderIds }); + void invoke("set_tagging_queue_folder_ids", { folderIds: taggingQueueFolderIds }).catch(() => {}); + }, + + openAppDataFolder: async () => { + await invoke("open_app_data_folder"); + }, loadTaggerModelStatus: async () => { try {