diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 74c4642..9a960d7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,8 +5,32 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::menu::{Menu, MenuItem}; + +struct MigrationState(Mutex>); + +#[derive(Serialize)] +struct PickDataDirResult { + path: String, + has_documents: bool, +} + +#[tauri::command] +fn pick_data_dir(app: AppHandle, state: tauri::State<'_, MigrationState>) -> Result, String> { + if let Some(p) = app.dialog().file().blocking_pick_folder() { + let path = p.into_path().map_err(|e| e.to_string())?; + let has_docs = path.join("documents.json").exists(); + *state.0.lock().unwrap() = Some(path.clone()); + Ok(Some(PickDataDirResult { + path: path.to_string_lossy().into_owned(), + has_documents: has_docs, + })) + } else { + Ok(None) + } +} use tauri::tray::TrayIconBuilder; use tauri::{AppHandle, Emitter, Manager, WindowEvent}; use tauri_plugin_dialog::DialogExt; @@ -537,11 +561,6 @@ fn open_backups_folder(app: AppHandle, dir: Option) -> Result<(), String // ---------- data folder migration -#[tauri::command] -fn data_dir_has_documents(dir: String) -> bool { - PathBuf::from(dir).join("documents.json").exists() -} - /// strategy: "move" (copy mine, delete originals), "overwrite" (same, target /// had documents), "use_existing" (just switch — keep the target's files) #[tauri::command] @@ -550,9 +569,15 @@ fn migrate_data_dir( old_dir: Option, new_dir: String, strategy: String, + state: tauri::State<'_, MigrationState>, ) -> Result<(), String> { - let old = docs_dir(&app, &old_dir); + let approved_dir = state.0.lock().unwrap().take(); let new = PathBuf::from(&new_dir); + if Some(new.clone()) != approved_dir { + return Err("Migration rejected: target directory was not explicitly chosen by the user.".into()); + } + + let old = docs_dir(&app, &old_dir); fs::create_dir_all(&new).map_err(|e| e.to_string())?; // canonicalize: "C:\Data" and "c:\data" are the same folder on Windows let canon_old = fs::canonicalize(&old).unwrap_or_else(|_| old.clone()); @@ -743,7 +768,7 @@ fn main() { run_backups, backup_deleted_sheet, open_backups_folder, - data_dir_has_documents, + pick_data_dir, migrate_data_dir, fetch_rates, fetch_historical_rates, @@ -758,6 +783,7 @@ fn main() { write_image_file ]) .setup(|app| { + app.manage(MigrationState(Mutex::new(None))); let handle = app.handle().clone(); let show = MenuItem::with_id(app, "show", "Show / Hide", true, None::<&str>)?; let quit = MenuItem::with_id(app, "quit", "Quit Summarum", true, None::<&str>)?; diff --git a/src/main.ts b/src/main.ts index 91b0657..a51cdea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,7 @@ import { fetchRates, fetchMarketData, fetchHistoricalRates, loadExtensionScripts, openExtensionsFolder, isTauri, getLaunchFile, onOpenFile, onFileDrop, setDataDir, runBackups, backupDeletedSheet, openBackupsFolder, - chooseFolder, dataDirHasDocuments, migrateDataDir, + pickDataDir, migrateDataDir, } from "./storage"; import type { LineResult } from "./engine"; import { setLang, detectLang, t } from "./i18n"; @@ -404,10 +404,12 @@ function bindDataDirSettings(): void { }; renderDataDir(); dataDirBtn.addEventListener("click", async () => { - const picked = await chooseFolder(); - if (!picked || picked === settings.dataDir) return; + const res = await pickDataDir(); + if (!res) return; + const { path: picked, hasDocuments } = res; + if (picked === settings.dataDir) return; let strategy: "move" | "overwrite" | "use_existing" = "move"; - if (await dataDirHasDocuments(picked)) { + if (hasDocuments) { const ans = await askModal(t("folderConflict"), t("useExisting"), t("replaceMine")); if (ans === null) return; strategy = ans === "a" ? "use_existing" : "overwrite"; diff --git a/src/storage.ts b/src/storage.ts index c316eda..dc96d62 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -167,16 +167,12 @@ export async function openBackupsFolder(dataDir: string): Promise { if (isTauri()) await invoke("open_backups_folder", { dir: dirArg(dataDir) }); } -export async function chooseFolder(): Promise { +export async function pickDataDir(): Promise<{ path: string, hasDocuments: boolean } | null> { if (!isTauri()) return null; - const { open } = await import("@tauri-apps/plugin-dialog"); - const picked = await open({ directory: true, multiple: false }); - return typeof picked === "string" ? picked : null; -} - -export async function dataDirHasDocuments(dir: string): Promise { - if (!isTauri()) return false; - return invoke("data_dir_has_documents", { dir }); + return invoke<{ path: string, has_documents: boolean } | null>("pick_data_dir").then(res => { + if (!res) return null; + return { path: res.path, hasDocuments: res.has_documents }; + }); } export type MigrateStrategy = "move" | "overwrite" | "use_existing";