Skip to main content

max / audiofiles

11.2 KB · 296 lines History Blame Raw
1 use tracing::{error, warn};
2
3 use super::*;
4
5 impl BrowserState {
6 /// Database ID of the currently active VFS, or `None` if the list is empty.
7 pub fn current_vfs_id(&self) -> Option<VfsId> {
8 self.vfs_list.get(self.current_vfs_idx).map(|v| v.id)
9 }
10
11 /// Reload the child node list and apply current sort/search.
12 pub fn refresh_contents(&mut self) {
13 // In similarity mode, contents are managed by find_similar() — skip normal refresh.
14 if self.similarity_search_hash.is_some() {
15 return;
16 }
17
18 let vfs_id = match self.current_vfs_id() {
19 Some(id) => id,
20 None => {
21 self.contents = Arc::new(Vec::new());
22 return;
23 }
24 };
25
26 if self.search_filter.is_active() || !self.search_query.is_empty() {
27 let mut filter = self.search_filter.clone();
28 filter.text_query = self.search_query.clone();
29 match filter.scope {
30 audiofiles_core::search::SearchScope::CurrentFolder => {
31 match self.backend.search_in_folder(&filter, vfs_id, self.current_dir) {
32 Ok(results) => self.contents = Arc::new(results),
33 Err(e) => {
34 error!("Search failed: {e}");
35 self.status = "Search error".to_string();
36 self.contents = Arc::new(Vec::new());
37 }
38 }
39 }
40 audiofiles_core::search::SearchScope::Global => {
41 match self.backend.search_global(&filter) {
42 Ok(results) => self.contents = Arc::new(results),
43 Err(e) => {
44 error!("Global search failed: {e}");
45 self.status = "Search error".to_string();
46 self.contents = Arc::new(Vec::new());
47 }
48 }
49 }
50 }
51 } else {
52 match self.backend.list_children_enriched(vfs_id, self.current_dir) {
53 Ok(nodes) => self.contents = Arc::new(nodes),
54 Err(e) => {
55 error!("Failed to list directory: {e}");
56 self.status = "Failed to load contents".to_string();
57 self.contents = Arc::new(Vec::new());
58 }
59 }
60 }
61
62 self.sort_contents();
63 self.refresh_selected_tags();
64 self.mark_mirror_dirty();
65 }
66
67 /// Apply current search query and filters.
68 pub fn apply_search(&mut self) {
69 self.selection.clear();
70 self.refresh_contents();
71 }
72
73 /// Sort contents by the current sort column and direction.
74 pub fn sort_contents(&mut self) {
75 // Directories always first
76 Arc::make_mut(&mut self.contents).sort_by(|a, b| {
77 let a_is_dir = a.node.node_type == NodeType::Directory;
78 let b_is_dir = b.node.node_type == NodeType::Directory;
79 if a_is_dir != b_is_dir {
80 return b_is_dir.cmp(&a_is_dir);
81 }
82
83 let cmp = match self.sort_column {
84 SortColumn::Name => a.node.name.to_lowercase().cmp(&b.node.name.to_lowercase()),
85 SortColumn::Bpm => a.bpm.partial_cmp(&b.bpm).unwrap_or(std::cmp::Ordering::Equal),
86 SortColumn::Key => a.musical_key.cmp(&b.musical_key),
87 SortColumn::Duration => a
88 .duration
89 .partial_cmp(&b.duration)
90 .unwrap_or(std::cmp::Ordering::Equal),
91 SortColumn::Classification => a.classification.cmp(&b.classification),
92 };
93
94 match self.sort_direction {
95 SortDirection::Ascending => cmp,
96 SortDirection::Descending => cmp.reverse(),
97 }
98 });
99 }
100
101 /// Cycle sort for a column: ascending -> descending -> default(name asc).
102 pub fn toggle_sort(&mut self, column: SortColumn) {
103 if self.sort_column == column {
104 match self.sort_direction {
105 SortDirection::Ascending => self.sort_direction = SortDirection::Descending,
106 SortDirection::Descending => {
107 self.sort_column = SortColumn::Name;
108 self.sort_direction = SortDirection::Ascending;
109 }
110 }
111 } else {
112 self.sort_column = column;
113 self.sort_direction = SortDirection::Ascending;
114 }
115 self.sort_contents();
116 }
117
118 /// Reload the tag list for the currently focused sample (shown in the detail panel).
119 pub fn refresh_selected_tags(&mut self) {
120 self.selected_tags = Arc::new(Vec::new());
121 if let Some(node) = self.selected_node()
122 && let Some(hash) = &node.node.sample_hash {
123 self.selected_tags = Arc::new(self.backend.get_sample_tags(hash).unwrap_or_else(|e| {
124 warn!("Failed to load tags: {e}");
125 Vec::new()
126 }));
127 }
128 }
129
130 /// Refresh the detail panel (analysis + waveform) for the currently selected sample.
131 pub fn refresh_selected_detail(&mut self) {
132 self.selected_analysis = None;
133 self.selected_waveform = None;
134
135 if let Some(node) = self.selected_node()
136 && let Some(hash) = &node.node.sample_hash {
137 self.selected_analysis = self.backend.get_analysis(hash)
138 .unwrap_or(None);
139 self.selected_waveform = self.backend.get_waveform(hash)
140 .unwrap_or(None);
141 }
142 }
143
144 /// Whether the file list currently shows a ".." parent-directory entry.
145 fn has_parent_entry(&self) -> bool {
146 self.current_dir.is_some()
147 }
148
149 /// Total number of visible rows: contents + optional ".." parent entry.
150 pub fn visible_len(&self) -> usize {
151 self.contents.len() + if self.has_parent_entry() { 1 } else { 0 }
152 }
153
154 /// Return the VfsNodeWithAnalysis at the current selection focus, or `None` if ".." is selected.
155 pub fn selected_node(&self) -> Option<VfsNodeWithAnalysis> {
156 let focus = self.selection.focus;
157 if self.has_parent_entry() {
158 if focus == 0 {
159 return None; // ".." selected
160 }
161 self.contents.get(focus - 1).cloned()
162 } else {
163 self.contents.get(focus).cloned()
164 }
165 }
166
167 /// Move selection focus down by one row (Down arrow).
168 pub fn select_next(&mut self) {
169 let len = self.visible_len();
170 if len > 0 && self.selection.focus < len - 1 {
171 let next = self.selection.focus + 1;
172 self.selection.set_single(next);
173 self.scroll_to_row = Some(next);
174 self.refresh_selected_tags();
175 self.refresh_selected_detail();
176 }
177 }
178
179 /// Move selection focus up by one row (Up arrow).
180 pub fn select_prev(&mut self) {
181 if self.selection.focus > 0 {
182 let prev = self.selection.focus - 1;
183 self.selection.set_single(prev);
184 self.scroll_to_row = Some(prev);
185 self.refresh_selected_tags();
186 self.refresh_selected_detail();
187 }
188 }
189
190 /// Navigate into the selected directory, or go up if ".." is selected.
191 pub fn enter_directory(&mut self) {
192 self.similarity_search_hash = None;
193 self.similarity_source_name = None;
194
195 if self.has_parent_entry() && self.selection.focus == 0 {
196 self.go_up();
197 return;
198 }
199
200 if let Some(node) = self.selected_node()
201 && node.node.node_type == NodeType::Directory {
202 self.current_dir = Some(node.node.id);
203 self.breadcrumb = self.backend.get_breadcrumb(node.node.id).unwrap_or_else(|e| {
204 warn!("Breadcrumb failed: {e}");
205 Vec::new()
206 });
207 self.selection.clear();
208 self.refresh_contents();
209 }
210 }
211
212 /// Navigate to the parent directory, or do nothing if already at root.
213 /// If a collection is active, exits collection view first.
214 pub fn go_up(&mut self) {
215 self.similarity_search_hash = None;
216 self.similarity_source_name = None;
217 if self.active_collection.is_some() {
218 self.deactivate_collection();
219 return;
220 }
221 if let Some(current) = self.current_dir {
222 if let Ok(node) = self.backend.get_node(current) {
223 self.current_dir = node.parent_id;
224 if let Some(pid) = node.parent_id {
225 self.breadcrumb = self.backend.get_breadcrumb(pid).unwrap_or_else(|e| {
226 warn!("Breadcrumb failed: {e}");
227 Vec::new()
228 });
229 } else {
230 self.breadcrumb.clear();
231 }
232 } else {
233 self.current_dir = None;
234 self.breadcrumb.clear();
235 }
236 self.selection.clear();
237 self.refresh_contents();
238 }
239 }
240
241 /// Switch to a different VFS by index, resetting navigation to its root.
242 pub fn select_vfs(&mut self, idx: usize) {
243 if idx < self.vfs_list.len() && idx != self.current_vfs_idx {
244 self.current_vfs_idx = idx;
245 self.current_dir = None;
246 self.breadcrumb.clear();
247 self.selection.clear();
248 self.similarity_search_hash = None;
249 self.similarity_source_name = None;
250 // Persist the selection by VFS id (not index — indices shift when
251 // vaults are added or removed) so it restores on next launch.
252 let _ = self.backend.set_config(
253 "current_vfs_id",
254 &self.vfs_list[self.current_vfs_idx].id.as_i64().to_string(),
255 );
256 self.refresh_contents();
257 self.refresh_collections();
258 self.status = format!("Switched to: {}", self.vfs_list[self.current_vfs_idx].name);
259 }
260 }
261
262 /// Toggle the left sidebar and persist the choice across restarts.
263 pub fn toggle_sidebar(&mut self) {
264 self.sidebar_visible = !self.sidebar_visible;
265 let _ = self.backend.set_config(
266 "sidebar_visible",
267 if self.sidebar_visible { "1" } else { "0" },
268 );
269 }
270
271 /// Toggle the right detail panel and persist the choice across restarts.
272 pub fn toggle_detail(&mut self) {
273 self.set_detail_visible(!self.detail_visible);
274 }
275
276 /// Set detail-panel visibility and persist it (used by both the explicit
277 /// toggle and the auto-show on selection so the stored state never drifts).
278 pub fn set_detail_visible(&mut self, visible: bool) {
279 if self.detail_visible != visible {
280 self.detail_visible = visible;
281 let _ = self
282 .backend
283 .set_config("detail_visible", if visible { "1" } else { "0" });
284 }
285 }
286
287 /// Toggle the filter panel and persist the choice across restarts.
288 pub fn toggle_filter_panel(&mut self) {
289 self.filter_panel_open = !self.filter_panel_open;
290 let _ = self.backend.set_config(
291 "filter_panel_open",
292 if self.filter_panel_open { "1" } else { "0" },
293 );
294 }
295 }
296