feat: persist worker-pause state across restarts
Worker pause states can optionally survive app restarts. A new toggle in Settings saves the current pause map to settings/worker_pauses.json; on startup lib.rs restores it before workers are spawned. Backend: new snapshot/replace helpers in indexer.rs, persist functions in commands.rs (get/set_worker_pauses_persist). Frontend: workerPausesPersist store field, load/setWorkerPausesPersist actions, toggle in SettingsModal.
This commit is contained in:
@@ -1947,6 +1947,7 @@ pub struct FolderWorkerStates {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_worker_paused(
|
pub async fn set_worker_paused(
|
||||||
|
app: AppHandle,
|
||||||
db: State<'_, DbState>,
|
db: State<'_, DbState>,
|
||||||
worker: String,
|
worker: String,
|
||||||
folder_id: i64,
|
folder_id: i64,
|
||||||
@@ -1963,6 +1964,9 @@ pub async fn set_worker_paused(
|
|||||||
db::requeue_processing_tagging_jobs_for_folder(&conn, folder_id)
|
db::requeue_processing_tagging_jobs_for_folder(&conn, folder_id)
|
||||||
.map_err(|e| e.to_string())?;
|
.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(())
|
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_SCOPE_FILE: &str = "settings/tagging_queue_scope.txt";
|
||||||
const TAGGING_QUEUE_FOLDER_IDS_FILE: &str = "settings/tagging_queue_folder_ids.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)]
|
#[derive(Deserialize)]
|
||||||
pub struct SetTaggingQueueScopeParams {
|
pub struct SetTaggingQueueScopeParams {
|
||||||
@@ -2553,6 +2559,67 @@ pub async fn set_tagging_queue_folder_ids(
|
|||||||
Ok(())
|
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::<indexer::PersistedPausedWorkerFolders>(&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<bool, String> {
|
||||||
|
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
|
// App data folder
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::vector;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
@@ -41,6 +41,14 @@ struct PausedWorkerFolders {
|
|||||||
tagging: HashSet<i64>,
|
tagging: HashSet<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
pub struct PersistedPausedWorkerFolders {
|
||||||
|
pub thumbnail: Vec<i64>,
|
||||||
|
pub metadata: Vec<i64>,
|
||||||
|
pub embedding: Vec<i64>,
|
||||||
|
pub tagging: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct FolderWorkerPausedState {
|
pub struct FolderWorkerPausedState {
|
||||||
pub thumbnail: bool,
|
pub thumbnail: bool,
|
||||||
@@ -50,6 +58,41 @@ pub struct FolderWorkerPausedState {
|
|||||||
pub tagging: bool,
|
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<i64>| {
|
||||||
|
let mut ids = set.iter().copied().collect::<Vec<_>>();
|
||||||
|
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) {
|
pub fn set_worker_paused(worker: &str, folder_id: i64, paused: bool) {
|
||||||
if let Ok(mut paused_folders) = PAUSED_WORKER_FOLDERS
|
if let Ok(mut paused_folders) = PAUSED_WORKER_FOLDERS
|
||||||
.get_or_init(|| Mutex::new(PausedWorkerFolders::default()))
|
.get_or_init(|| Mutex::new(PausedWorkerFolders::default()))
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ pub fn run() {
|
|||||||
|
|
||||||
let thumb_dir = app_dir.join("thumbnails");
|
let thumb_dir = app_dir.join("thumbnails");
|
||||||
std::fs::create_dir_all(&thumb_dir).expect("Failed to create thumbnail dir");
|
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
|
// The asset protocol scope is no longer a blanket "**": thumbnails
|
||||||
// are allowed statically in tauri.conf.json, and each indexed
|
// are allowed statically in tauri.conf.json, and each indexed
|
||||||
@@ -195,6 +196,8 @@ pub fn run() {
|
|||||||
commands::suggest_image_tags,
|
commands::suggest_image_tags,
|
||||||
commands::set_worker_paused,
|
commands::set_worker_paused,
|
||||||
commands::get_worker_states,
|
commands::get_worker_states,
|
||||||
|
commands::get_worker_pauses_persist,
|
||||||
|
commands::set_worker_pauses_persist,
|
||||||
commands::get_tag_cloud,
|
commands::get_tag_cloud,
|
||||||
commands::get_explore_tags,
|
commands::get_explore_tags,
|
||||||
commands::get_related_tags,
|
commands::get_related_tags,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function App() {
|
|||||||
const loadAlbums = useGalleryStore((state) => state.loadAlbums);
|
const loadAlbums = useGalleryStore((state) => state.loadAlbums);
|
||||||
const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds);
|
const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds);
|
||||||
const loadNotificationsPaused = useGalleryStore((state) => state.loadNotificationsPaused);
|
const loadNotificationsPaused = useGalleryStore((state) => state.loadNotificationsPaused);
|
||||||
|
const loadWorkerPausesPersist = useGalleryStore((state) => state.loadWorkerPausesPersist);
|
||||||
const subscribeToProgress = useGalleryStore((state) => state.subscribeToProgress);
|
const subscribeToProgress = useGalleryStore((state) => state.subscribeToProgress);
|
||||||
const loadAppVersion = useGalleryStore((state) => state.loadAppVersion);
|
const loadAppVersion = useGalleryStore((state) => state.loadAppVersion);
|
||||||
const checkForUpdates = useGalleryStore((state) => state.checkForUpdates);
|
const checkForUpdates = useGalleryStore((state) => state.checkForUpdates);
|
||||||
@@ -39,6 +40,7 @@ export default function App() {
|
|||||||
void initializeNotifications();
|
void initializeNotifications();
|
||||||
void loadMutedFolderIds();
|
void loadMutedFolderIds();
|
||||||
void loadNotificationsPaused();
|
void loadNotificationsPaused();
|
||||||
|
void loadWorkerPausesPersist();
|
||||||
void loadFfmpegStatus();
|
void loadFfmpegStatus();
|
||||||
void loadOnboardingCompleted();
|
void loadOnboardingCompleted();
|
||||||
// Load the app version first so the What's New toast/modal (which read
|
// Load the app version first so the What's New toast/modal (which read
|
||||||
|
|||||||
@@ -228,6 +228,8 @@ export function SettingsModal() {
|
|||||||
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
|
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
|
||||||
const notificationsPaused = useGalleryStore((state) => state.notificationsPaused);
|
const notificationsPaused = useGalleryStore((state) => state.notificationsPaused);
|
||||||
const setNotificationsPaused = useGalleryStore((state) => state.setNotificationsPaused);
|
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 getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo);
|
||||||
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
|
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
|
||||||
const rebuildSemanticIndex = useGalleryStore((state) => state.rebuildSemanticIndex);
|
const rebuildSemanticIndex = useGalleryStore((state) => state.rebuildSemanticIndex);
|
||||||
@@ -854,6 +856,19 @@ export function SettingsModal() {
|
|||||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${notificationsPaused ? "translate-x-4" : "translate-x-0"}`} />
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${notificationsPaused ? "translate-x-4" : "translate-x-0"}`} />
|
||||||
</button>
|
</button>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem
|
||||||
|
label="Keep background pauses after restart"
|
||||||
|
description="When enabled, folders you pause from the sidebar or background bar stay paused the next time Phokus opens."
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={workerPausesPersist}
|
||||||
|
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${workerPausesPersist ? "bg-sky-500" : "bg-white/15"}`}
|
||||||
|
onClick={() => setWorkerPausesPersist(!workerPausesPersist)}
|
||||||
|
>
|
||||||
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${workerPausesPersist ? "translate-x-4" : "translate-x-0"}`} />
|
||||||
|
</button>
|
||||||
|
</SettingsItem>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
||||||
<SettingsGroup title="Maintenance">
|
<SettingsGroup title="Maintenance">
|
||||||
|
|||||||
@@ -381,9 +381,11 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise
|
|||||||
case "set_tagging_queue_folder_ids":
|
case "set_tagging_queue_folder_ids":
|
||||||
case "set_muted_folder_ids":
|
case "set_muted_folder_ids":
|
||||||
case "set_notifications_paused":
|
case "set_notifications_paused":
|
||||||
|
case "set_worker_pauses_persist":
|
||||||
case "set_worker_paused":
|
case "set_worker_paused":
|
||||||
return null;
|
return null;
|
||||||
case "get_notifications_paused":
|
case "get_notifications_paused":
|
||||||
|
case "get_worker_pauses_persist":
|
||||||
case "get_onboarding_completed":
|
case "get_onboarding_completed":
|
||||||
return db.scenario !== "empty";
|
return db.scenario !== "empty";
|
||||||
case "set_onboarding_completed":
|
case "set_onboarding_completed":
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ interface GalleryState {
|
|||||||
taggingQueueFolderIds: number[];
|
taggingQueueFolderIds: number[];
|
||||||
mutedFolderIds: number[];
|
mutedFolderIds: number[];
|
||||||
notificationsPaused: boolean;
|
notificationsPaused: boolean;
|
||||||
|
workerPausesPersist: boolean;
|
||||||
theme: AppTheme;
|
theme: AppTheme;
|
||||||
lightboxAutoplay: boolean;
|
lightboxAutoplay: boolean;
|
||||||
lightboxAutoMute: boolean;
|
lightboxAutoMute: boolean;
|
||||||
@@ -524,6 +525,8 @@ interface GalleryState {
|
|||||||
toggleMutedFolder: (folderId: number) => void;
|
toggleMutedFolder: (folderId: number) => void;
|
||||||
loadNotificationsPaused: () => Promise<void>;
|
loadNotificationsPaused: () => Promise<void>;
|
||||||
setNotificationsPaused: (paused: boolean) => void;
|
setNotificationsPaused: (paused: boolean) => void;
|
||||||
|
loadWorkerPausesPersist: () => Promise<void>;
|
||||||
|
setWorkerPausesPersist: (persist: boolean) => void;
|
||||||
setTheme: (theme: AppTheme) => void;
|
setTheme: (theme: AppTheme) => void;
|
||||||
setLightboxAutoplay: (enabled: boolean) => void;
|
setLightboxAutoplay: (enabled: boolean) => void;
|
||||||
setLightboxAutoMute: (enabled: boolean) => void;
|
setLightboxAutoMute: (enabled: boolean) => void;
|
||||||
@@ -889,6 +892,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
taggingQueueFolderIds: [],
|
taggingQueueFolderIds: [],
|
||||||
mutedFolderIds: [],
|
mutedFolderIds: [],
|
||||||
notificationsPaused: false,
|
notificationsPaused: false,
|
||||||
|
workerPausesPersist: false,
|
||||||
theme: initialTheme(),
|
theme: initialTheme(),
|
||||||
lightboxAutoplay: initialBoolSetting(LIGHTBOX_AUTOPLAY_KEY, true),
|
lightboxAutoplay: initialBoolSetting(LIGHTBOX_AUTOPLAY_KEY, true),
|
||||||
lightboxAutoMute: initialBoolSetting(LIGHTBOX_AUTO_MUTE_KEY, false),
|
lightboxAutoMute: initialBoolSetting(LIGHTBOX_AUTO_MUTE_KEY, false),
|
||||||
@@ -1921,6 +1925,20 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
return entries;
|
return entries;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadWorkerPausesPersist: async () => {
|
||||||
|
try {
|
||||||
|
const persist = await invoke<boolean>("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) => {
|
setTheme: (theme) => {
|
||||||
window.localStorage.setItem(THEME_KEY, theme);
|
window.localStorage.setItem(THEME_KEY, theme);
|
||||||
document.documentElement.dataset.theme = theme;
|
document.documentElement.dataset.theme = theme;
|
||||||
|
|||||||
Reference in New Issue
Block a user