//! Vault setup screen, vault switching, and registry management. use std::path::{Path, PathBuf}; use audiofiles_browser::ui::{theme, widgets}; use audiofiles_core::vault::{self, VaultRegistry}; use eframe::egui; use super::AudioFilesApp; const DEFAULT_VAULT_NAME: &str = "Library"; impl AudioFilesApp { /// Draw the vault setup screen (first-open flow, after activation). pub(crate) fn draw_vault_setup_screen(&mut self, ui: &mut egui::Ui) { let default_path = vault::default_vault_path(); let existing_db = default_path.join("audiofiles.db").exists(); // Pre-fill the vault-name field on first entry so the fallback is visible. if self.vault_setup_name.is_empty() { self.vault_setup_name = DEFAULT_VAULT_NAME.to_string(); } egui::CentralPanel::default().show_inside(ui, |ui| { let available = ui.available_size(); ui.add_space((available.y * 0.20).max(40.0)); ui.vertical_centered(|ui| { let column_width = 480.0_f32.min(available.x - 40.0); ui.allocate_ui(egui::vec2(column_width, 0.0), |ui| { ui.vertical(|ui| { ui.heading("Where should we store your library?"); ui.add_space(theme::space::MD); if existing_db && self.vault_setup_path.is_none() { ui.label( egui::RichText::new(format!( "Your existing library at {} will be opened.", default_path.display() )) .color(theme::text_secondary()), ); ui.add_space(theme::space::SECTION); } else { ui.add_space(theme::space::SM); } // Vault name ui.horizontal(|ui| { ui.label("Name:"); ui.text_edit_singleline(&mut self.vault_setup_name); }); ui.add_space(theme::space::MD); // Location: shown inline with the picker action. No // separate "Use default location" button — the default // is already the value displayed, and "Continue" // commits whatever is shown here. let chosen_path = self .vault_setup_path .clone() .unwrap_or_else(|| default_path.clone()); let is_custom = self.vault_setup_path.is_some(); ui.label("Location:"); ui.horizontal_wrapped(|ui| { let path_text = if is_custom { widgets::accent_strong(chosen_path.display().to_string()) } else { egui::RichText::new(chosen_path.display().to_string()) .color(theme::text_secondary()) }; ui.label(path_text); }); ui.add_space(theme::space::SM); ui.horizontal(|ui| { if ui.button("Choose different location...").clicked() && let Some(path) = rfd::FileDialog::new().pick_folder() { self.vault_setup_path = Some(path); } if is_custom && ui.button("Use default").clicked() { self.vault_setup_path = None; } }); ui.add_space(theme::space::XL); // Single primary action — the only path that advances. if widgets::primary_button(ui, "Continue").clicked() { self.finalize_vault_setup(chosen_path); } }); }); }); }); } /// Finalise vault setup: create registry, set data_dir, open browser. fn finalize_vault_setup(&mut self, vault_path: PathBuf) { let name = if self.vault_setup_name.trim().is_empty() { DEFAULT_VAULT_NAME.to_string() } else { self.vault_setup_name.trim().to_string() }; let mut reg = VaultRegistry { vaults: Vec::new(), active: vault_path.clone(), }; // If the path already has a DB, add as existing; otherwise create new. if vault_path.join("audiofiles.db").exists() { if let Err(e) = vault::add_existing_vault(&mut reg, &name, &vault_path) { tracing::error!("Failed to add existing vault: {e}"); } } else if let Err(e) = vault::create_vault(&mut reg, &name, &vault_path) { tracing::error!("Failed to create vault: {e}"); } if let Err(e) = vault::save_registry(®) { tracing::error!("Failed to save vault registry: {e}"); } self.vault_registry = Some(reg); self.data_dir = vault_path; self.activate_browser(); } /// Switch to a different vault: tear down the current browser and rebuild. pub(crate) fn switch_vault(&mut self, path: PathBuf) { if !vault::is_vault_reachable(&path) { if let Some(ref mut browser) = self.browser { browser.status = format!("Vault is offline: {}", path.display()); } return; } // Tear down current state self.midi_connection = None; if let Some(ref mut browser) = self.browser { browser.stop_preview(); } self.browser = None; self.sync_manager = None; // Update registry if let Some(ref mut reg) = self.vault_registry { reg.active = path.clone(); if let Err(e) = vault::save_registry(reg) { tracing::error!("Failed to save vault registry: {e}"); } } self.data_dir = path; self.activate_browser(); // Populate vault_list on the new browser self.sync_vault_list_to_browser(); } /// Run a vault registry mutation, save the registry, and sync the vault list /// to the browser. Returns `true` if the operation and save both succeeded. pub(crate) fn with_vault_registry(&mut self, op: F) -> bool where F: FnOnce(&mut VaultRegistry) -> Result<(), vault::VaultError>, { let Some(ref mut reg) = self.vault_registry else { return false }; if let Err(e) = op(reg) { if let Some(ref mut b) = self.browser { b.status = format!("Vault error: {e}"); } return false; } if let Err(e) = vault::save_registry(reg) { tracing::error!("Failed to save vault registry: {e}"); return false; } self.sync_vault_list_to_browser(); true } /// Push the current registry's vault list into the browser state. pub(crate) fn sync_vault_list_to_browser(&mut self) { if let (Some(reg), Some(browser)) = (&self.vault_registry, &mut self.browser) { browser.settings.list = reg .vaults .iter() .map(|v| (v.name.clone(), v.path.clone(), vault::is_vault_reachable(&v.path))) .collect(); browser.settings.name = vault_name_for_path(reg, &self.data_dir); } } } /// One-time migration: copy license.json and machine_id from the default vault /// directory to the global config directory if they don't exist there yet. pub(crate) fn migrate_license_to_config(config_dir: &Path, default_vault: &Path) { // Skip if config_dir and default_vault are the same (macOS) if config_dir == default_vault { return; } let _ = std::fs::create_dir_all(config_dir); for filename in &["license.json", "machine_id"] { let src = default_vault.join(filename); let dst = config_dir.join(filename); if src.exists() && !dst.exists() { if let Err(e) = std::fs::copy(&src, &dst) { tracing::warn!("Failed to migrate {filename} to config_dir: {e}"); } else { tracing::info!("Migrated {filename} from {} to {}", src.display(), dst.display()); } } } } /// Look up the vault name for a given path in the registry. Falls back to "Library". /// Uses canonicalize for consistent matching regardless of symlinks or relative components. pub(crate) fn vault_name_for_path(reg: &VaultRegistry, path: &Path) -> String { let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); reg.vaults .iter() .find(|v| v.path.canonicalize().unwrap_or_else(|_| v.path.clone()) == canonical) .map(|v| v.name.clone()) .unwrap_or_else(|| "Library".to_string()) }