Skip to main content

max / audiofiles

9.1 KB · 226 lines History Blame Raw
1 //! Vault setup screen, vault switching, and registry management.
2
3 use std::path::{Path, PathBuf};
4
5 use audiofiles_browser::ui::{theme, widgets};
6 use audiofiles_core::vault::{self, VaultRegistry};
7 use eframe::egui;
8
9 use super::AudioFilesApp;
10
11 const DEFAULT_VAULT_NAME: &str = "Library";
12
13 impl AudioFilesApp {
14 /// Draw the vault setup screen (first-open flow, after activation).
15 pub(crate) fn draw_vault_setup_screen(&mut self, ui: &mut egui::Ui) {
16 let default_path = vault::default_vault_path();
17 let existing_db = default_path.join("audiofiles.db").exists();
18
19 // Pre-fill the vault-name field on first entry so the fallback is visible.
20 if self.vault_setup_name.is_empty() {
21 self.vault_setup_name = DEFAULT_VAULT_NAME.to_string();
22 }
23
24 egui::CentralPanel::default().show_inside(ui, |ui| {
25 let available = ui.available_size();
26 ui.add_space((available.y * 0.20).max(40.0));
27
28 ui.vertical_centered(|ui| {
29 let column_width = 480.0_f32.min(available.x - 40.0);
30 ui.allocate_ui(egui::vec2(column_width, 0.0), |ui| {
31 ui.vertical(|ui| {
32 ui.heading("Where should we store your library?");
33 ui.add_space(theme::space::MD);
34
35 if existing_db && self.vault_setup_path.is_none() {
36 ui.label(
37 egui::RichText::new(format!(
38 "Your existing library at {} will be opened.",
39 default_path.display()
40 ))
41 .color(theme::text_secondary()),
42 );
43 ui.add_space(theme::space::SECTION);
44 } else {
45 ui.add_space(theme::space::SM);
46 }
47
48 // Vault name
49 ui.horizontal(|ui| {
50 ui.label("Name:");
51 ui.text_edit_singleline(&mut self.vault_setup_name);
52 });
53
54 ui.add_space(theme::space::MD);
55
56 // Location: shown inline with the picker action. No
57 // separate "Use default location" button — the default
58 // is already the value displayed, and "Continue"
59 // commits whatever is shown here.
60 let chosen_path = self
61 .vault_setup_path
62 .clone()
63 .unwrap_or_else(|| default_path.clone());
64 let is_custom = self.vault_setup_path.is_some();
65 ui.label("Location:");
66 ui.horizontal_wrapped(|ui| {
67 let path_text = if is_custom {
68 widgets::accent_strong(chosen_path.display().to_string())
69 } else {
70 egui::RichText::new(chosen_path.display().to_string())
71 .color(theme::text_secondary())
72 };
73 ui.label(path_text);
74 });
75 ui.add_space(theme::space::SM);
76 ui.horizontal(|ui| {
77 if ui.button("Choose different location...").clicked()
78 && let Some(path) = rfd::FileDialog::new().pick_folder() {
79 self.vault_setup_path = Some(path);
80 }
81 if is_custom && ui.button("Use default").clicked() {
82 self.vault_setup_path = None;
83 }
84 });
85
86 ui.add_space(theme::space::XL);
87
88 // Single primary action — the only path that advances.
89 if widgets::primary_button(ui, "Continue").clicked() {
90 self.finalize_vault_setup(chosen_path);
91 }
92 });
93 });
94 });
95 });
96 }
97
98 /// Finalise vault setup: create registry, set data_dir, open browser.
99 fn finalize_vault_setup(&mut self, vault_path: PathBuf) {
100 let name = if self.vault_setup_name.trim().is_empty() {
101 DEFAULT_VAULT_NAME.to_string()
102 } else {
103 self.vault_setup_name.trim().to_string()
104 };
105
106 let mut reg = VaultRegistry {
107 vaults: Vec::new(),
108 active: vault_path.clone(),
109 };
110
111 // If the path already has a DB, add as existing; otherwise create new.
112 if vault_path.join("audiofiles.db").exists() {
113 if let Err(e) = vault::add_existing_vault(&mut reg, &name, &vault_path) {
114 tracing::error!("Failed to add existing vault: {e}");
115 }
116 } else if let Err(e) = vault::create_vault(&mut reg, &name, &vault_path) {
117 tracing::error!("Failed to create vault: {e}");
118 }
119
120 if let Err(e) = vault::save_registry(&reg) {
121 tracing::error!("Failed to save vault registry: {e}");
122 }
123
124 self.vault_registry = Some(reg);
125 self.data_dir = vault_path;
126 self.activate_browser();
127 }
128
129 /// Switch to a different vault: tear down the current browser and rebuild.
130 pub(crate) fn switch_vault(&mut self, path: PathBuf) {
131 if !vault::is_vault_reachable(&path) {
132 if let Some(ref mut browser) = self.browser {
133 browser.status = format!("Vault is offline: {}", path.display());
134 }
135 return;
136 }
137
138 // Tear down current state
139 self.midi_connection = None;
140 if let Some(ref mut browser) = self.browser {
141 browser.stop_preview();
142 }
143 self.browser = None;
144 self.sync_manager = None;
145
146 // Update registry
147 if let Some(ref mut reg) = self.vault_registry {
148 reg.active = path.clone();
149 if let Err(e) = vault::save_registry(reg) {
150 tracing::error!("Failed to save vault registry: {e}");
151 }
152 }
153
154 self.data_dir = path;
155 self.activate_browser();
156
157 // Populate vault_list on the new browser
158 self.sync_vault_list_to_browser();
159 }
160
161 /// Run a vault registry mutation, save the registry, and sync the vault list
162 /// to the browser. Returns `true` if the operation and save both succeeded.
163 pub(crate) fn with_vault_registry<F>(&mut self, op: F) -> bool
164 where
165 F: FnOnce(&mut VaultRegistry) -> Result<(), vault::VaultError>,
166 {
167 let Some(ref mut reg) = self.vault_registry else { return false };
168 if let Err(e) = op(reg) {
169 if let Some(ref mut b) = self.browser {
170 b.status = format!("Vault error: {e}");
171 }
172 return false;
173 }
174 if let Err(e) = vault::save_registry(reg) {
175 tracing::error!("Failed to save vault registry: {e}");
176 return false;
177 }
178 self.sync_vault_list_to_browser();
179 true
180 }
181
182 /// Push the current registry's vault list into the browser state.
183 pub(crate) fn sync_vault_list_to_browser(&mut self) {
184 if let (Some(reg), Some(browser)) = (&self.vault_registry, &mut self.browser) {
185 browser.settings.list = reg
186 .vaults
187 .iter()
188 .map(|v| (v.name.clone(), v.path.clone(), vault::is_vault_reachable(&v.path)))
189 .collect();
190 browser.settings.name = vault_name_for_path(reg, &self.data_dir);
191 }
192 }
193 }
194
195 /// One-time migration: copy license.json and machine_id from the default vault
196 /// directory to the global config directory if they don't exist there yet.
197 pub(crate) fn migrate_license_to_config(config_dir: &Path, default_vault: &Path) {
198 // Skip if config_dir and default_vault are the same (macOS)
199 if config_dir == default_vault {
200 return;
201 }
202 let _ = std::fs::create_dir_all(config_dir);
203 for filename in &["license.json", "machine_id"] {
204 let src = default_vault.join(filename);
205 let dst = config_dir.join(filename);
206 if src.exists() && !dst.exists() {
207 if let Err(e) = std::fs::copy(&src, &dst) {
208 tracing::warn!("Failed to migrate {filename} to config_dir: {e}");
209 } else {
210 tracing::info!("Migrated {filename} from {} to {}", src.display(), dst.display());
211 }
212 }
213 }
214 }
215
216 /// Look up the vault name for a given path in the registry. Falls back to "Library".
217 /// Uses canonicalize for consistent matching regardless of symlinks or relative components.
218 pub(crate) fn vault_name_for_path(reg: &VaultRegistry, path: &Path) -> String {
219 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
220 reg.vaults
221 .iter()
222 .find(|v| v.path.canonicalize().unwrap_or_else(|_| v.path.clone()) == canonical)
223 .map(|v| v.name.clone())
224 .unwrap_or_else(|| "Library".to_string())
225 }
226