diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 445f10c..7c0d70d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1947,6 +1947,7 @@ pub struct FolderWorkerStates { #[tauri::command] pub async fn set_worker_paused( + app: AppHandle, db: State<'_, DbState>, worker: String, folder_id: i64, @@ -1963,6 +1964,9 @@ pub async fn set_worker_paused( db::requeue_processing_tagging_jobs_for_folder(&conn, folder_id) .map_err(|e| e.to_string())?; } + if let Err(error) = persist_worker_pauses_if_enabled(&app) { + log::warn!("Failed to persist worker pause state: {error}"); + } Ok(()) } @@ -2482,6 +2486,8 @@ pub async fn bulk_remove_tag( const TAGGING_QUEUE_SCOPE_FILE: &str = "settings/tagging_queue_scope.txt"; const TAGGING_QUEUE_FOLDER_IDS_FILE: &str = "settings/tagging_queue_folder_ids.txt"; +const WORKER_PAUSES_PERSIST_FILE: &str = "settings/worker_pauses_persist.txt"; +const WORKER_PAUSES_FILE: &str = "settings/worker_pauses.json"; #[derive(Deserialize)] pub struct SetTaggingQueueScopeParams { @@ -2553,6 +2559,67 @@ pub async fn set_tagging_queue_folder_ids( Ok(()) } +fn worker_pause_persistence_enabled(app_dir: &Path) -> bool { + std::fs::read_to_string(app_dir.join(WORKER_PAUSES_PERSIST_FILE)) + .map(|value| value.trim() == "true") + .unwrap_or(false) +} + +fn write_worker_pause_snapshot(app_dir: &Path) -> Result<(), String> { + let path = app_dir.join(WORKER_PAUSES_FILE); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let json = serde_json::to_string_pretty(&indexer::snapshot_worker_paused_states()) + .map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) +} + +fn persist_worker_pauses_if_enabled(app: &AppHandle) -> Result<(), String> { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + if worker_pause_persistence_enabled(&app_dir) { + write_worker_pause_snapshot(&app_dir)?; + } + Ok(()) +} + +pub fn restore_persisted_worker_pauses(app_dir: &Path) { + if !worker_pause_persistence_enabled(app_dir) { + return; + } + + let path = app_dir.join(WORKER_PAUSES_FILE); + let Ok(content) = std::fs::read_to_string(path) else { + return; + }; + match serde_json::from_str::(&content) { + Ok(states) => indexer::replace_worker_paused_states(states), + Err(error) => log::warn!("Failed to restore persisted worker pauses: {error}"), + } +} + +#[tauri::command] +pub async fn get_worker_pauses_persist(app: AppHandle) -> Result { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + Ok(worker_pause_persistence_enabled(&app_dir)) +} + +#[tauri::command] +pub async fn set_worker_pauses_persist(app: AppHandle, persist: bool) -> Result<(), String> { + let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let path = app_dir.join(WORKER_PAUSES_PERSIST_FILE); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + std::fs::write(path, if persist { "true" } else { "false" }).map_err(|e| e.to_string())?; + if persist { + write_worker_pause_snapshot(&app_dir)?; + } else { + let _ = std::fs::remove_file(app_dir.join(WORKER_PAUSES_FILE)); + } + Ok(()) +} + // --------------------------------------------------------------------------- // App data folder // --------------------------------------------------------------------------- diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index 28dab25..8e3f1be 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -9,7 +9,7 @@ use crate::vector; use anyhow::Result; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use rayon::prelude::*; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; @@ -41,6 +41,14 @@ struct PausedWorkerFolders { tagging: HashSet, } +#[derive(Default, Deserialize, Serialize)] +pub struct PersistedPausedWorkerFolders { + pub thumbnail: Vec, + pub metadata: Vec, + pub embedding: Vec, + pub tagging: Vec, +} + #[derive(Clone, Copy)] pub struct FolderWorkerPausedState { pub thumbnail: bool, @@ -50,6 +58,41 @@ pub struct FolderWorkerPausedState { pub tagging: bool, } +pub fn replace_worker_paused_states(states: PersistedPausedWorkerFolders) { + if let Ok(mut paused_folders) = PAUSED_WORKER_FOLDERS + .get_or_init(|| Mutex::new(PausedWorkerFolders::default())) + .lock() + { + paused_folders.thumbnail = states.thumbnail.into_iter().collect(); + paused_folders.metadata = states.metadata.into_iter().collect(); + paused_folders.embedding = states.embedding.into_iter().collect(); + paused_folders.caption = HashSet::new(); + paused_folders.tagging = states.tagging.into_iter().collect(); + } +} + +pub fn snapshot_worker_paused_states() -> PersistedPausedWorkerFolders { + let Ok(paused_folders) = PAUSED_WORKER_FOLDERS + .get_or_init(|| Mutex::new(PausedWorkerFolders::default())) + .lock() + else { + return PersistedPausedWorkerFolders::default(); + }; + + let sorted = |set: &HashSet| { + let mut ids = set.iter().copied().collect::>(); + ids.sort_unstable(); + ids + }; + + PersistedPausedWorkerFolders { + thumbnail: sorted(&paused_folders.thumbnail), + metadata: sorted(&paused_folders.metadata), + embedding: sorted(&paused_folders.embedding), + tagging: sorted(&paused_folders.tagging), + } +} + pub fn set_worker_paused(worker: &str, folder_id: i64, paused: bool) { if let Ok(mut paused_folders) = PAUSED_WORKER_FOLDERS .get_or_init(|| Mutex::new(PausedWorkerFolders::default())) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c5d167b..6c51442 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -119,6 +119,7 @@ pub fn run() { let thumb_dir = app_dir.join("thumbnails"); std::fs::create_dir_all(&thumb_dir).expect("Failed to create thumbnail dir"); + commands::restore_persisted_worker_pauses(&app_dir); // The asset protocol scope is no longer a blanket "**": thumbnails // are allowed statically in tauri.conf.json, and each indexed @@ -195,6 +196,8 @@ pub fn run() { commands::suggest_image_tags, commands::set_worker_paused, commands::get_worker_states, + commands::get_worker_pauses_persist, + commands::set_worker_pauses_persist, commands::get_tag_cloud, commands::get_explore_tags, commands::get_related_tags, diff --git a/src/App.tsx b/src/App.tsx index 75de078..9a66c3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ export default function App() { const loadAlbums = useGalleryStore((state) => state.loadAlbums); const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds); const loadNotificationsPaused = useGalleryStore((state) => state.loadNotificationsPaused); + const loadWorkerPausesPersist = useGalleryStore((state) => state.loadWorkerPausesPersist); const subscribeToProgress = useGalleryStore((state) => state.subscribeToProgress); const loadAppVersion = useGalleryStore((state) => state.loadAppVersion); const checkForUpdates = useGalleryStore((state) => state.checkForUpdates); @@ -39,6 +40,7 @@ export default function App() { void initializeNotifications(); void loadMutedFolderIds(); void loadNotificationsPaused(); + void loadWorkerPausesPersist(); void loadFfmpegStatus(); void loadOnboardingCompleted(); // Load the app version first so the What's New toast/modal (which read diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index c36dc30..c9a2a41 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -228,6 +228,8 @@ export function SettingsModal() { const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder); const notificationsPaused = useGalleryStore((state) => state.notificationsPaused); const setNotificationsPaused = useGalleryStore((state) => state.setNotificationsPaused); + const workerPausesPersist = useGalleryStore((state) => state.workerPausesPersist); + const setWorkerPausesPersist = useGalleryStore((state) => state.setWorkerPausesPersist); const getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo); const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase); const rebuildSemanticIndex = useGalleryStore((state) => state.rebuildSemanticIndex); @@ -854,6 +856,19 @@ export function SettingsModal() { + + + diff --git a/src/dev/mockBackend.ts b/src/dev/mockBackend.ts index a4d8ddd..7bd8fd1 100644 --- a/src/dev/mockBackend.ts +++ b/src/dev/mockBackend.ts @@ -381,9 +381,11 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise case "set_tagging_queue_folder_ids": case "set_muted_folder_ids": case "set_notifications_paused": + case "set_worker_pauses_persist": case "set_worker_paused": return null; case "get_notifications_paused": + case "get_worker_pauses_persist": case "get_onboarding_completed": return db.scenario !== "empty"; case "set_onboarding_completed": diff --git a/src/store.ts b/src/store.ts index e1c9bb3..72ab5a7 100644 --- a/src/store.ts +++ b/src/store.ts @@ -400,6 +400,7 @@ interface GalleryState { taggingQueueFolderIds: number[]; mutedFolderIds: number[]; notificationsPaused: boolean; + workerPausesPersist: boolean; theme: AppTheme; lightboxAutoplay: boolean; lightboxAutoMute: boolean; @@ -524,6 +525,8 @@ interface GalleryState { toggleMutedFolder: (folderId: number) => void; loadNotificationsPaused: () => Promise; setNotificationsPaused: (paused: boolean) => void; + loadWorkerPausesPersist: () => Promise; + setWorkerPausesPersist: (persist: boolean) => void; setTheme: (theme: AppTheme) => void; setLightboxAutoplay: (enabled: boolean) => void; setLightboxAutoMute: (enabled: boolean) => void; @@ -889,6 +892,7 @@ export const useGalleryStore = create((set, get) => ({ taggingQueueFolderIds: [], mutedFolderIds: [], notificationsPaused: false, + workerPausesPersist: false, theme: initialTheme(), lightboxAutoplay: initialBoolSetting(LIGHTBOX_AUTOPLAY_KEY, true), lightboxAutoMute: initialBoolSetting(LIGHTBOX_AUTO_MUTE_KEY, false), @@ -1921,6 +1925,20 @@ export const useGalleryStore = create((set, get) => ({ return entries; }, + loadWorkerPausesPersist: async () => { + try { + const persist = await invoke("get_worker_pauses_persist"); + set({ workerPausesPersist: persist }); + } catch { + // fall back to in-memory default + } + }, + + setWorkerPausesPersist: (persist) => { + set({ workerPausesPersist: persist }); + void invoke("set_worker_pauses_persist", { persist }).catch(() => {}); + }, + setTheme: (theme) => { window.localStorage.setItem(THEME_KEY, theme); document.documentElement.dataset.theme = theme;