redesign: file tree always left, transfer area bottom, bookmarks, history
This commit is contained in:
parent
4e637535f0
commit
9cb8e47a97
7 changed files with 287 additions and 151 deletions
110
src/ui/app.rs
110
src/ui/app.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use egui::{CentralPanel, Style, TopBottomPanel, Visuals};
|
use egui::{CentralPanel, SidePanel, Style, TopBottomPanel, Visuals};
|
||||||
|
|
||||||
use crate::domain::connection::ConnectionStatus;
|
use crate::domain::connection::ConnectionStatus;
|
||||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||||
|
|
@ -14,6 +14,8 @@ use crate::ui::panels::remote_pane;
|
||||||
use crate::ui::panels::status_bar;
|
use crate::ui::panels::status_bar;
|
||||||
use crate::ui::panels::tabs;
|
use crate::ui::panels::tabs;
|
||||||
use crate::ui::panels::toolbar;
|
use crate::ui::panels::toolbar;
|
||||||
|
use crate::ui::panels::tree;
|
||||||
|
use crate::ui::panels::transfer_area;
|
||||||
|
|
||||||
pub struct FileManagerApp {
|
pub struct FileManagerApp {
|
||||||
pub state: crate::ui::state::AppState,
|
pub state: crate::ui::state::AppState,
|
||||||
|
|
@ -72,7 +74,7 @@ impl FileManagerApp {
|
||||||
let tab = crate::ui::state::ConnectionTab {
|
let tab = crate::ui::state::ConnectionTab {
|
||||||
id: params.id.clone(),
|
id: params.id.clone(),
|
||||||
label: params.label.clone(),
|
label: params.label.clone(),
|
||||||
params,
|
params: params.clone(),
|
||||||
status: ConnectionStatus::Connected,
|
status: ConnectionStatus::Connected,
|
||||||
remote_path: path,
|
remote_path: path,
|
||||||
remote_entries: list,
|
remote_entries: list,
|
||||||
|
|
@ -84,10 +86,7 @@ impl FileManagerApp {
|
||||||
self.state.status_message = "Connected".into();
|
self.state.status_message = "Connected".into();
|
||||||
self.state.connect_loading = false;
|
self.state.connect_loading = false;
|
||||||
self.state.show_connect_dialog = false;
|
self.state.show_connect_dialog = false;
|
||||||
self.state.onboarding_host.clear();
|
self.state.add_history(¶ms.host, params.port, ¶ms.username);
|
||||||
self.state.onboarding_user.clear();
|
|
||||||
self.state.onboarding_pass.clear();
|
|
||||||
self.state.onboarding_port.clear();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.state.status_message = format!("Connection failed: {}", e);
|
self.state.status_message = format!("Connection failed: {}", e);
|
||||||
|
|
@ -151,13 +150,11 @@ impl eframe::App for FileManagerApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
if self.first_frame {
|
if self.first_frame {
|
||||||
self.first_frame = false;
|
self.first_frame = false;
|
||||||
|
|
||||||
let style = Style {
|
let style = Style {
|
||||||
visuals: Visuals::dark(),
|
visuals: Visuals::dark(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
ctx.set_style(style);
|
ctx.set_style(style);
|
||||||
|
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,13 +167,20 @@ impl eframe::App for FileManagerApp {
|
||||||
.filter(|t| t.status == ConnectionStatus::Connected)
|
.filter(|t| t.status == ConnectionStatus::Connected)
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
|
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
||||||
|
toolbar::render(ui, &mut self.state);
|
||||||
|
});
|
||||||
|
|
||||||
TopBottomPanel::top("tabs").show(ctx, |ui| {
|
TopBottomPanel::top("tabs").show(ctx, |ui| {
|
||||||
tabs::render(ui, &mut self.state);
|
tabs::render(ui, &mut self.state);
|
||||||
});
|
});
|
||||||
|
|
||||||
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
SidePanel::left("tree_panel")
|
||||||
toolbar::render(ui, &mut self.state);
|
.resizable(true)
|
||||||
});
|
.min_width(180.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
tree::render(ui, &mut self.state);
|
||||||
|
});
|
||||||
|
|
||||||
let has_connection = self.state.active_tab_ref().is_some()
|
let has_connection = self.state.active_tab_ref().is_some()
|
||||||
&& self.state.active_tab_ref().unwrap().status == ConnectionStatus::Connected;
|
&& self.state.active_tab_ref().unwrap().status == ConnectionStatus::Connected;
|
||||||
|
|
@ -209,78 +213,13 @@ impl eframe::App for FileManagerApp {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
CentralPanel::default().show(ctx, |ui| {
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
let available = ui.available_height();
|
local_pane::render(
|
||||||
ui.vertical_centered(|ui| {
|
ui,
|
||||||
ui.add_space(available * 0.15);
|
&mut self.state,
|
||||||
|
&self.queue,
|
||||||
ui.heading("LoFlum");
|
&self.registry,
|
||||||
ui.label("FTP/SFTP client");
|
self.rt.handle(),
|
||||||
ui.add_space(24.0);
|
|
||||||
|
|
||||||
let card_frame = egui::Frame::window(&ctx.style());
|
|
||||||
card_frame.show(ui, |ui| {
|
|
||||||
ui.set_min_width(320.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label("Connect to a server");
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Host:");
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut self.state.onboarding_host)
|
|
||||||
.id("onb_host".into())
|
|
||||||
.desired_width(150.0)
|
|
||||||
.hint_text("hostname"),
|
|
||||||
);
|
);
|
||||||
ui.label("Port:");
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut self.state.onboarding_port)
|
|
||||||
.id("onb_port".into())
|
|
||||||
.desired_width(60.0)
|
|
||||||
.char_limit(5)
|
|
||||||
.hint_text("port"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("User:");
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut self.state.onboarding_user)
|
|
||||||
.id("onb_user".into())
|
|
||||||
.desired_width(200.0)
|
|
||||||
.hint_text("username"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Pass:");
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::singleline(&mut self.state.onboarding_pass)
|
|
||||||
.password(true)
|
|
||||||
.id("onb_pass".into())
|
|
||||||
.desired_width(200.0)
|
|
||||||
.hint_text("password"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(12.0);
|
|
||||||
let conn_btn = egui::Button::new("Connect")
|
|
||||||
.min_size(egui::vec2(200.0, 32.0));
|
|
||||||
if ui.add(conn_btn).clicked() {
|
|
||||||
self.state.connect_host = self.state.onboarding_host.clone();
|
|
||||||
self.state.connect_user = self.state.onboarding_user.clone();
|
|
||||||
self.state.connect_pass = self.state.onboarding_pass.clone();
|
|
||||||
self.state.connect_port = if self.state.onboarding_port.is_empty() {
|
|
||||||
"22".into()
|
|
||||||
} else {
|
|
||||||
self.state.onboarding_port.clone()
|
|
||||||
};
|
|
||||||
self.state.connect_protocol = 0;
|
|
||||||
self.state.show_connect_dialog = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,6 +234,13 @@ impl eframe::App for FileManagerApp {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TopBottomPanel::bottom("transfer_area")
|
||||||
|
.default_height(40.0)
|
||||||
|
.min_height(30.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
transfer_area::render(ui, &mut self.state);
|
||||||
|
});
|
||||||
|
|
||||||
TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
|
TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
|
||||||
status_bar::render(ui, &mut self.state);
|
status_bar::render(ui, &mut self.state);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,31 +35,9 @@ pub fn render(
|
||||||
registry: &Arc<RemoteRegistry>,
|
registry: &Arc<RemoteRegistry>,
|
||||||
_rt_handle: &tokio::runtime::Handle,
|
_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 frame = egui::Frame::none().inner_margin(egui::Margin::symmetric(4.0, 2.0));
|
||||||
let (_inner, dropped) = ui.dnd_drop_zone::<DragPayload, _>(frame, |ui| {
|
let (_inner, dropped) = ui.dnd_drop_zone::<DragPayload, _>(frame, |ui| {
|
||||||
if state.local_tree_open {
|
render_content(ui, state);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(payload_arc) = dropped {
|
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) {
|
fn render_content(ui: &mut egui::Ui, state: &mut AppState) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("\u{1F4C1}");
|
||||||
let mut path = state.local_path.clone();
|
let mut path = state.local_path.clone();
|
||||||
ui.add(
|
ui.add(
|
||||||
TextEdit::singleline(&mut path)
|
TextEdit::singleline(&mut path)
|
||||||
|
|
@ -174,6 +117,19 @@ fn render_content(ui: &mut egui::Ui, state: &mut AppState) {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(format!("{} items", state.local_entries.len()));
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,5 @@ pub mod remote_pane;
|
||||||
pub mod status_bar;
|
pub mod status_bar;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
pub mod toolbar;
|
pub mod toolbar;
|
||||||
|
pub mod transfer_area;
|
||||||
|
pub mod tree;
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,61 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
|
||||||
if ui.button("+ New Connection").clicked() {
|
if ui.button("+ New Connection").clicked() {
|
||||||
state.show_connect_dialog = true;
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/ui/panels/transfer_area.rs
Normal file
13
src/ui/panels/transfer_area.rs
Normal 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
126
src/ui/panels/tree.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::domain::connection::{ConnectionParams, ConnectionStatus};
|
use crate::domain::connection::{ConnectionParams, ConnectionStatus};
|
||||||
|
|
@ -27,18 +28,34 @@ pub struct PendingRemoteList {
|
||||||
pub result: PendingResult<Vec<FileEntry>>,
|
pub result: PendingResult<Vec<FileEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Bookmark {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HistoryEntry {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub user: String,
|
||||||
|
pub time: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub tabs: Vec<ConnectionTab>,
|
pub tabs: Vec<ConnectionTab>,
|
||||||
pub active_tab: usize,
|
pub active_tab: usize,
|
||||||
pub local_path: String,
|
pub local_path: String,
|
||||||
pub local_entries: Vec<FileEntry>,
|
pub local_entries: Vec<FileEntry>,
|
||||||
pub local_selected: Option<String>,
|
pub local_selected: Option<String>,
|
||||||
pub local_tree_open: bool,
|
|
||||||
pub show_connect_dialog: bool,
|
pub show_connect_dialog: bool,
|
||||||
pub onboarding_host: String,
|
pub show_bookmarks: bool,
|
||||||
pub onboarding_user: String,
|
pub show_history: bool,
|
||||||
pub onboarding_pass: String,
|
pub bookmarks: Vec<Bookmark>,
|
||||||
pub onboarding_port: String,
|
pub history: Vec<HistoryEntry>,
|
||||||
|
pub tree_expanded: HashMap<String, bool>,
|
||||||
|
pub tree_children: HashMap<String, Vec<FileEntry>>,
|
||||||
|
pub tree_loading: HashMap<String, bool>,
|
||||||
pub connect_label: String,
|
pub connect_label: String,
|
||||||
pub connect_host: String,
|
pub connect_host: String,
|
||||||
pub connect_port: String,
|
pub connect_port: String,
|
||||||
|
|
@ -68,12 +85,18 @@ impl Default for AppState {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
local_entries: Vec::new(),
|
local_entries: Vec::new(),
|
||||||
local_selected: None,
|
local_selected: None,
|
||||||
local_tree_open: false,
|
|
||||||
show_connect_dialog: false,
|
show_connect_dialog: false,
|
||||||
onboarding_host: String::new(),
|
show_bookmarks: false,
|
||||||
onboarding_user: String::new(),
|
show_history: false,
|
||||||
onboarding_pass: String::new(),
|
bookmarks: vec![
|
||||||
onboarding_port: String::new(),
|
Bookmark { name: "Home".into(), path: dirs::home_dir().unwrap_or_default().to_string_lossy().to_string() },
|
||||||
|
Bookmark { name: "Root".into(), path: "/".into() },
|
||||||
|
Bookmark { name: "Downloads".into(), path: dirs::download_dir().map(|p| p.to_string_lossy().to_string()).unwrap_or_else(|| "/tmp".into()) },
|
||||||
|
],
|
||||||
|
history: Vec::new(),
|
||||||
|
tree_expanded: HashMap::new(),
|
||||||
|
tree_children: HashMap::new(),
|
||||||
|
tree_loading: HashMap::new(),
|
||||||
connect_label: String::new(),
|
connect_label: String::new(),
|
||||||
connect_host: String::new(),
|
connect_host: String::new(),
|
||||||
connect_port: "22".into(),
|
connect_port: "22".into(),
|
||||||
|
|
@ -112,4 +135,18 @@ impl AppState {
|
||||||
Some(&self.tabs[idx])
|
Some(&self.tabs[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_history(&mut self, host: &str, port: u16, user: &str) {
|
||||||
|
use chrono::Local;
|
||||||
|
let now = Local::now().format("%H:%M %d.%m").to_string();
|
||||||
|
self.history.insert(0, HistoryEntry {
|
||||||
|
host: host.into(),
|
||||||
|
port,
|
||||||
|
user: user.into(),
|
||||||
|
time: now,
|
||||||
|
});
|
||||||
|
if self.history.len() > 20 {
|
||||||
|
self.history.truncate(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue