redesign: file tree always left, transfer area bottom, bookmarks, history

This commit is contained in:
loki5512344 2026-06-15 21:43:10 +02:00
parent 4e637535f0
commit 9cb8e47a97
7 changed files with 287 additions and 151 deletions

View file

@ -35,31 +35,9 @@ pub fn render(
registry: &Arc<RemoteRegistry>,
_rt_handle: &tokio::runtime::Handle,
) {
ui.horizontal(|ui| {
ui.heading("\u{1F4C1} Local");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("\u{1F5C4}").clicked() {
state.local_tree_open = !state.local_tree_open;
}
});
});
let frame = egui::Frame::none().inner_margin(egui::Margin::symmetric(4.0, 2.0));
let (_inner, dropped) = ui.dnd_drop_zone::<DragPayload, _>(frame, |ui| {
if state.local_tree_open {
ui.horizontal(|ui| {
ui.separator();
ui.vertical(|ui| {
render_tree(ui, state);
});
ui.separator();
ui.vertical(|ui| {
render_content(ui, state);
});
});
} else {
render_content(ui, state);
}
render_content(ui, state);
});
if let Some(payload_arc) = dropped {
@ -90,44 +68,9 @@ pub fn render(
}
}
fn render_tree(ui: &mut egui::Ui, state: &mut AppState) {
let home = dirs::home_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.min_scrolled_width(120.0)
.show(ui, |ui| {
ui.strong("Folders");
ui.separator();
let roots = [
("/", "Root"),
(&home, "Home"),
("/tmp", "tmp"),
("/var", "var"),
("/etc", "etc"),
];
for (root_path, label) in &roots {
let is_current = state.local_path == *root_path
|| state.local_path.starts_with(root_path);
let mut btn = egui::Button::new(*label).min_size(egui::vec2(120.0, 20.0));
if is_current {
btn = btn.fill(egui::Color32::from_rgb(50, 60, 80));
}
if ui.add(btn).clicked() {
state.local_path = root_path.to_string();
refresh_local(state);
}
}
});
}
fn render_content(ui: &mut egui::Ui, state: &mut AppState) {
ui.horizontal(|ui| {
ui.label("\u{1F4C1}");
let mut path = state.local_path.clone();
ui.add(
TextEdit::singleline(&mut path)
@ -174,6 +117,19 @@ fn render_content(ui: &mut egui::Ui, state: &mut AppState) {
ui.separator();
ui.horizontal(|ui| {
ui.label(format!("{} items", state.local_entries.len()));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("\u{2B50}").clicked() {
let name = std::path::Path::new(&state.local_path)
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "Bookmark".into());
state.bookmarks.push(crate::ui::state::Bookmark {
name,
path: state.local_path.clone(),
});
state.status_message = "Bookmark added".into();
}
});
});
}

View file

@ -5,3 +5,5 @@ pub mod remote_pane;
pub mod status_bar;
pub mod tabs;
pub mod toolbar;
pub mod transfer_area;
pub mod tree;

View file

@ -5,5 +5,61 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
if ui.button("+ New Connection").clicked() {
state.show_connect_dialog = true;
}
ui.separator();
if ui.button("Bookmarks").clicked() {
state.show_bookmarks = !state.show_bookmarks;
state.show_history = false;
}
if ui.button("History").clicked() {
state.show_history = !state.show_history;
state.show_bookmarks = false;
}
if state.show_bookmarks {
egui::ComboBox::from_id_source("bookmarks_menu")
.selected_text("Bookmarks")
.width(150.0)
.show_ui(ui, |ui| {
for bm in &state.bookmarks.clone() {
if ui.button(&bm.name).clicked() {
state.local_path = bm.path.clone();
crate::ui::panels::local_pane::refresh_local(state);
state.show_bookmarks = false;
}
}
});
}
if state.show_history {
egui::ComboBox::from_id_source("history_menu")
.selected_text("History")
.width(200.0)
.show_ui(ui, |ui| {
if state.history.is_empty() {
ui.label("No history yet");
} else {
for entry in &state.history.clone() {
let label = format!("{}@{}:{} [{}]", entry.user, entry.host, entry.port, entry.time);
if ui.button(&label).clicked() {
state.connect_host = entry.host.clone();
state.connect_user = entry.user.clone();
state.connect_port = entry.port.to_string();
state.show_connect_dialog = true;
state.show_history = false;
}
}
}
});
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let queued = state.queue_tasks.len();
if queued > 0 {
ui.label(format!("Queue: {}", queued));
}
});
});
}

View file

@ -0,0 +1,13 @@
use crate::ui::state::AppState;
pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
ui.horizontal(|ui| {
ui.label("\u{1F4E4} Transfer area");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let queued = state.queue_tasks.len();
if queued > 0 {
ui.label(format!("{} files", queued));
}
});
});
}

126
src/ui/panels/tree.rs Normal file
View file

@ -0,0 +1,126 @@
use crate::domain::file_entry::{EntryKind, FileEntry};
use crate::fs::local;
use crate::ui::state::AppState;
pub fn refresh_tree(state: &mut AppState, path: &str) {
if state.tree_children.contains_key(path) {
return;
}
state.tree_loading.insert(path.to_string(), true);
let entries = local::list(path).unwrap_or_default();
let dirs: Vec<FileEntry> = entries
.into_iter()
.filter(|e| e.kind == EntryKind::Dir)
.collect();
state.tree_children.insert(path.to_string(), dirs);
state.tree_loading.remove(path);
}
pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.strong("Local Files");
ui.separator();
let home = dirs::home_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let roots = [("/", "Root"), (&home, "Home"), ("/tmp", "tmp")];
for (root_path, label) in &roots {
render_tree_node(ui, state, root_path, label, 0);
}
ui.separator();
ui.strong("Bookmarks");
for bm in &state.bookmarks.clone() {
let is_current = state.local_path == bm.path
|| state.local_path.starts_with(&bm.path);
let mut btn = egui::Button::new(&bm.name).min_size(egui::vec2(140.0, 20.0));
if is_current {
btn = btn.fill(egui::Color32::from_rgb(50, 55, 60));
}
if ui.add(btn).clicked() {
state.local_path = bm.path.clone();
crate::ui::panels::local_pane::refresh_local(state);
}
}
});
}
fn render_tree_node(
ui: &mut egui::Ui,
state: &mut AppState,
path: &str,
label: &str,
depth: usize,
) {
let is_current = state.local_path == path
|| (path != "/"
&& state.local_path.starts_with(path)
&& state.local_path.as_str()[path.len()..].starts_with('/'));
let expanded = state.tree_expanded.get(path).copied().unwrap_or(false);
let has_children = state.tree_children.contains_key(path);
ui.horizontal(|ui| {
ui.add_space(depth as f32 * 16.0);
let arrow = if has_children {
if expanded {
"\u{25BC}"
} else {
"\u{25B6}"
}
} else {
" "
};
let arrow_resp = ui.selectable_label(false, arrow);
if arrow_resp.clicked() && has_children {
if expanded {
state.tree_expanded.insert(path.to_string(), false);
} else {
state.tree_expanded.insert(path.to_string(), true);
if !state.tree_children.contains_key(path) {
refresh_tree(state, path);
}
}
}
let icon = if expanded {
"\u{1F4C2}"
} else {
"\u{1F4C1}"
};
let mut btn = egui::Button::new(format!("{} {}", icon, label))
.min_size(egui::vec2(120.0, 20.0));
if is_current {
btn = btn.fill(egui::Color32::from_rgb(50, 55, 60));
}
if ui.add(btn).clicked() {
state.local_path = path.to_string();
crate::ui::panels::local_pane::refresh_local(state);
}
});
if expanded {
let children = state.tree_children.get(path).cloned().unwrap_or_default();
let mut dirs: Vec<_> = children.iter().collect();
dirs.sort_by(|a, b| a.name.cmp(&b.name));
for child in &dirs {
let child_path = child.path.clone();
let child_label = child.name.clone();
if !state.tree_children.contains_key(&child_path) {
refresh_tree(state, &child_path);
}
render_tree_node(ui, state, &child_path, &child_label, depth + 1);
}
}
}