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::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -25,10 +26,62 @@ struct CommandEnvelope {
|
|||||||
payload: Option<Value>,
|
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 {
|
struct AppSettings {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
sidecar_root: Option<String>,
|
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 {
|
struct ManagedSidecar {
|
||||||
@ -295,12 +348,9 @@ async fn set_sidecar_root(
|
|||||||
let is_custom = new_override.is_some();
|
let is_custom = new_override.is_some();
|
||||||
*state.root_override.lock().await = new_override.clone();
|
*state.root_override.lock().await = new_override.clone();
|
||||||
|
|
||||||
save_settings(
|
let mut settings = load_settings(&state.config_path);
|
||||||
&state.config_path,
|
settings.sidecar_root = new_override.map(|p| p.to_string_lossy().into_owned());
|
||||||
&AppSettings {
|
save_settings(&state.config_path, &settings)?;
|
||||||
sidecar_root: new_override.map(|p| p.to_string_lossy().into_owned()),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"root": root.to_string_lossy(),
|
"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]
|
#[tauri::command]
|
||||||
async fn shutdown(
|
async fn shutdown(
|
||||||
state: tauri::State<'_, SidecarState>,
|
state: tauri::State<'_, SidecarState>,
|
||||||
@ -343,6 +422,8 @@ pub fn run() {
|
|||||||
shutdown,
|
shutdown,
|
||||||
get_sidecar_root,
|
get_sidecar_root,
|
||||||
set_sidecar_root,
|
set_sidecar_root,
|
||||||
|
get_ui_settings,
|
||||||
|
set_ui_settings,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let config_dir = app.path().app_config_dir()?;
|
let config_dir = app.path().app_config_dir()?;
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export const settingsTags = writable<string[]>(["Personal", "Work", "Ideas", "Journal"]);
|
const defaultTags = ["Personal", "Work", "Ideas", "Journal"];
|
||||||
export const settingsFragmentTypes = writable<string[]>(["Quote", "Snippet", "Reference"]);
|
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 {
|
function normalize(value: string): string {
|
||||||
return value.trim().toLowerCase();
|
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);
|
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 {
|
export function addSettingsTag(value: string): boolean {
|
||||||
const next = value.trim();
|
const next = value.trim();
|
||||||
if (!next) return false;
|
if (!next) return false;
|
||||||
const tags = get(settingsTags);
|
const tags = get(settingsTags);
|
||||||
if (hasDuplicate(tags, next)) return false;
|
if (hasDuplicate(tags, next)) return false;
|
||||||
settingsTags.set([...tags, next]);
|
settingsTags.set([...tags, next]);
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +91,7 @@ export function updateSettingsTag(index: number, value: string): boolean {
|
|||||||
if (index < 0 || index >= tags.length) return false;
|
if (index < 0 || index >= tags.length) return false;
|
||||||
if (hasDuplicate(tags, next, index)) return false;
|
if (hasDuplicate(tags, next, index)) return false;
|
||||||
settingsTags.set(tags.map((tag, idx) => (idx === index ? next : tag)));
|
settingsTags.set(tags.map((tag, idx) => (idx === index ? next : tag)));
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +99,7 @@ export function removeSettingsTag(index: number): boolean {
|
|||||||
const tags = get(settingsTags);
|
const tags = get(settingsTags);
|
||||||
if (index < 0 || index >= tags.length) return false;
|
if (index < 0 || index >= tags.length) return false;
|
||||||
settingsTags.set(tags.filter((_, idx) => idx !== index));
|
settingsTags.set(tags.filter((_, idx) => idx !== index));
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +109,7 @@ export function addFragmentType(value: string): boolean {
|
|||||||
const types = get(settingsFragmentTypes);
|
const types = get(settingsFragmentTypes);
|
||||||
if (hasDuplicate(types, next)) return false;
|
if (hasDuplicate(types, next)) return false;
|
||||||
settingsFragmentTypes.set([...types, next]);
|
settingsFragmentTypes.set([...types, next]);
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +120,7 @@ export function updateFragmentType(index: number, value: string): boolean {
|
|||||||
if (index < 0 || index >= types.length) return false;
|
if (index < 0 || index >= types.length) return false;
|
||||||
if (hasDuplicate(types, next, index)) return false;
|
if (hasDuplicate(types, next, index)) return false;
|
||||||
settingsFragmentTypes.set(types.map((type, idx) => (idx === index ? next : type)));
|
settingsFragmentTypes.set(types.map((type, idx) => (idx === index ? next : type)));
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,5 +128,6 @@ export function removeFragmentType(index: number): boolean {
|
|||||||
const types = get(settingsFragmentTypes);
|
const types = get(settingsFragmentTypes);
|
||||||
if (index < 0 || index >= types.length) return false;
|
if (index < 0 || index >= types.length) return false;
|
||||||
settingsFragmentTypes.set(types.filter((_, idx) => idx !== index));
|
settingsFragmentTypes.set(types.filter((_, idx) => idx !== index));
|
||||||
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,13 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
|
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
|
||||||
import { persistAndClearVault } from "$lib/backend/auth";
|
import { persistAndClearVault } from "$lib/backend/auth";
|
||||||
|
import { hydrateUiSettings } from "$lib/stores/settings";
|
||||||
|
|
||||||
let closeInProgress = false;
|
let closeInProgress = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
void hydrateUiSettings();
|
||||||
|
|
||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
const unlistenPromise = appWindow.onCloseRequested(async (event) => {
|
const unlistenPromise = appWindow.onCloseRequested(async (event) => {
|
||||||
if (closeInProgress) return;
|
if (closeInProgress) return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user