botserver/src/ui_tree/file_tree.rs

266 lines
6 KiB
Rust
Raw Normal View History

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, name: String },
File { bucket: String, path: String, name: String },
}
pub struct FileTree {
app_state: Arc<AppState>,
items: Vec<(String, TreeNode)>,
selected: usize,
current_bucket: Option<String>,
current_path: Vec<String>,
}
impl FileTree {
pub fn new(app_state: Arc<AppState>) -> 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(),
name: "..".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<String> = 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,
name: folder_name,
}));
}
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,
name,
}));
}
}
if self.items.len() == 1 {
self.items.push(("(empty folder)".to_string(), TreeNode::Folder {
bucket: bucket.to_string(),
path: String::new(),
name: 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<String> {
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;
}
}
}