Compare commits
5 Commits
a40e5c2771
...
8eaa0bd8e8
| Author | SHA1 | Date | |
|---|---|---|---|
| 8eaa0bd8e8 | |||
| 3707a35cc4 | |||
| d1eb75a4f5 | |||
| fbdd43d9d9 | |||
| 33fb3c6c77 |
@@ -1614,3 +1614,233 @@ pub async fn remove_tag(db: State<'_, DbState>, params: RemoveTagParams) -> Resu
|
|||||||
let conn = db.get().map_err(|e| e.to_string())?;
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
db::remove_tag(&conn, params.tag_id).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<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_tagging_queue_scope(app: AppHandle) -> Result<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<Vec<i64>, 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::<i64>().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<String> = 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Database maintenance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DatabaseInfo {
|
||||||
|
pub size_mb: f64,
|
||||||
|
pub reclaimable_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct VacuumResult {
|
||||||
|
pub before_mb: f64,
|
||||||
|
pub after_mb: f64,
|
||||||
|
pub freed_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_database_info(app: AppHandle, db: State<'_, DbState>) -> Result<DatabaseInfo, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("gallery.db");
|
||||||
|
let size_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
let page_size: i64 = conn.query_row("PRAGMA page_size", [], |r| r.get(0)).map_err(|e| e.to_string())?;
|
||||||
|
let freelist_count: i64 = conn.query_row("PRAGMA freelist_count", [], |r| r.get(0)).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(DatabaseInfo {
|
||||||
|
size_mb: size_bytes as f64 / 1_048_576.0,
|
||||||
|
reclaimable_mb: (freelist_count * page_size) as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn vacuum_database(app: AppHandle, db: State<'_, DbState>) -> Result<VacuumResult, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("gallery.db");
|
||||||
|
let before_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
conn.execute_batch("PRAGMA wal_checkpoint(FULL); VACUUM;").map_err(|e| e.to_string())?;
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let after_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
Ok(VacuumResult {
|
||||||
|
before_mb: before_bytes as f64 / 1_048_576.0,
|
||||||
|
after_mb: after_bytes as f64 / 1_048_576.0,
|
||||||
|
freed_mb: before_bytes.saturating_sub(after_bytes) as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct OrphanedThumbnailsInfo {
|
||||||
|
pub count: u64,
|
||||||
|
pub size_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct CleanupOrphanedThumbnailsResult {
|
||||||
|
pub deleted_count: u64,
|
||||||
|
pub freed_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_db_thumbnail_filenames(conn: &rusqlite::Connection) -> Result<std::collections::HashSet<String>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT thumbnail_path FROM images WHERE thumbnail_path IS NOT NULL")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let set = stmt
|
||||||
|
.query_map([], |row| row.get::<_, String>(0))
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.filter_map(|p| {
|
||||||
|
std::path::Path::new(&p)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_orphaned_thumbnails_info(
|
||||||
|
app: AppHandle,
|
||||||
|
db: State<'_, DbState>,
|
||||||
|
) -> Result<OrphanedThumbnailsInfo, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let thumb_dir = app_dir.join("thumbnails");
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
let db_filenames = collect_db_thumbnail_filenames(&conn)?;
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let mut count = 0u64;
|
||||||
|
let mut size_bytes = 0u64;
|
||||||
|
|
||||||
|
if thumb_dir.exists() {
|
||||||
|
for entry in std::fs::read_dir(&thumb_dir).map_err(|e| e.to_string())? {
|
||||||
|
let entry = entry.map_err(|e| e.to_string())?;
|
||||||
|
let fname = entry.file_name();
|
||||||
|
if !db_filenames.contains(fname.to_string_lossy().as_ref()) {
|
||||||
|
size_bytes += entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OrphanedThumbnailsInfo {
|
||||||
|
count,
|
||||||
|
size_mb: size_bytes as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cleanup_orphaned_thumbnails(
|
||||||
|
app: AppHandle,
|
||||||
|
db: State<'_, DbState>,
|
||||||
|
) -> Result<CleanupOrphanedThumbnailsResult, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let thumb_dir = app_dir.join("thumbnails");
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
let db_filenames = collect_db_thumbnail_filenames(&conn)?;
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let mut deleted_count = 0u64;
|
||||||
|
let mut freed_bytes = 0u64;
|
||||||
|
|
||||||
|
if thumb_dir.exists() {
|
||||||
|
for entry in std::fs::read_dir(&thumb_dir).map_err(|e| e.to_string())? {
|
||||||
|
let entry = entry.map_err(|e| e.to_string())?;
|
||||||
|
let fname = entry.file_name();
|
||||||
|
if !db_filenames.contains(fname.to_string_lossy().as_ref()) {
|
||||||
|
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||||
|
if std::fs::remove_file(entry.path()).is_ok() {
|
||||||
|
deleted_count += 1;
|
||||||
|
freed_bytes += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CleanupOrphanedThumbnailsResult {
|
||||||
|
deleted_count,
|
||||||
|
freed_mb: freed_bytes as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,15 @@ pub fn run() {
|
|||||||
commands::delete_images_from_disk,
|
commands::delete_images_from_disk,
|
||||||
commands::rename_folder,
|
commands::rename_folder,
|
||||||
commands::update_folder_path,
|
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,
|
||||||
|
commands::get_database_info,
|
||||||
|
commands::vacuum_database,
|
||||||
|
commands::get_orphaned_thumbnails_info,
|
||||||
|
commands::cleanup_orphaned_thumbnails,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { TaggerAcceleration, TaggingQueueScope, useGalleryStore } from "../store";
|
import { CleanupOrphanedThumbnailsResult, DatabaseInfo, OrphanedThumbnailsInfo, TaggerAcceleration, TaggingQueueScope, VacuumResult, useGalleryStore } from "../store";
|
||||||
|
|
||||||
type SettingsSection = "workspace" | "workers";
|
type SettingsSection = "workspace" | "general";
|
||||||
|
|
||||||
const SECTIONS: { id: SettingsSection; label: string; detail: string }[] = [
|
const SECTIONS: { id: SettingsSection; label: string; detail: string }[] = [
|
||||||
{ id: "workspace", label: "AI Workspace", detail: "Tagging models and queue targets" },
|
{ 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" }) {
|
function StatusPill({ children, tone }: { children: React.ReactNode; tone: "ready" | "muted" | "busy" }) {
|
||||||
@@ -109,19 +109,35 @@ export function SettingsModal() {
|
|||||||
const [taggerQueueing, setTaggerQueueing] = useState(false);
|
const [taggerQueueing, setTaggerQueueing] = useState(false);
|
||||||
const [taggerClearing, setTaggerClearing] = useState(false);
|
const [taggerClearing, setTaggerClearing] = useState(false);
|
||||||
const [taggerAccelerationSaving, setTaggerAccelerationSaving] = useState(false);
|
const [taggerAccelerationSaving, setTaggerAccelerationSaving] = useState(false);
|
||||||
|
const [taggerAccelerationError, setTaggerAccelerationError] = useState<string | null>(null);
|
||||||
const [taggerThresholdDraft, setTaggerThresholdDraft] = useState<string | null>(null);
|
const [taggerThresholdDraft, setTaggerThresholdDraft] = useState<string | null>(null);
|
||||||
const [taggerThresholdSaving, setTaggerThresholdSaving] = useState(false);
|
const [taggerThresholdSaving, setTaggerThresholdSaving] = useState(false);
|
||||||
|
const [taggerThresholdError, setTaggerThresholdError] = useState<string | null>(null);
|
||||||
const [taggerBatchSizeDraft, setTaggerBatchSizeDraft] = useState<string | null>(null);
|
const [taggerBatchSizeDraft, setTaggerBatchSizeDraft] = useState<string | null>(null);
|
||||||
const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false);
|
const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false);
|
||||||
|
const [taggerBatchSizeError, setTaggerBatchSizeError] = useState<string | null>(null);
|
||||||
|
const [openingDataFolder, setOpeningDataFolder] = useState(false);
|
||||||
|
const [dbInfo, setDbInfo] = useState<DatabaseInfo | null>(null);
|
||||||
|
const [vacuuming, setVacuuming] = useState(false);
|
||||||
|
const [vacuumResult, setVacuumResult] = useState<VacuumResult | null>(null);
|
||||||
|
const [thumbnailInfo, setThumbnailInfo] = useState<OrphanedThumbnailsInfo | null>(null);
|
||||||
|
const [cleaningThumbnails, setCleaningThumbnails] = useState(false);
|
||||||
|
const [thumbnailCleanupResult, setThumbnailCleanupResult] = useState<CleanupOrphanedThumbnailsResult | null>(null);
|
||||||
|
|
||||||
|
const thresholdErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const batchSizeErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const settingsOpen = useGalleryStore((state) => state.settingsOpen);
|
const settingsOpen = useGalleryStore((state) => state.settingsOpen);
|
||||||
const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen);
|
const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen);
|
||||||
const folders = useGalleryStore((state) => state.folders);
|
const folders = useGalleryStore((state) => state.folders);
|
||||||
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
||||||
const taggingQueueScope = useGalleryStore((state) => state.taggingQueueScope);
|
const taggingQueueScope = useGalleryStore((state) => state.taggingQueueScope);
|
||||||
const taggingQueueFolderIds = useGalleryStore((state) => state.taggingQueueFolderIds);
|
const taggingQueueFolderIds = useGalleryStore((state) => state.taggingQueueFolderIds);
|
||||||
|
const loadTaggingQueueScope = useGalleryStore((state) => state.loadTaggingQueueScope);
|
||||||
const setTaggingQueueScope = useGalleryStore((state) => state.setTaggingQueueScope);
|
const setTaggingQueueScope = useGalleryStore((state) => state.setTaggingQueueScope);
|
||||||
const toggleTaggingQueueFolder = useGalleryStore((state) => state.toggleTaggingQueueFolder);
|
const toggleTaggingQueueFolder = useGalleryStore((state) => state.toggleTaggingQueueFolder);
|
||||||
const setTaggingQueueFolderIds = useGalleryStore((state) => state.setTaggingQueueFolderIds);
|
const setTaggingQueueFolderIds = useGalleryStore((state) => state.setTaggingQueueFolderIds);
|
||||||
|
const loadTaggingQueueFolderIds = useGalleryStore((state) => state.loadTaggingQueueFolderIds);
|
||||||
const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus);
|
const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus);
|
||||||
const taggerModelPreparing = useGalleryStore((state) => state.taggerModelPreparing);
|
const taggerModelPreparing = useGalleryStore((state) => state.taggerModelPreparing);
|
||||||
const taggerModelProgress = useGalleryStore((state) => state.taggerModelProgress);
|
const taggerModelProgress = useGalleryStore((state) => state.taggerModelProgress);
|
||||||
@@ -145,6 +161,11 @@ export function SettingsModal() {
|
|||||||
const queueTaggingJobsForFolders = useGalleryStore((state) => state.queueTaggingJobsForFolders);
|
const queueTaggingJobsForFolders = useGalleryStore((state) => state.queueTaggingJobsForFolders);
|
||||||
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
|
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
|
||||||
const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders);
|
const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders);
|
||||||
|
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
|
||||||
|
const getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo);
|
||||||
|
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
|
||||||
|
const getOrphanedThumbnailsInfo = useGalleryStore((state) => state.getOrphanedThumbnailsInfo);
|
||||||
|
const cleanupOrphanedThumbnails = useGalleryStore((state) => state.cleanupOrphanedThumbnails);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!settingsOpen) return;
|
if (!settingsOpen) return;
|
||||||
@@ -152,24 +173,37 @@ export function SettingsModal() {
|
|||||||
void loadTaggerAcceleration();
|
void loadTaggerAcceleration();
|
||||||
void loadTaggerThreshold();
|
void loadTaggerThreshold();
|
||||||
void loadTaggerBatchSize();
|
void loadTaggerBatchSize();
|
||||||
|
void loadTaggingQueueScope();
|
||||||
|
void loadTaggingQueueFolderIds();
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape") setSettingsOpen(false);
|
if (event.key === "Escape") setSettingsOpen(false);
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, setSettingsOpen]);
|
}, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, loadTaggingQueueScope, loadTaggingQueueFolderIds, setSettingsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settingsOpen || activeSection !== "general") return;
|
||||||
|
setVacuumResult(null);
|
||||||
|
setThumbnailCleanupResult(null);
|
||||||
|
void getDatabaseInfo().then(setDbInfo).catch(() => {});
|
||||||
|
void getOrphanedThumbnailsInfo().then(setThumbnailInfo).catch(() => {});
|
||||||
|
}, [settingsOpen, activeSection, getDatabaseInfo, getOrphanedThumbnailsInfo]);
|
||||||
|
|
||||||
|
// Clean up error timers on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current);
|
||||||
|
if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedFolders = useMemo(
|
const selectedFolders = useMemo(
|
||||||
() => folders.filter((folder) => taggingQueueFolderIds.includes(folder.id)),
|
() => folders.filter((folder) => taggingQueueFolderIds.includes(folder.id)),
|
||||||
[folders, taggingQueueFolderIds],
|
[folders, taggingQueueFolderIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalQueuedJobs = useMemo(
|
|
||||||
() => Object.values(mediaJobProgress).reduce((sum, progress) => sum + (progress?.tagging_pending ?? 0), 0),
|
|
||||||
[mediaJobProgress],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!settingsOpen) return null;
|
if (!settingsOpen) return null;
|
||||||
|
|
||||||
const taggerReady = taggerModelStatus?.ready ?? false;
|
const taggerReady = taggerModelStatus?.ready ?? false;
|
||||||
@@ -268,8 +302,8 @@ export function SettingsModal() {
|
|||||||
<main className="flex min-w-0 flex-1 flex-col">
|
<main className="flex min-w-0 flex-1 flex-col">
|
||||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.07] px-6">
|
<div className="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.07] px-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-white">{activeSection === "workspace" ? "AI Workspace" : "Workers"}</p>
|
<p className="text-sm font-medium text-white">{activeSection === "workspace" ? "AI Workspace" : "General"}</p>
|
||||||
<p className="text-xs text-gray-600">{activeSection === "workspace" ? "Model setup and queue targets" : "Background processing status"}</p>
|
<p className="text-xs text-gray-600">{activeSection === "workspace" ? "Model setup and queue targets" : "App data and diagnostics"}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white"
|
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white"
|
||||||
@@ -344,8 +378,9 @@ export function SettingsModal() {
|
|||||||
current={taggerAcceleration}
|
current={taggerAcceleration}
|
||||||
onSelect={(nextAcceleration) => {
|
onSelect={(nextAcceleration) => {
|
||||||
setTaggerAccelerationSaving(true);
|
setTaggerAccelerationSaving(true);
|
||||||
|
setTaggerAccelerationError(null);
|
||||||
void setTaggerAcceleration(nextAcceleration)
|
void setTaggerAcceleration(nextAcceleration)
|
||||||
.catch((error) => setTaggerQueueStatus(String(error)))
|
.catch((error: unknown) => setTaggerAccelerationError(String(error)))
|
||||||
.finally(() => setTaggerAccelerationSaving(false));
|
.finally(() => setTaggerAccelerationSaving(false));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -353,7 +388,11 @@ export function SettingsModal() {
|
|||||||
</TaggerAccelerationButton>
|
</TaggerAccelerationButton>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-gray-600">{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}</p>
|
{taggerAccelerationError ? (
|
||||||
|
<p className="text-[11px] text-amber-300">{taggerAccelerationError}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-gray-600">{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
@@ -370,19 +409,27 @@ export function SettingsModal() {
|
|||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const value = parseFloat(thresholdDisplay);
|
const value = parseFloat(thresholdDisplay);
|
||||||
if (!isNaN(value) && value >= 0.05 && value <= 0.99) {
|
if (!isNaN(value) && value >= 0.05 && value <= 0.99) {
|
||||||
|
setTaggerThresholdError(null);
|
||||||
setTaggerThresholdSaving(true);
|
setTaggerThresholdSaving(true);
|
||||||
void setTaggerThreshold(value)
|
void setTaggerThreshold(value)
|
||||||
.catch((error) => setTaggerQueueStatus(String(error)))
|
.catch((error: unknown) => setTaggerThresholdError(String(error)))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setTaggerThresholdDraft(null);
|
setTaggerThresholdDraft(null);
|
||||||
setTaggerThresholdSaving(false);
|
setTaggerThresholdSaving(false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setTaggerThresholdDraft(null);
|
setTaggerThresholdDraft(null);
|
||||||
|
setTaggerThresholdError("Must be 0.05 – 0.99");
|
||||||
|
if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current);
|
||||||
|
thresholdErrorTimerRef.current = setTimeout(() => setTaggerThresholdError(null), 2000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-gray-600">{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}</p>
|
{taggerThresholdError ? (
|
||||||
|
<p className="text-[11px] text-amber-300">{taggerThresholdError}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-gray-600">{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
@@ -399,6 +446,7 @@ export function SettingsModal() {
|
|||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const value = parseInt(batchSizeDisplay, 10);
|
const value = parseInt(batchSizeDisplay, 10);
|
||||||
if (!isNaN(value) && value >= 1 && value <= 100) {
|
if (!isNaN(value) && value >= 1 && value <= 100) {
|
||||||
|
setTaggerBatchSizeError(null);
|
||||||
setTaggerBatchSizeSaving(true);
|
setTaggerBatchSizeSaving(true);
|
||||||
void setTaggerBatchSize(value)
|
void setTaggerBatchSize(value)
|
||||||
.catch((error: unknown) => setTaggerQueueStatus(String(error)))
|
.catch((error: unknown) => setTaggerQueueStatus(String(error)))
|
||||||
@@ -408,10 +456,17 @@ export function SettingsModal() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setTaggerBatchSizeDraft(null);
|
setTaggerBatchSizeDraft(null);
|
||||||
|
setTaggerBatchSizeError("Must be 1 – 100");
|
||||||
|
if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current);
|
||||||
|
batchSizeErrorTimerRef.current = setTimeout(() => setTaggerBatchSizeError(null), 2000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-gray-600">{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}</p>
|
{taggerBatchSizeError ? (
|
||||||
|
<p className="text-[11px] text-amber-300">{taggerBatchSizeError}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-gray-600">{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
@@ -453,16 +508,16 @@ export function SettingsModal() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40"
|
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
onClick={() => setTaggingQueueFolderIds(folders.map((folder) => folder.id))}
|
onClick={() => setTaggingQueueFolderIds(folders.map((folder) => folder.id))}
|
||||||
disabled={folders.length === 0}
|
disabled={taggingQueueScope === "all" || folders.length === 0}
|
||||||
>
|
>
|
||||||
Select all
|
Select all
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40"
|
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
onClick={() => setTaggingQueueFolderIds([])}
|
onClick={() => setTaggingQueueFolderIds([])}
|
||||||
disabled={taggingQueueFolderIds.length === 0}
|
disabled={taggingQueueScope === "all" || taggingQueueFolderIds.length === 0}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
@@ -477,12 +532,13 @@ export function SettingsModal() {
|
|||||||
<button
|
<button
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center justify-between rounded-xl border px-3 py-2 text-left transition-colors ${
|
className={`flex items-center justify-between rounded-xl border px-3 py-2 text-left transition-colors disabled:cursor-not-allowed ${
|
||||||
active
|
active
|
||||||
? "border-emerald-400/30 bg-emerald-500/10 text-white"
|
? "border-emerald-400/30 bg-emerald-500/10 text-white"
|
||||||
: "border-white/[0.07] bg-black/20 text-gray-300 hover:border-white/15 hover:bg-white/[0.04]"
|
: "border-white/[0.07] bg-black/20 text-gray-300 hover:border-white/15 hover:bg-white/[0.04]"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleTaggingQueueFolder(folder.id)}
|
onClick={() => toggleTaggingQueueFolder(folder.id)}
|
||||||
|
disabled={taggingQueueScope === "all"}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{folder.name}</p>
|
<p className="text-sm font-medium">{folder.name}</p>
|
||||||
@@ -520,41 +576,139 @@ export function SettingsModal() {
|
|||||||
|
|
||||||
{taggerQueueStatus ? <p className="pt-4 text-xs text-gray-500">{taggerQueueStatus}</p> : null}
|
{taggerQueueStatus ? <p className="pt-4 text-xs text-gray-500">{taggerQueueStatus}</p> : null}
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
<SettingsCard title="Captioning">
|
|
||||||
<div className="rounded-xl border border-dashed border-white/[0.09] bg-black/20 px-4 py-4">
|
|
||||||
<p className="text-sm font-medium text-white/50">Coming soon</p>
|
|
||||||
</div>
|
|
||||||
</SettingsCard>
|
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<SectionShell
|
<SectionShell
|
||||||
eyebrow="Workers"
|
eyebrow="General"
|
||||||
title="Background processing"
|
title="App data"
|
||||||
|
description="Access the folder where Phokus stores its database, thumbnails, AI models, and settings."
|
||||||
>
|
>
|
||||||
<SettingsCard title="Queue summary" description="Live totals across all folder workers.">
|
<SettingsCard title="Storage location">
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
<p className="text-sm text-gray-400">Open the app data folder in Explorer to inspect or back up files.</p>
|
||||||
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Tagging queued</p>
|
<button
|
||||||
<p className="mt-2 text-2xl font-semibold text-white">{totalQueuedJobs.toLocaleString()}</p>
|
className="shrink-0 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={() => {
|
||||||
|
setOpeningDataFolder(true);
|
||||||
|
void openAppDataFolder().finally(() => setOpeningDataFolder(false));
|
||||||
|
}}
|
||||||
|
disabled={openingDataFolder}
|
||||||
|
>
|
||||||
|
{openingDataFolder ? "Opening..." : "Open data folder"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
|
||||||
|
<SettingsCard
|
||||||
|
title="Compact database"
|
||||||
|
description="Reclaims wasted space left behind when images or tags are deleted. Safe to run at any time."
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Database size</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-white">
|
||||||
|
{vacuumResult
|
||||||
|
? `${vacuumResult.after_mb.toFixed(1)} MB`
|
||||||
|
: dbInfo
|
||||||
|
? `${dbInfo.size_mb.toFixed(1)} MB`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Reclaimable</p>
|
||||||
|
<p className={`mt-2 text-2xl font-semibold ${vacuumResult ? "text-emerald-300" : "text-white"}`}>
|
||||||
|
{vacuumResult
|
||||||
|
? `−${vacuumResult.freed_mb.toFixed(1)} MB freed`
|
||||||
|
: dbInfo
|
||||||
|
? `${dbInfo.reclaimable_mb.toFixed(1)} MB`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Selected folders</p>
|
<p className="text-sm text-gray-400">
|
||||||
<p className="mt-2 text-2xl font-semibold text-white">{selectedFolders.length.toLocaleString()}</p>
|
{vacuumResult
|
||||||
</div>
|
? `Compacted from ${vacuumResult.before_mb.toFixed(1)} MB to ${vacuumResult.after_mb.toFixed(1)} MB.`
|
||||||
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
: dbInfo && dbInfo.reclaimable_mb < 0.5
|
||||||
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Library folders</p>
|
? "Database is already compact."
|
||||||
<p className="mt-2 text-2xl font-semibold text-white">{folders.length.toLocaleString()}</p>
|
: "Run this after removing folders or bulk-deleting images."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="shrink-0 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={() => {
|
||||||
|
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"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
<SettingsCard title="Worker controls" description="Pause, resume, retry, and per-folder failure details are available in the background tasks panel.">
|
<SettingsCard
|
||||||
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
title="Thumbnail cache"
|
||||||
<p className="text-sm text-gray-400">Workers are running in the background.</p>
|
description="Thumbnails left behind when folders or images are removed. Safe to delete — they will be regenerated if the original files are re-indexed."
|
||||||
<StatusPill tone="ready">Live</StatusPill>
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Orphaned files</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-white">
|
||||||
|
{thumbnailCleanupResult
|
||||||
|
? thumbnailCleanupResult.deleted_count.toLocaleString()
|
||||||
|
: thumbnailInfo
|
||||||
|
? thumbnailInfo.count.toLocaleString()
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Reclaimable</p>
|
||||||
|
<p className={`mt-2 text-2xl font-semibold ${thumbnailCleanupResult ? "text-emerald-300" : "text-white"}`}>
|
||||||
|
{thumbnailCleanupResult
|
||||||
|
? `−${thumbnailCleanupResult.freed_mb.toFixed(1)} MB freed`
|
||||||
|
: thumbnailInfo
|
||||||
|
? `${thumbnailInfo.size_mb.toFixed(1)} MB`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{thumbnailCleanupResult
|
||||||
|
? `Removed ${thumbnailCleanupResult.deleted_count.toLocaleString()} orphaned thumbnail${thumbnailCleanupResult.deleted_count === 1 ? "" : "s"}.`
|
||||||
|
: thumbnailInfo && thumbnailInfo.count === 0
|
||||||
|
? "No orphaned thumbnails found."
|
||||||
|
: "Remove thumbnails no longer associated with any indexed image."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="shrink-0 rounded-lg bg-white/[0.07] px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-white/[0.11] disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
onClick={() => {
|
||||||
|
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"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|||||||
+64
-1
@@ -71,6 +71,27 @@ export interface ImageTag {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DatabaseInfo {
|
||||||
|
size_mb: number;
|
||||||
|
reclaimable_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VacuumResult {
|
||||||
|
before_mb: number;
|
||||||
|
after_mb: number;
|
||||||
|
freed_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrphanedThumbnailsInfo {
|
||||||
|
count: number;
|
||||||
|
size_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanupOrphanedThumbnailsResult {
|
||||||
|
deleted_count: number;
|
||||||
|
freed_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TaggerModelStatus {
|
export interface TaggerModelStatus {
|
||||||
model_id: string;
|
model_id: string;
|
||||||
model_name: string;
|
model_name: string;
|
||||||
@@ -334,9 +355,16 @@ interface GalleryState {
|
|||||||
setCaptionDetail: (detail: CaptionDetail) => Promise<void>;
|
setCaptionDetail: (detail: CaptionDetail) => Promise<void>;
|
||||||
setAiCaptionsEnabled: (enabled: boolean) => void;
|
setAiCaptionsEnabled: (enabled: boolean) => void;
|
||||||
setSettingsOpen: (open: boolean) => void;
|
setSettingsOpen: (open: boolean) => void;
|
||||||
|
loadTaggingQueueScope: () => Promise<void>;
|
||||||
setTaggingQueueScope: (scope: TaggingQueueScope) => void;
|
setTaggingQueueScope: (scope: TaggingQueueScope) => void;
|
||||||
|
loadTaggingQueueFolderIds: () => Promise<void>;
|
||||||
toggleTaggingQueueFolder: (folderId: number) => void;
|
toggleTaggingQueueFolder: (folderId: number) => void;
|
||||||
setTaggingQueueFolderIds: (folderIds: number[]) => void;
|
setTaggingQueueFolderIds: (folderIds: number[]) => void;
|
||||||
|
openAppDataFolder: () => Promise<void>;
|
||||||
|
getDatabaseInfo: () => Promise<DatabaseInfo>;
|
||||||
|
vacuumDatabase: () => Promise<VacuumResult>;
|
||||||
|
getOrphanedThumbnailsInfo: () => Promise<OrphanedThumbnailsInfo>;
|
||||||
|
cleanupOrphanedThumbnails: () => Promise<CleanupOrphanedThumbnailsResult>;
|
||||||
retryFailedEmbeddings: (folderId: number) => Promise<void>;
|
retryFailedEmbeddings: (folderId: number) => Promise<void>;
|
||||||
updateImageDetails: (imageId: number, updates: { favorite?: boolean; rating?: number }) => Promise<void>;
|
updateImageDetails: (imageId: number, updates: { favorite?: boolean; rating?: number }) => Promise<void>;
|
||||||
setCacheDir: (dir: string) => void;
|
setCacheDir: (dir: string) => void;
|
||||||
@@ -1299,6 +1327,15 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
|
|
||||||
setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
|
setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
|
||||||
|
|
||||||
|
loadTaggingQueueScope: async () => {
|
||||||
|
try {
|
||||||
|
const scope = await invoke<TaggingQueueScope>("get_tagging_queue_scope");
|
||||||
|
set({ taggingQueueScope: scope });
|
||||||
|
} catch {
|
||||||
|
// silently fall back to in-memory default
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setTaggingQueueScope: (taggingQueueScope) => {
|
setTaggingQueueScope: (taggingQueueScope) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
taggingQueueScope,
|
taggingQueueScope,
|
||||||
@@ -1307,6 +1344,16 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
? [state.folders[0].id]
|
? [state.folders[0].id]
|
||||||
: state.taggingQueueFolderIds,
|
: state.taggingQueueFolderIds,
|
||||||
}));
|
}));
|
||||||
|
void invoke("set_tagging_queue_scope", { scope: taggingQueueScope }).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTaggingQueueFolderIds: async () => {
|
||||||
|
try {
|
||||||
|
const folderIds = await invoke<number[]>("get_tagging_queue_folder_ids");
|
||||||
|
set({ taggingQueueFolderIds: folderIds });
|
||||||
|
} catch {
|
||||||
|
// silently fall back to in-memory default
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleTaggingQueueFolder: (folderId) => {
|
toggleTaggingQueueFolder: (folderId) => {
|
||||||
@@ -1314,11 +1361,27 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
const next = state.taggingQueueFolderIds.includes(folderId)
|
const next = state.taggingQueueFolderIds.includes(folderId)
|
||||||
? state.taggingQueueFolderIds.filter((id) => id !== folderId)
|
? state.taggingQueueFolderIds.filter((id) => id !== folderId)
|
||||||
: [...state.taggingQueueFolderIds, folderId].sort((a, b) => a - b);
|
: [...state.taggingQueueFolderIds, folderId].sort((a, b) => a - b);
|
||||||
|
void invoke("set_tagging_queue_folder_ids", { folder_ids: next }).catch(() => {});
|
||||||
return { taggingQueueFolderIds: next };
|
return { taggingQueueFolderIds: next };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setTaggingQueueFolderIds: (taggingQueueFolderIds) => set({ taggingQueueFolderIds }),
|
setTaggingQueueFolderIds: (taggingQueueFolderIds) => {
|
||||||
|
set({ taggingQueueFolderIds });
|
||||||
|
void invoke("set_tagging_queue_folder_ids", { folder_ids: taggingQueueFolderIds }).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
openAppDataFolder: async () => {
|
||||||
|
await invoke("open_app_data_folder");
|
||||||
|
},
|
||||||
|
|
||||||
|
getDatabaseInfo: () => invoke<DatabaseInfo>("get_database_info"),
|
||||||
|
|
||||||
|
vacuumDatabase: () => invoke<VacuumResult>("vacuum_database"),
|
||||||
|
|
||||||
|
getOrphanedThumbnailsInfo: () => invoke<OrphanedThumbnailsInfo>("get_orphaned_thumbnails_info"),
|
||||||
|
|
||||||
|
cleanupOrphanedThumbnails: () => invoke<CleanupOrphanedThumbnailsResult>("cleanup_orphaned_thumbnails"),
|
||||||
|
|
||||||
loadTaggerModelStatus: async () => {
|
loadTaggerModelStatus: async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user