portable storage, bug fixes, tooltips
all app data now lives next to the exe (presets, undo, settings, window state, webview cache). dropped the directories crate. auto-detects and fixes stale Explorer context menu entries when the exe is moved. fixed regex case_insensitive field mismatch, update check type mismatch, drive detection condition, rename panic on root paths. added tooltips to zoom and browse buttons. escape key now deselects files instead of wiping the pipeline
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -652,15 +652,6 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "directories"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -2220,7 +2211,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"directories",
|
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"nomina-core",
|
"nomina-core",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
directories = "6"
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
winreg = "0.55"
|
winreg = "0.55"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -93,6 +93,42 @@ pub fn unregister_context_menu() -> Result<(), String> {
|
|||||||
Err("Context menu registration is only supported on Windows".to_string())
|
Err("Context menu registration is only supported on Windows".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if Explorer context menu entries exist and whether they point to the current exe.
|
||||||
|
/// Returns: "ok" if registered and correct, "stale" if registered but wrong path,
|
||||||
|
/// "none" if not registered.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_context_menu_path() -> Result<String, String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let exe = std::env::current_exe()
|
||||||
|
.map_err(|e| format!("Failed to get exe path: {}", e))?;
|
||||||
|
let exe_str = exe.to_string_lossy().to_string();
|
||||||
|
let expected_cmd = format!("\"{}\"", exe_str);
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let cmd_path = r"Software\Classes\*\shell\Nomina\command";
|
||||||
|
|
||||||
|
match hkcu.open_subkey(cmd_path) {
|
||||||
|
Ok(key) => {
|
||||||
|
let val: String = key.get_value("").unwrap_or_default();
|
||||||
|
if val.starts_with(&expected_cmd) {
|
||||||
|
Ok("ok".to_string())
|
||||||
|
} else {
|
||||||
|
// auto-fix: re-register with current exe path
|
||||||
|
drop(key);
|
||||||
|
let _ = crate::commands::context_menu::unregister_context_menu();
|
||||||
|
crate::commands::context_menu::register_context_menu()?;
|
||||||
|
Ok("fixed".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Ok("none".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
Ok("none".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Given CLI args (paths), figure out what folder to open and which files to select.
|
/// Given CLI args (paths), figure out what folder to open and which files to select.
|
||||||
/// Returns (folder_path, selected_file_paths).
|
/// Returns (folder_path, selected_file_paths).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pub mod presets;
|
|||||||
pub mod undo;
|
pub mod undo;
|
||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod updates;
|
pub mod updates;
|
||||||
|
pub mod state;
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ pub struct PresetInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_presets_dir() -> PathBuf {
|
fn get_presets_dir() -> PathBuf {
|
||||||
let dirs = directories::ProjectDirs::from("com", "nomina", "Nomina")
|
crate::portable::data_dir().join("presets")
|
||||||
.expect("failed to get app data directory");
|
|
||||||
dirs.data_dir().join("presets")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_filename(name: &str) -> String {
|
fn sanitize_filename(name: &str) -> String {
|
||||||
|
|||||||
@@ -178,11 +178,13 @@ pub async fn execute_rename(
|
|||||||
if !bp.is_empty() {
|
if !bp.is_empty() {
|
||||||
PathBuf::from(bp)
|
PathBuf::from(bp)
|
||||||
} else {
|
} else {
|
||||||
let first_parent = valid[0].original_path.parent().unwrap();
|
let first_parent = valid[0].original_path.parent()
|
||||||
|
.ok_or("Cannot determine backup directory")?;
|
||||||
first_parent.join("_nomina_backup")
|
first_parent.join("_nomina_backup")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let first_parent = valid[0].original_path.parent().unwrap();
|
let first_parent = valid[0].original_path.parent()
|
||||||
|
.ok_or("Cannot determine backup directory")?;
|
||||||
first_parent.join("_nomina_backup")
|
first_parent.join("_nomina_backup")
|
||||||
};
|
};
|
||||||
std::fs::create_dir_all(&backup_dir)
|
std::fs::create_dir_all(&backup_dir)
|
||||||
@@ -203,7 +205,13 @@ pub async fn execute_rename(
|
|||||||
let mut undo_entries = Vec::new();
|
let mut undo_entries = Vec::new();
|
||||||
|
|
||||||
for op in &valid {
|
for op in &valid {
|
||||||
let parent = op.original_path.parent().unwrap();
|
let parent = match op.original_path.parent() {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
failed.push(format!("{}: cannot determine parent directory", op.original_name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let new_path = parent.join(&op.new_name);
|
let new_path = parent.join(&op.new_name);
|
||||||
|
|
||||||
// if target exists and isn't another file we're renaming
|
// if target exists and isn't another file we're renaming
|
||||||
@@ -261,7 +269,5 @@ pub async fn execute_rename(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_undo_log_path() -> PathBuf {
|
fn get_undo_log_path() -> PathBuf {
|
||||||
let dirs = directories::ProjectDirs::from("com", "nomina", "Nomina")
|
crate::portable::data_dir().join("undo.json")
|
||||||
.expect("failed to get app data directory");
|
|
||||||
dirs.data_dir().join("undo.json")
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
crates/nomina-app/src/commands/state.rs
Normal file
23
crates/nomina-app/src/commands/state.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn state_file() -> PathBuf {
|
||||||
|
crate::portable::data_dir().join("state.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn load_app_state() -> Result<String, String> {
|
||||||
|
let path = state_file();
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok("{}".to_string());
|
||||||
|
}
|
||||||
|
std::fs::read_to_string(&path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_app_state(json: String) -> Result<(), String> {
|
||||||
|
let path = state_file();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
std::fs::write(&path, json).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
@@ -78,7 +78,5 @@ fn execute_undo(batch: &UndoBatch) -> Result<UndoReport, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_undo_log_path() -> PathBuf {
|
fn get_undo_log_path() -> PathBuf {
|
||||||
let dirs = directories::ProjectDirs::from("com", "nomina", "Nomina")
|
crate::portable::data_dir().join("undo.json")
|
||||||
.expect("failed to get app data directory");
|
|
||||||
dirs.data_dir().join("undo.json")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod portable;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// redirect WebView2 data to portable location next to the exe
|
||||||
|
let webview_data = portable::data_dir().join("webview");
|
||||||
|
std::env::set_var("WEBVIEW2_USER_DATA_FOLDER", &webview_data);
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
@@ -25,8 +30,11 @@ fn main() {
|
|||||||
commands::context_menu::get_launch_args,
|
commands::context_menu::get_launch_args,
|
||||||
commands::context_menu::register_context_menu,
|
commands::context_menu::register_context_menu,
|
||||||
commands::context_menu::unregister_context_menu,
|
commands::context_menu::unregister_context_menu,
|
||||||
|
commands::context_menu::check_context_menu_path,
|
||||||
commands::context_menu::resolve_launch_paths,
|
commands::context_menu::resolve_launch_paths,
|
||||||
commands::updates::check_for_updates,
|
commands::updates::check_for_updates,
|
||||||
|
commands::state::load_app_state,
|
||||||
|
commands::state::save_app_state,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error running nomina");
|
.expect("error running nomina");
|
||||||
|
|||||||
14
crates/nomina-app/src/portable.rs
Normal file
14
crates/nomina-app/src/portable.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Returns the portable data directory next to the executable.
|
||||||
|
/// All app data (presets, undo history, state) is stored here.
|
||||||
|
pub fn data_dir() -> &'static PathBuf {
|
||||||
|
DATA_DIR.get_or_init(|| {
|
||||||
|
let exe = std::env::current_exe().expect("cannot determine executable path");
|
||||||
|
let exe_dir = exe.parent().expect("executable has no parent directory");
|
||||||
|
exe_dir.join("data")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ pub fn parse_bru_file(path: &Path) -> crate::Result<NominaPreset> {
|
|||||||
rules.push(make_rule("regex", serde_json::json!({
|
rules.push(make_rule("regex", serde_json::json!({
|
||||||
"pattern": pattern,
|
"pattern": pattern,
|
||||||
"replacement": replace,
|
"replacement": replace,
|
||||||
"case_sensitive": case,
|
"case_insensitive": !case,
|
||||||
"global": true,
|
"global": true,
|
||||||
"target": "Name",
|
"target": "Name",
|
||||||
})));
|
})));
|
||||||
|
|||||||
@@ -550,10 +550,7 @@ export function FileList() {
|
|||||||
const hasError = preview?.has_error;
|
const hasError = preview?.has_error;
|
||||||
const hasConflict = preview?.has_conflict;
|
const hasConflict = preview?.has_conflict;
|
||||||
|
|
||||||
// entry index relative to allDisplayEntries for shift-select
|
const entryIndex = virtualRow.index - parentRow;
|
||||||
const entryIndex = isDir
|
|
||||||
? virtualRow.index - parentRow
|
|
||||||
: virtualRow.index - parentRow;
|
|
||||||
|
|
||||||
const isOdd = virtualRow.index % 2 === 1;
|
const isOdd = virtualRow.index % 2 === 1;
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,20 @@ export function AppShell() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// check if Explorer context menu points to the wrong exe (app was moved)
|
||||||
|
useEffect(() => {
|
||||||
|
invoke<string>("check_context_menu_path")
|
||||||
|
.then((status) => {
|
||||||
|
if (status === "fixed") {
|
||||||
|
toast.info("Explorer context menu updated", {
|
||||||
|
description: "The app was moved since the context menu was registered. The registry entries have been updated to point to the new location.",
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onDividerPointerDown = useCallback((e: React.PointerEvent) => {
|
const onDividerPointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragging.current = true;
|
dragging.current = true;
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function Sidebar() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (entries.length >= 0) {
|
if (entries.length > 0) {
|
||||||
drives.push({ path, name: path, children: [], loaded: false, expanded: false });
|
drives.push({ path, name: path, children: [], loaded: false, expanded: false });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function PresetsDialog({ open, onOpenChange }: PresetsDialogProps) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const path = typeof selected === "string" ? selected : selected;
|
const path = typeof selected === "string" ? selected : selected[0];
|
||||||
const preset = await invoke<NominaPreset>("import_preset", { path });
|
const preset = await invoke<NominaPreset>("import_preset", { path });
|
||||||
loadPipeline(preset.rules);
|
loadPipeline(preset.rules);
|
||||||
if (preset.filters) {
|
if (preset.filters) {
|
||||||
|
|||||||
@@ -655,8 +655,8 @@ function StartupSection() {
|
|||||||
setChecking(true);
|
setChecking(true);
|
||||||
setUpdateStatus(null);
|
setUpdateStatus(null);
|
||||||
try {
|
try {
|
||||||
const result = await invoke<{ has_update: boolean; latest_version: string; current_version: string }>("check_for_updates");
|
const result = await invoke<{ available: boolean; latest_version: string; current_version: string }>("check_for_updates");
|
||||||
if (result.has_update) {
|
if (result.available) {
|
||||||
setUpdateStatus(`Update available: v${result.latest_version} (current: v${result.current_version})`);
|
setUpdateStatus(`Update available: v${result.latest_version} (current: v${result.current_version})`);
|
||||||
} else {
|
} else {
|
||||||
setUpdateStatus(`You're up to date (v${result.current_version})`);
|
setUpdateStatus(`You're up to date (v${result.current_version})`);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useFileStore } from "@/stores/fileStore";
|
import { useFileStore } from "@/stores/fileStore";
|
||||||
import { useRuleStore } from "@/stores/ruleStore";
|
|
||||||
|
|
||||||
export function useKeyboardShortcuts() {
|
export function useKeyboardShortcuts() {
|
||||||
const selectAll = useFileStore((s) => s.selectAll);
|
const selectAll = useFileStore((s) => s.selectAll);
|
||||||
const deselectAll = useFileStore((s) => s.deselectAll);
|
const deselectAll = useFileStore((s) => s.deselectAll);
|
||||||
const resetAllRules = useRuleStore((s) => s.resetAllRules);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handler(e: KeyboardEvent) {
|
function handler(e: KeyboardEvent) {
|
||||||
@@ -18,11 +16,17 @@ export function useKeyboardShortcuts() {
|
|||||||
deselectAll();
|
deselectAll();
|
||||||
}
|
}
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
resetAllRules();
|
const target = e.target as HTMLElement;
|
||||||
|
const inOverlay = target.closest(
|
||||||
|
"[data-slot='dialog-content'], [data-slot='popover-content'], [data-slot='context-menu-content'], [data-slot='dropdown-menu-content']"
|
||||||
|
);
|
||||||
|
if (!inOverlay) {
|
||||||
|
deselectAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("keydown", handler);
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener("keydown", handler);
|
return () => window.removeEventListener("keydown", handler);
|
||||||
}, [selectAll, deselectAll, resetAllRules]);
|
}, [selectAll, deselectAll]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { getCurrentWindow, availableMonitors } from "@tauri-apps/api/window";
|
import { getCurrentWindow, availableMonitors } from "@tauri-apps/api/window";
|
||||||
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
|
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
|
||||||
|
import { getStorageItem, setStorageItem } from "@/lib/storage";
|
||||||
|
|
||||||
const KEY = "nomina-window-state";
|
const KEY = "nomina-window-state";
|
||||||
const MIN_WIDTH = 400;
|
const MIN_WIDTH = 400;
|
||||||
@@ -52,7 +53,7 @@ export function useWindowState() {
|
|||||||
// restore
|
// restore
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(KEY);
|
const raw = getStorageItem(KEY);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const s: WindowState = JSON.parse(raw);
|
const s: WindowState = JSON.parse(raw);
|
||||||
if (s.maximized) {
|
if (s.maximized) {
|
||||||
@@ -90,13 +91,13 @@ export function useWindowState() {
|
|||||||
try {
|
try {
|
||||||
const maximized = await win.isMaximized();
|
const maximized = await win.isMaximized();
|
||||||
if (maximized) {
|
if (maximized) {
|
||||||
const prev = localStorage.getItem(KEY);
|
const prev = getStorageItem(KEY);
|
||||||
if (prev) {
|
if (prev) {
|
||||||
const s: WindowState = JSON.parse(prev);
|
const s: WindowState = JSON.parse(prev);
|
||||||
s.maximized = true;
|
s.maximized = true;
|
||||||
localStorage.setItem(KEY, JSON.stringify(s));
|
setStorageItem(KEY, JSON.stringify(s));
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(KEY, JSON.stringify({ x: 100, y: 100, width: 1200, height: 800, maximized: true }));
|
setStorageItem(KEY, JSON.stringify({ x: 100, y: 100, width: 1200, height: 800, maximized: true }));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ export function useWindowState() {
|
|||||||
height: size.height,
|
height: size.height,
|
||||||
maximized: false,
|
maximized: false,
|
||||||
};
|
};
|
||||||
localStorage.setItem(KEY, JSON.stringify(state));
|
setStorageItem(KEY, JSON.stringify(state));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
ui/src/lib/storage.ts
Normal file
56
ui/src/lib/storage.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { StateStorage } from "zustand/middleware";
|
||||||
|
|
||||||
|
let cache: Record<string, string> = {};
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
async function loadAll(): Promise<Record<string, string>> {
|
||||||
|
if (loaded) return cache;
|
||||||
|
try {
|
||||||
|
const raw = await invoke<string>("load_app_state");
|
||||||
|
cache = JSON.parse(raw) || {};
|
||||||
|
} catch {
|
||||||
|
cache = {};
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function scheduleSave() {
|
||||||
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await invoke("save_app_state", { json: JSON.stringify(cache) });
|
||||||
|
} catch {}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initStorage() {
|
||||||
|
await loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const portableStorage: StateStorage = {
|
||||||
|
getItem(name: string): string | null {
|
||||||
|
return cache[name] ?? null;
|
||||||
|
},
|
||||||
|
setItem(name: string, value: string): void {
|
||||||
|
cache[name] = value;
|
||||||
|
scheduleSave();
|
||||||
|
},
|
||||||
|
removeItem(name: string): void {
|
||||||
|
delete cache[name];
|
||||||
|
scheduleSave();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// direct read/write for non-Zustand stores
|
||||||
|
export function getStorageItem(key: string): string | null {
|
||||||
|
return cache[key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStorageItem(key: string, value: string): void {
|
||||||
|
cache[key] = value;
|
||||||
|
scheduleSave();
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import { initStorage } from "@/lib/storage";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
initStorage().then(() => {
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { create } from "zustand";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { PreviewResult } from "@/types/files";
|
import type { PreviewResult } from "@/types/files";
|
||||||
import type { RuleConfig } from "@/types/rules";
|
import type { RuleConfig } from "@/types/rules";
|
||||||
|
import { getStorageItem, setStorageItem } from "@/lib/storage";
|
||||||
import { useFileStore } from "./fileStore";
|
import { useFileStore } from "./fileStore";
|
||||||
import { useSettingsStore } from "./settingsStore";
|
import { useSettingsStore } from "./settingsStore";
|
||||||
import {
|
import {
|
||||||
@@ -54,13 +55,13 @@ function nextId(): string {
|
|||||||
|
|
||||||
function savePipeline(pipeline: PipelineRule[]) {
|
function savePipeline(pipeline: PipelineRule[]) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(PIPELINE_KEY, JSON.stringify(pipeline.map((r) => r.config)));
|
setStorageItem(PIPELINE_KEY, JSON.stringify(pipeline.map((r) => r.config)));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSavedPipeline(): RuleConfig[] | null {
|
function loadSavedPipeline(): RuleConfig[] | null {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(PIPELINE_KEY);
|
const raw = getStorageItem(PIPELINE_KEY);
|
||||||
if (raw) return JSON.parse(raw);
|
if (raw) return JSON.parse(raw);
|
||||||
} catch {}
|
} catch {}
|
||||||
return null;
|
return null;
|
||||||
@@ -80,7 +81,7 @@ function createDefault(type: string): RuleConfig {
|
|||||||
const caseSensitive = useSettingsStore.getState().caseSensitiveMatch;
|
const caseSensitive = useSettingsStore.getState().caseSensitiveMatch;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "replace": return { ...defaultReplace(), match_case: caseSensitive } as RuleConfig;
|
case "replace": return { ...defaultReplace(), match_case: caseSensitive } as RuleConfig;
|
||||||
case "regex": return { ...defaultRegex(), case_sensitive: caseSensitive } as RuleConfig;
|
case "regex": return { ...defaultRegex(), case_insensitive: !caseSensitive } as RuleConfig;
|
||||||
case "remove": return defaultRemove();
|
case "remove": return defaultRemove();
|
||||||
case "add": return defaultAdd();
|
case "add": return defaultAdd();
|
||||||
case "case": return defaultCase();
|
case "case": return defaultCase();
|
||||||
@@ -138,7 +139,7 @@ export const useRuleStore = create<RuleState>((set, get) => ({
|
|||||||
const current = get().pipeline;
|
const current = get().pipeline;
|
||||||
const idx = current.findIndex((r) => r.id === id);
|
const idx = current.findIndex((r) => r.id === id);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
const clone: PipelineRule = { id: nextId(), config: { ...current[idx].config } };
|
const clone: PipelineRule = { id: nextId(), config: JSON.parse(JSON.stringify(current[idx].config)) };
|
||||||
const pipeline = [...current.slice(0, idx + 1), clone, ...current.slice(idx + 1)];
|
const pipeline = [...current.slice(0, idx + 1), clone, ...current.slice(idx + 1)];
|
||||||
set({ pipeline });
|
set({ pipeline });
|
||||||
savePipeline(pipeline);
|
savePipeline(pipeline);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
|
import { portableStorage } from "@/lib/storage";
|
||||||
|
|
||||||
type Theme = "light" | "dark" | "system";
|
type Theme = "light" | "dark" | "system";
|
||||||
type SortOrder = "name" | "date" | "size" | "type";
|
type SortOrder = "name" | "date" | "size" | "type";
|
||||||
@@ -247,6 +248,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "nomina-settings",
|
name: "nomina-settings",
|
||||||
|
storage: createJSONStorage(() => portableStorage),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user