use color_eyre::Result; use std::sync::Arc; use crate::shared::state::AppState; #[derive(Debug, Clone)] pub enum TreeNode { Bucket { name: String }, Folder { bucket: String, path: String }, File { bucket: String, path: String }, } pub struct FileTree { app_state: Arc, items: Vec<(String, TreeNode)>, selected: usize, current_bucket: Option, current_path: Vec, } impl FileTree { pub fn new(app_state: Arc) -> Self { Self { app_state, items: Vec::new(), selected: 0, current_bucket: None, current_path: Vec::new(), } } pub async fn load_root(&mut self) -> Result<()> { self.items.clear(); self.current_bucket = None; self.current_path.clear(); if let Some(drive) = &self.app_state.drive { let result = drive.list_buckets().send().await; match result { Ok(response) => { let buckets = response.buckets(); for bucket in buckets { if let Some(name) = bucket.name() { let icon = if name.ends_with(".gbai") { "🤖" } else { "📦" }; let display = format!("{} {}", icon, name); self.items.push((display, TreeNode::Bucket { name: name.to_string() })); } } } Err(e) => { self.items.push((format!("✗ Error: {}", e), TreeNode::Bucket { name: String::new() })); } } } else { self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() })); } if self.items.is_empty() { self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() })); } self.selected = 0; Ok(()) } pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> { self.current_bucket = Some(bucket.clone()); self.current_path.clear(); self.load_bucket_contents(&bucket, "").await } pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> { self.current_bucket = Some(bucket.clone()); let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect(); self.current_path = parts.iter().map(|s| s.to_string()).collect(); self.load_bucket_contents(&bucket, &path).await } pub fn go_up(&mut self) -> bool { if self.current_path.is_empty() { if self.current_bucket.is_some() { self.current_bucket = None; return true; } return false; } self.current_path.pop(); true } pub async fn refresh_current(&mut self) -> Result<()> { if let Some(bucket) = &self.current_bucket.clone() { let path = self.current_path.join("/"); self.load_bucket_contents(bucket, &path).await } else { self.load_root().await } } async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> { self.items.clear(); self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder { bucket: bucket.to_string(), path: "..".to_string(), })); if let Some(drive) = &self.app_state.drive { let normalized_prefix = if prefix.is_empty() { String::new() } else if prefix.ends_with('/') { prefix.to_string() } else { format!("{}/", prefix) }; let mut continuation_token = None; let mut all_keys = Vec::new(); loop { let mut request = drive.list_objects_v2().bucket(bucket); if !normalized_prefix.is_empty() { request = request.prefix(&normalized_prefix); } if let Some(token) = continuation_token { request = request.continuation_token(token); } let result = request.send().await?; for obj in result.contents() { if let Some(key) = obj.key() { all_keys.push(key.to_string()); } } if !result.is_truncated.unwrap_or(false) { break; } continuation_token = result.next_continuation_token; } let mut folders = std::collections::HashSet::new(); let mut files = Vec::new(); for key in all_keys { if key == normalized_prefix { continue; } let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) { &key[normalized_prefix.len()..] } else { &key }; if relative.is_empty() { continue; } if let Some(slash_pos) = relative.find('/') { let folder_name = &relative[..slash_pos]; if !folder_name.is_empty() { folders.insert(folder_name.to_string()); } } else { files.push((relative.to_string(), key.clone())); } } let mut folder_vec: Vec = folders.into_iter().collect(); folder_vec.sort(); for folder_name in folder_vec { let full_path = if normalized_prefix.is_empty() { folder_name.clone() } else { format!("{}{}", normalized_prefix, folder_name) }; let display = format!("📁 {}/", folder_name); self.items.push((display, TreeNode::Folder { bucket: bucket.to_string(), path: full_path, })); } files.sort_by(|(a, _), (b, _)| a.cmp(b)); for (name, full_path) in files { let icon = if name.ends_with(".bas") { "⚙️" } else if name.ends_with(".ast") { "🔧" } else if name.ends_with(".csv") { "📊" } else if name.ends_with(".gbkb") { "📚" } else if name.ends_with(".json") { "🔖" } else { "📄" }; let display = format!("{} {}", icon, name); self.items.push((display, TreeNode::File { bucket: bucket.to_string(), path: full_path, })); } } if self.items.len() == 1 { self.items.push(("(empty folder)".to_string(), TreeNode::Folder { bucket: bucket.to_string(), path: String::new(), })); } self.selected = 0; Ok(()) } pub fn render_items(&self) -> &[(String, TreeNode)] { &self.items } pub fn selected_index(&self) -> usize { self.selected } pub fn get_selected_node(&self) -> Option<&TreeNode> { self.items.get(self.selected).map(|(_, node)| node) } pub fn get_selected_bot(&self) -> Option { if let Some(bucket) = &self.current_bucket { if bucket.ends_with(".gbai") { return Some(bucket.trim_end_matches(".gbai").to_string()); } } if let Some((_, node)) = self.items.get(self.selected) { match node { TreeNode::Bucket { name } => { if name.ends_with(".gbai") { return Some(name.trim_end_matches(".gbai").to_string()); } } _ => {} } } None } pub fn move_up(&mut self) { if self.selected > 0 { self.selected -= 1; } } pub fn move_down(&mut self) { if self.selected < self.items.len().saturating_sub(1) { self.selected += 1; } } }