fix: Settings persistent (kein Keyring) + Single-Instance

Config-Persistenz:
- Passwort wird base64-kodiert in config.json gespeichert
  (statt OS-Keyring der beim Cross-Compile nicht funktioniert)
- Config-Pfad wird beim Laden/Speichern geloggt fuer Debugging
- Keyring-Dependency entfernt, base64 hinzugefuegt

Single-Instance:
- Lock-File in Config-Dir verhindert doppelte Instanz
- Wenn .cloud Datei doppelgeklickt wird und Client laeuft:
  Pfad wird in open_request.txt geschrieben und 2. Instanz beendet sich
- Laufende Instanz pollt open_request.txt und oeffnet die Datei
- Fenster wird automatisch in den Vordergrund geholt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 01:27:03 +02:00
parent c354682905
commit 4cc8de8a1a
3 changed files with 100 additions and 33 deletions
+61 -9
View File
@@ -39,11 +39,11 @@ async fn login(
*state.api.lock().unwrap() = Some(api);
*state.username.lock().unwrap() = Some(username.clone());
// Save credentials
let _ = AppConfig::save_password(&server_url, &username, &password);
// Save credentials to config file
let mut config = AppConfig::load();
config.server_url = server_url;
config.username = username;
config.save_password(&password);
let _ = config.save();
Ok(serde_json::json!({
@@ -57,11 +57,9 @@ async fn login(
#[tauri::command]
fn load_saved_config() -> Result<serde_json::Value, String> {
let config = AppConfig::load();
let has_credentials = if !config.server_url.is_empty() && !config.username.is_empty() {
AppConfig::load_password(&config.server_url, &config.username).is_some()
} else {
false
};
let has_credentials = !config.server_url.is_empty()
&& !config.username.is_empty()
&& config.get_password().is_some();
Ok(serde_json::json!({
"server_url": config.server_url,
@@ -79,8 +77,8 @@ async fn auto_login(state: State<'_, AppState>) -> Result<serde_json::Value, Str
return Err("Keine gespeicherten Zugangsdaten".to_string());
}
let password = AppConfig::load_password(&config.server_url, &config.username)
.ok_or("Passwort nicht im Keychain gefunden")?;
let password = config.get_password()
.ok_or("Passwort nicht gespeichert")?;
let mut api = MiniCloudApi::new(&config.server_url);
let result = api.login(&config.username, &password).await?;
@@ -527,8 +525,36 @@ fn start_background_sync(
// --- App Setup ---
/// Check if another instance is running. If yes, pass the .cloud file to it and exit.
fn handle_single_instance() {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("MiniCloud Sync");
std::fs::create_dir_all(&config_dir).ok();
let lock_file = config_dir.join("instance.lock");
let args: Vec<String> = std::env::args().collect();
// If we have a .cloud file argument and another instance is running
if args.len() > 1 && args[1].ends_with(".cloud") {
if lock_file.exists() {
// Write the .cloud path to a request file for the running instance
let request_file = config_dir.join("open_request.txt");
std::fs::write(&request_file, &args[1]).ok();
eprintln!("[SingleInstance] Passing {} to running instance", args[1]);
std::process::exit(0);
}
}
// Write our lock file
std::fs::write(&lock_file, std::process::id().to_string()).ok();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
handle_single_instance();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
@@ -558,6 +584,32 @@ pub fn run() {
// Use window icon for tray (set via tauri.conf.json)
let icon = app.default_window_icon().cloned();
// Watch for open requests from other instances
let app_req = app.handle().clone();
std::thread::spawn(move || {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("MiniCloud Sync");
let request_file = config_dir.join("open_request.txt");
loop {
std::thread::sleep(Duration::from_secs(1));
if request_file.exists() {
if let Ok(path) = std::fs::read_to_string(&request_file) {
let path = path.trim().to_string();
if !path.is_empty() {
let _ = app_req.emit("open-cloud-file", path);
// Show the window
if let Some(w) = app_req.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
}
std::fs::remove_file(&request_file).ok();
}
}
});
// Handle .cloud file opened via file association (double-click)
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {