feat(settings): persist tags and fragment types via Tauri settings
This commit is contained in:
parent
235e46240d
commit
7c3161c61b
@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@ -25,10 +26,62 @@ struct CommandEnvelope {
|
||||
payload: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
const DEFAULT_SETTINGS_TAGS: &[&str] = &["Personal", "Work", "Ideas", "Journal"];
|
||||
const DEFAULT_FRAGMENT_TYPES: &[&str] = &["Quote", "Snippet", "Reference"];
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct AppSettings {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sidecar_root: Option<String>,
|
||||
#[serde(default = "default_settings_tags")]
|
||||
tags: Vec<String>,
|
||||
#[serde(default = "default_fragment_types")]
|
||||
fragment_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sidecar_root: None,
|
||||
tags: default_settings_tags(),
|
||||
fragment_types: default_fragment_types(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_settings_tags() -> Vec<String> {
|
||||
DEFAULT_SETTINGS_TAGS
|
||||
.iter()
|
||||
.map(|v| (*v).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_fragment_types() -> Vec<String> {
|
||||
DEFAULT_FRAGMENT_TYPES
|
||||
.iter()
|
||||
.map(|v| (*v).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_items(values: Vec<String>, fallback: &[&str]) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
for item in values {
|
||||
let trimmed = item.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let key = trimmed.to_lowercase();
|
||||
if seen.insert(key) {
|
||||
normalized.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if normalized.is_empty() {
|
||||
return fallback.iter().map(|v| (*v).to_string()).collect();
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
struct ManagedSidecar {
|
||||
@ -295,12 +348,9 @@ async fn set_sidecar_root(
|
||||
let is_custom = new_override.is_some();
|
||||
*state.root_override.lock().await = new_override.clone();
|
||||
|
||||
save_settings(
|
||||
&state.config_path,
|
||||
&AppSettings {
|
||||
sidecar_root: new_override.map(|p| p.to_string_lossy().into_owned()),
|
||||
},
|
||||
)?;
|
||||
let mut settings = load_settings(&state.config_path);
|
||||
settings.sidecar_root = new_override.map(|p| p.to_string_lossy().into_owned());
|
||||
save_settings(&state.config_path, &settings)?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"root": root.to_string_lossy(),
|
||||
@ -308,6 +358,35 @@ async fn set_sidecar_root(
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_ui_settings(state: tauri::State<'_, SidecarState>) -> Result<Value, String> {
|
||||
let settings = load_settings(&state.config_path);
|
||||
let tags = normalize_items(settings.tags, DEFAULT_SETTINGS_TAGS);
|
||||
let fragment_types = normalize_items(settings.fragment_types, DEFAULT_FRAGMENT_TYPES);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"tags": tags,
|
||||
"fragmentTypes": fragment_types
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_ui_settings(
|
||||
state: tauri::State<'_, SidecarState>,
|
||||
tags: Vec<String>,
|
||||
fragment_types: Vec<String>,
|
||||
) -> Result<Value, String> {
|
||||
let mut settings = load_settings(&state.config_path);
|
||||
settings.tags = normalize_items(tags, DEFAULT_SETTINGS_TAGS);
|
||||
settings.fragment_types = normalize_items(fragment_types, DEFAULT_FRAGMENT_TYPES);
|
||||
save_settings(&state.config_path, &settings)?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"tags": settings.tags,
|
||||
"fragmentTypes": settings.fragment_types
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn shutdown(
|
||||
state: tauri::State<'_, SidecarState>,
|
||||
@ -343,6 +422,8 @@ pub fn run() {
|
||||
shutdown,
|
||||
get_sidecar_root,
|
||||
set_sidecar_root,
|
||||
get_ui_settings,
|
||||
set_ui_settings,
|
||||
])
|
||||
.setup(|app| {
|
||||
let config_dir = app.path().app_config_dir()?;
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export const settingsTags = writable<string[]>(["Personal", "Work", "Ideas", "Journal"]);
|
||||
export const settingsFragmentTypes = writable<string[]>(["Quote", "Snippet", "Reference"]);
|
||||
const defaultTags = ["Personal", "Work", "Ideas", "Journal"];
|
||||
const defaultFragmentTypes = ["Quote", "Snippet", "Reference"];
|
||||
|
||||
export const settingsTags = writable<string[]>([...defaultTags]);
|
||||
export const settingsFragmentTypes = writable<string[]>([...defaultFragmentTypes]);
|
||||
|
||||
let hydrationComplete = false;
|
||||
let hydrating = false;
|
||||
|
||||
type UiSettingsPayload = {
|
||||
tags?: string[];
|
||||
fragmentTypes?: string[];
|
||||
};
|
||||
|
||||
function normalize(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
@ -13,12 +25,62 @@ function hasDuplicate(values: string[], candidate: string, excludeIndex?: number
|
||||
return values.some((value, index) => index !== excludeIndex && normalize(value) === normalized);
|
||||
}
|
||||
|
||||
function normalizeValues(values: string[], fallback: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) continue;
|
||||
const key = normalize(trimmed);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(trimmed);
|
||||
}
|
||||
return result.length ? result : [...fallback];
|
||||
}
|
||||
|
||||
export async function hydrateUiSettings(): Promise<void> {
|
||||
if (hydrating || hydrationComplete) return;
|
||||
hydrating = true;
|
||||
try {
|
||||
const payload = await invoke<UiSettingsPayload>("get_ui_settings");
|
||||
settingsTags.set(normalizeValues(payload.tags ?? [], defaultTags));
|
||||
settingsFragmentTypes.set(normalizeValues(payload.fragmentTypes ?? [], defaultFragmentTypes));
|
||||
} catch (error) {
|
||||
console.error("[settings] hydrate failed", error);
|
||||
} finally {
|
||||
hydrating = false;
|
||||
hydrationComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function persistUiSettings(): Promise<void> {
|
||||
const tags = normalizeValues(get(settingsTags), defaultTags);
|
||||
const fragmentTypes = normalizeValues(get(settingsFragmentTypes), defaultFragmentTypes);
|
||||
settingsTags.set(tags);
|
||||
settingsFragmentTypes.set(fragmentTypes);
|
||||
|
||||
await invoke("set_ui_settings", {
|
||||
tags,
|
||||
fragmentTypes,
|
||||
fragment_types: fragmentTypes
|
||||
});
|
||||
}
|
||||
|
||||
function queuePersist(): void {
|
||||
if (!hydrationComplete || hydrating) return;
|
||||
void persistUiSettings().catch((error) => {
|
||||
console.error("[settings] persist failed", error);
|
||||
});
|
||||
}
|
||||
|
||||
export function addSettingsTag(value: string): boolean {
|
||||
const next = value.trim();
|
||||
if (!next) return false;
|
||||
const tags = get(settingsTags);
|
||||
if (hasDuplicate(tags, next)) return false;
|
||||
settingsTags.set([...tags, next]);
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -29,6 +91,7 @@ export function updateSettingsTag(index: number, value: string): boolean {
|
||||
if (index < 0 || index >= tags.length) return false;
|
||||
if (hasDuplicate(tags, next, index)) return false;
|
||||
settingsTags.set(tags.map((tag, idx) => (idx === index ? next : tag)));
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -36,6 +99,7 @@ export function removeSettingsTag(index: number): boolean {
|
||||
const tags = get(settingsTags);
|
||||
if (index < 0 || index >= tags.length) return false;
|
||||
settingsTags.set(tags.filter((_, idx) => idx !== index));
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -45,6 +109,7 @@ export function addFragmentType(value: string): boolean {
|
||||
const types = get(settingsFragmentTypes);
|
||||
if (hasDuplicate(types, next)) return false;
|
||||
settingsFragmentTypes.set([...types, next]);
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -55,6 +120,7 @@ export function updateFragmentType(index: number, value: string): boolean {
|
||||
if (index < 0 || index >= types.length) return false;
|
||||
if (hasDuplicate(types, next, index)) return false;
|
||||
settingsFragmentTypes.set(types.map((type, idx) => (idx === index ? next : type)));
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -62,5 +128,6 @@ export function removeFragmentType(index: number): boolean {
|
||||
const types = get(settingsFragmentTypes);
|
||||
if (index < 0 || index >= types.length) return false;
|
||||
settingsFragmentTypes.set(types.filter((_, idx) => idx !== index));
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@
|
||||
import { onMount } from "svelte";
|
||||
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
|
||||
import { persistAndClearVault } from "$lib/backend/auth";
|
||||
import { hydrateUiSettings } from "$lib/stores/settings";
|
||||
|
||||
let closeInProgress = false;
|
||||
|
||||
onMount(() => {
|
||||
void hydrateUiSettings();
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const unlistenPromise = appWindow.onCloseRequested(async (event) => {
|
||||
if (closeInProgress) return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user