rename src-tauri -> src
This commit is contained in:
parent
b7b70fe22d
commit
500da04ce7
55 changed files with 11 additions and 5277 deletions
100
src/ui/panels/file_pane.rs
Normal file
100
src/ui/panels/file_pane.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||
use crate::ui::drag::DragPayload;
|
||||
use egui::{Grid, Id};
|
||||
|
||||
pub fn format_size(bytes: Option<u64>) -> String {
|
||||
match bytes {
|
||||
None => String::new(),
|
||||
Some(b) if b < 1024 => format!("{} B", b),
|
||||
Some(b) if b < 1024 * 1024 => format!("{:.1} KB", b as f64 / 1024.0),
|
||||
Some(b) if b < 1024 * 1024 * 1024 => {
|
||||
format!("{:.1} MB", b as f64 / (1024.0 * 1024.0))
|
||||
}
|
||||
Some(b) => format!("{:.2} GB", b as f64 / (1024.0 * 1024.0 * 1024.0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_time(ts: Option<i64>) -> String {
|
||||
match ts.and_then(|t| chrono::DateTime::from_timestamp(t, 0)) {
|
||||
Some(dt) => dt.format("%Y-%m-%d %H:%M").to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileTableResponse {
|
||||
pub double_clicked: Option<String>,
|
||||
pub clicked: Option<String>,
|
||||
}
|
||||
|
||||
pub fn file_table<F>(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
entries: &[FileEntry],
|
||||
selected: &mut Option<String>,
|
||||
make_drag_payload: F,
|
||||
) -> FileTableResponse
|
||||
where
|
||||
F: Fn(&FileEntry) -> Option<DragPayload>,
|
||||
{
|
||||
let mut double_clicked: Option<String> = None;
|
||||
let mut clicked: Option<String> = None;
|
||||
|
||||
let mut sorted = entries.to_vec();
|
||||
sorted.sort_by(|a, b| match (&a.kind, &b.kind) {
|
||||
(EntryKind::Dir, EntryKind::Dir) => a.name.cmp(&b.name),
|
||||
(EntryKind::Dir, _) => std::cmp::Ordering::Less,
|
||||
(_, EntryKind::Dir) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
});
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
Grid::new(id)
|
||||
.striped(true)
|
||||
.min_col_width(60.0)
|
||||
.show(ui, |ui| {
|
||||
ui.strong("Name");
|
||||
ui.strong("Size");
|
||||
ui.strong("Type");
|
||||
ui.strong("Modified");
|
||||
ui.end_row();
|
||||
|
||||
for entry in &sorted {
|
||||
let is_selected = selected.as_ref() == Some(&entry.name);
|
||||
|
||||
let response = if let Some(payload) = make_drag_payload(entry) {
|
||||
let drag_id = Id::new(("drag", id, &entry.name));
|
||||
ui.dnd_drag_source(drag_id, payload, |ui| {
|
||||
ui.selectable_label(is_selected, &entry.name)
|
||||
})
|
||||
.response
|
||||
} else {
|
||||
ui.selectable_label(is_selected, &entry.name)
|
||||
};
|
||||
|
||||
if response.clicked() {
|
||||
*selected = Some(entry.name.clone());
|
||||
clicked = Some(entry.name.clone());
|
||||
}
|
||||
if response.double_clicked() {
|
||||
double_clicked = Some(entry.name.clone());
|
||||
}
|
||||
|
||||
ui.label(format_size(entry.size));
|
||||
ui.label(match entry.kind {
|
||||
EntryKind::Dir => "Dir",
|
||||
EntryKind::File => "File",
|
||||
EntryKind::Symlink => "Link",
|
||||
});
|
||||
ui.label(format_time(entry.modified));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
FileTableResponse {
|
||||
double_clicked,
|
||||
clicked,
|
||||
}
|
||||
}
|
||||
194
src/ui/panels/local_pane.rs
Normal file
194
src/ui/panels/local_pane.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
use crate::domain::connection::ConnectionStatus;
|
||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||
use crate::domain::transfer::{TransferKind, TransferTask};
|
||||
use crate::fs::local;
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
use crate::ui::drag::{make_local_payload, DragPayload};
|
||||
use crate::ui::panels::file_pane::{file_table, FileTableResponse};
|
||||
use crate::ui::state::AppState;
|
||||
use egui::TextEdit;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn parent_path(path: &str) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
p.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.to_string())
|
||||
}
|
||||
|
||||
fn make_parent_entry(path: &str) -> FileEntry {
|
||||
FileEntry {
|
||||
name: "..".into(),
|
||||
path: parent_path(path),
|
||||
kind: EntryKind::Dir,
|
||||
size: None,
|
||||
modified: None,
|
||||
permissions: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
queue: &TransferQueue,
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(payload_arc) = dropped {
|
||||
let payload = &*payload_arc;
|
||||
match payload {
|
||||
DragPayload::RemoteFile(remote_path, file_name, conn_id) => {
|
||||
if registry.get(conn_id).is_none() {
|
||||
state.status_message = "Connection no longer active".into();
|
||||
return;
|
||||
}
|
||||
let local_path_str =
|
||||
format!("{}/{}", state.local_path.trim_end_matches('/'), file_name);
|
||||
let task = TransferTask::new(
|
||||
TransferKind::Download,
|
||||
conn_id.clone(),
|
||||
local_path_str,
|
||||
remote_path.clone(),
|
||||
file_name.clone(),
|
||||
0,
|
||||
);
|
||||
queue.push(task);
|
||||
state.status_message = "Download queued via drag & drop".into();
|
||||
}
|
||||
DragPayload::LocalFile(_, _) => {
|
||||
state.status_message = "Cannot drop local file onto local pane".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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| {
|
||||
let mut path = state.local_path.clone();
|
||||
ui.add(
|
||||
TextEdit::singleline(&mut path)
|
||||
.id("local_path".into())
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
if path != state.local_path {
|
||||
state.local_path = path;
|
||||
refresh_local(state);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let entries = state.local_entries.clone();
|
||||
let mut selected = state.local_selected.clone();
|
||||
|
||||
let FileTableResponse { double_clicked, .. } =
|
||||
file_table(ui, "local_table", &entries, &mut selected, |entry| {
|
||||
let has_connection = state
|
||||
.active_tab_ref()
|
||||
.map(|t| t.status == ConnectionStatus::Connected)
|
||||
.unwrap_or(false);
|
||||
if has_connection {
|
||||
make_local_payload(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
state.local_selected = selected;
|
||||
|
||||
if let Some(name) = double_clicked {
|
||||
if name == ".." {
|
||||
state.local_path = parent_path(&state.local_path);
|
||||
} else if let Some(entry) = state.local_entries.iter().find(|e| e.name == name)
|
||||
&& entry.kind == EntryKind::Dir
|
||||
{
|
||||
state.local_path = entry.path.clone();
|
||||
}
|
||||
refresh_local(state);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{} items", state.local_entries.len()));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh_local(state: &mut AppState) {
|
||||
let mut entries = Vec::new();
|
||||
if state.local_path != "/" {
|
||||
entries.push(make_parent_entry(&state.local_path));
|
||||
}
|
||||
match local::list(&state.local_path) {
|
||||
Ok(mut list) => {
|
||||
entries.append(&mut list);
|
||||
state.local_entries = entries;
|
||||
}
|
||||
Err(e) => {
|
||||
state.status_message = format!("Local list error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/ui/panels/mod.rs
Normal file
7
src/ui/panels/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod file_pane;
|
||||
pub mod local_pane;
|
||||
pub mod queue;
|
||||
pub mod remote_pane;
|
||||
pub mod status_bar;
|
||||
pub mod tabs;
|
||||
pub mod toolbar;
|
||||
83
src/ui/panels/queue.rs
Normal file
83
src/ui/panels/queue.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use crate::domain::transfer::TransferTask;
|
||||
use crate::ui::panels::file_pane::format_size;
|
||||
use crate::ui::state::AppState;
|
||||
use egui::Color32;
|
||||
|
||||
pub fn render(ui: &mut egui::Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("\u{1F4E6} Transfer Queue");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.small_button("\u{2715}").clicked() {
|
||||
state.show_queue = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if tasks.is_empty() {
|
||||
ui.label("No pending transfers");
|
||||
return;
|
||||
}
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.max_height(200.0)
|
||||
.show(ui, |ui| {
|
||||
for task in tasks {
|
||||
let bg = match &task.state {
|
||||
crate::domain::transfer::TaskState::Completed => {
|
||||
Color32::from_rgb(220, 255, 220)
|
||||
}
|
||||
crate::domain::transfer::TaskState::Failed(_) => {
|
||||
Color32::from_rgb(255, 220, 220)
|
||||
}
|
||||
crate::domain::transfer::TaskState::Running => Color32::from_rgb(220, 220, 255),
|
||||
_ => Color32::TRANSPARENT,
|
||||
};
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(bg)
|
||||
.inner_margin(egui::Margin::symmetric(4.0, 2.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let arrow = match &task.kind {
|
||||
crate::domain::transfer::TransferKind::Upload => "\u{2191}",
|
||||
crate::domain::transfer::TransferKind::Download => "\u{2193}",
|
||||
};
|
||||
ui.label(arrow);
|
||||
ui.label(&task.file_name);
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.label(task.state.to_string());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let pct = task.progress_pct();
|
||||
let pb = egui::ProgressBar::new(pct as f32 / 100.0)
|
||||
.text(format!("{:.1}%", pct))
|
||||
.desired_width(ui.available_width() * 0.6);
|
||||
ui.add(pb);
|
||||
|
||||
ui.label(format!(
|
||||
"{}/{}",
|
||||
format_size(Some(task.transferred_bytes)),
|
||||
format_size(Some(task.total_bytes))
|
||||
));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(speed) = task.speed {
|
||||
ui.label(format!("{}/s", format_size(Some(speed))));
|
||||
}
|
||||
if let Some(eta) = task.eta_secs {
|
||||
ui.label(format!("ETA: {}s", eta));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
215
src/ui/panels/remote_pane.rs
Normal file
215
src/ui/panels/remote_pane.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
use crate::domain::connection::ConnectionStatus;
|
||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||
use crate::domain::transfer::{TransferKind, TransferTask};
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
use crate::ui::drag::{make_remote_payload, DragPayload};
|
||||
use crate::ui::panels::file_pane::{file_table, FileTableResponse};
|
||||
use crate::ui::state::{AppState, PendingRemoteList};
|
||||
use egui::TextEdit;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn remote_parent(path: &str) -> String {
|
||||
let trimmed = path.trim_end_matches('/');
|
||||
if trimmed.is_empty() || trimmed == "/" {
|
||||
return "/".into();
|
||||
}
|
||||
if let Some(idx) = trimmed.rfind('/') {
|
||||
let parent = &trimmed[..idx];
|
||||
if parent.is_empty() {
|
||||
"/".into()
|
||||
} else {
|
||||
parent.to_string()
|
||||
}
|
||||
} else {
|
||||
"/".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
tab_idx: usize,
|
||||
registry: &Arc<RemoteRegistry>,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
queue: &TransferQueue,
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("\u{1F310} Remote");
|
||||
});
|
||||
|
||||
let frame = egui::Frame::none().inner_margin(egui::Margin::symmetric(4.0, 2.0));
|
||||
let (_inner, dropped) = ui.dnd_drop_zone::<DragPayload, _>(frame, |ui| {
|
||||
render_content(ui, state, tab_idx, registry, rt_handle);
|
||||
});
|
||||
|
||||
let is_connected = state.tabs[tab_idx].status == ConnectionStatus::Connected;
|
||||
let connection_id = state.tabs[tab_idx].id.clone();
|
||||
let remote_path = state.tabs[tab_idx].remote_path.clone();
|
||||
|
||||
if let Some(payload_arc) = dropped {
|
||||
let payload = &*payload_arc;
|
||||
match payload {
|
||||
DragPayload::LocalFile(local_path, file_name) => {
|
||||
if !is_connected {
|
||||
state.status_message = "No active connection".into();
|
||||
return;
|
||||
}
|
||||
let remote_path_str =
|
||||
format!("{}/{}", remote_path.trim_end_matches('/'), file_name);
|
||||
let task = TransferTask::new(
|
||||
TransferKind::Upload,
|
||||
connection_id.clone(),
|
||||
local_path.clone(),
|
||||
remote_path_str,
|
||||
file_name.clone(),
|
||||
0,
|
||||
);
|
||||
queue.push(task);
|
||||
state.status_message = "Upload queued via drag & drop".into();
|
||||
}
|
||||
DragPayload::RemoteFile(_, _, _) => {
|
||||
state.status_message = "Cannot drop remote file onto remote pane".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
tab_idx: usize,
|
||||
registry: &Arc<RemoteRegistry>,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
) {
|
||||
let label = state.tabs[tab_idx].label.clone();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("\u{25CF} {}", label));
|
||||
if state.tabs[tab_idx].loading {
|
||||
ui.label("loading...");
|
||||
}
|
||||
});
|
||||
|
||||
let path_changed;
|
||||
{
|
||||
let tab = &state.tabs[tab_idx];
|
||||
let mut path = tab.remote_path.clone();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
TextEdit::singleline(&mut path)
|
||||
.id("remote_path".into())
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
});
|
||||
path_changed = path != tab.remote_path;
|
||||
if path_changed {
|
||||
state.tabs[tab_idx].remote_path = path;
|
||||
}
|
||||
}
|
||||
|
||||
if path_changed {
|
||||
let loading = state.tabs[tab_idx].loading;
|
||||
if !loading {
|
||||
trigger_list(state, tab_idx, registry, rt_handle);
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
let entries;
|
||||
let mut selected;
|
||||
{
|
||||
let tab = &state.tabs[tab_idx];
|
||||
entries = tab.remote_entries.clone();
|
||||
selected = tab.remote_selected.clone();
|
||||
}
|
||||
|
||||
let is_connected = state.tabs[tab_idx].status == ConnectionStatus::Connected;
|
||||
let conn_id = state.tabs[tab_idx].id.clone();
|
||||
|
||||
let table_id = format!("remote_table_{}", tab_idx);
|
||||
let FileTableResponse { double_clicked, .. } =
|
||||
file_table(ui, &table_id, &entries, &mut selected, move |entry| {
|
||||
if is_connected {
|
||||
make_remote_payload(entry, &conn_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
state.tabs[tab_idx].remote_selected = selected;
|
||||
|
||||
if let Some(name) = double_clicked {
|
||||
let (nav_path, is_dir) = {
|
||||
if name == ".." {
|
||||
let parent = remote_parent(state.tabs[tab_idx].remote_path.as_str());
|
||||
(parent, true)
|
||||
} else {
|
||||
let entry = state.tabs[tab_idx]
|
||||
.remote_entries
|
||||
.iter()
|
||||
.find(|e| e.name == name);
|
||||
match entry {
|
||||
Some(e) if e.kind == EntryKind::Dir => (e.path.clone(), true),
|
||||
_ => (state.tabs[tab_idx].remote_path.clone(), false),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if is_dir {
|
||||
state.tabs[tab_idx].remote_path = nav_path;
|
||||
let loading = state.tabs[tab_idx].loading;
|
||||
if !loading {
|
||||
trigger_list(state, tab_idx, registry, rt_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
{
|
||||
let tab = &state.tabs[tab_idx];
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{} items", tab.remote_entries.len()));
|
||||
if tab.loading {
|
||||
ui.label(" loading...");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_list(
|
||||
state: &mut AppState,
|
||||
tab_idx: usize,
|
||||
registry: &Arc<RemoteRegistry>,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
) {
|
||||
if state.tabs[tab_idx].status != ConnectionStatus::Connected {
|
||||
state.status_message = "Not connected".into();
|
||||
return;
|
||||
}
|
||||
state.tabs[tab_idx].loading = true;
|
||||
|
||||
let connection_id = state.tabs[tab_idx].id.clone();
|
||||
let path = state.tabs[tab_idx].remote_path.clone();
|
||||
let registry = registry.clone();
|
||||
|
||||
let result: Arc<std::sync::Mutex<Option<Result<Vec<FileEntry>, String>>>> =
|
||||
Arc::new(std::sync::Mutex::new(None));
|
||||
let result_clone = result.clone();
|
||||
|
||||
rt_handle.spawn(async move {
|
||||
let fs = match registry.get(&connection_id) {
|
||||
Some(fs) => fs,
|
||||
None => {
|
||||
*result_clone.lock().unwrap() = Some(Err("connection not found".into()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let r = fs.list(&path).await.map_err(|e| e.to_string());
|
||||
*result_clone.lock().unwrap() = Some(r);
|
||||
});
|
||||
|
||||
state
|
||||
.pending_remote_list
|
||||
.push(PendingRemoteList { tab_idx, result });
|
||||
}
|
||||
20
src/ui/panels/status_bar.rs
Normal file
20
src/ui/panels/status_bar.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use crate::domain::connection::ConnectionStatus;
|
||||
use crate::ui::state::AppState;
|
||||
|
||||
pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&state.status_message);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let queued = state.queue_tasks.len();
|
||||
ui.label(format!("Queue: {}", queued));
|
||||
ui.separator();
|
||||
let connected = state
|
||||
.tabs
|
||||
.iter()
|
||||
.filter(|t| t.status == ConnectionStatus::Connected)
|
||||
.count();
|
||||
let total = state.tabs.len();
|
||||
ui.label(format!("Connections: {}/{}", connected, total));
|
||||
});
|
||||
});
|
||||
}
|
||||
55
src/ui/panels/tabs.rs
Normal file
55
src/ui/panels/tabs.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use crate::domain::connection::ConnectionStatus;
|
||||
use crate::ui::state::AppState;
|
||||
|
||||
pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
|
||||
ui.horizontal(|ui| {
|
||||
if state.tabs.is_empty() {
|
||||
ui.label("No connections");
|
||||
if ui.button("+ New Connection").clicked() {
|
||||
state.show_connect_dialog = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut to_close: Option<usize> = None;
|
||||
|
||||
for (i, tab) in state.tabs.iter().enumerate() {
|
||||
let is_active = i == state.active_tab;
|
||||
|
||||
let dot = match &tab.status {
|
||||
ConnectionStatus::Connected => "\u{25CF}",
|
||||
ConnectionStatus::Disconnected => "\u{25CB}",
|
||||
ConnectionStatus::Connecting => "\u{25D0}",
|
||||
ConnectionStatus::Error(_) => "\u{2716}",
|
||||
};
|
||||
|
||||
let label = format!("{} {}", dot, tab.label);
|
||||
|
||||
let mut tab_button = egui::Button::new(&label);
|
||||
if is_active {
|
||||
tab_button = tab_button.fill(egui::Color32::from_rgb(60, 60, 80));
|
||||
}
|
||||
let response = ui.add(tab_button);
|
||||
if response.clicked() {
|
||||
state.active_tab = i;
|
||||
}
|
||||
|
||||
if ui.small_button("\u{2715}").clicked() {
|
||||
to_close = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = to_close {
|
||||
state.tabs.remove(idx);
|
||||
if state.active_tab >= state.tabs.len() && !state.tabs.is_empty() {
|
||||
state.active_tab = state.tabs.len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("+").clicked() {
|
||||
state.show_connect_dialog = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
139
src/ui/panels/toolbar.rs
Normal file
139
src/ui/panels/toolbar.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::domain::connection::ConnectionStatus;
|
||||
use crate::domain::file_entry::EntryKind;
|
||||
use crate::domain::transfer::{TransferKind, TransferTask};
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
use crate::ui::state::AppState;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn render(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
queue: &TransferQueue,
|
||||
registry: &Arc<RemoteRegistry>,
|
||||
_rt_handle: &tokio::runtime::Handle,
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
let add_btn = egui::Button::new("+ New Connection");
|
||||
if ui.add(add_btn).clicked() {
|
||||
state.show_connect_dialog = true;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
let has_local_selection = state.local_selected.is_some();
|
||||
let has_remote_selection = state
|
||||
.active_tab_ref()
|
||||
.and_then(|t| t.remote_selected.as_ref())
|
||||
.is_some();
|
||||
let (is_connected, tab_exists) = {
|
||||
let active = state.active_tab_ref();
|
||||
(
|
||||
active
|
||||
.map(|t| t.status == ConnectionStatus::Connected)
|
||||
.unwrap_or(false),
|
||||
active.is_some(),
|
||||
)
|
||||
};
|
||||
|
||||
let tab_id = state.active_tab_ref().map(|t| t.id.clone());
|
||||
let remote_path = state.active_tab_ref().map(|t| t.remote_path.clone());
|
||||
|
||||
let upload_btn = egui::Button::new("\u{2191} Upload")
|
||||
.min_size(egui::vec2(100.0, 24.0));
|
||||
ui.add_enabled_ui(is_connected && has_local_selection && tab_exists, |ui| {
|
||||
if ui.add(upload_btn).clicked()
|
||||
&& let Some(sel) = state.local_selected.clone()
|
||||
&& let Some(entry) = state.local_entries.iter().find(|e| e.name == sel)
|
||||
{
|
||||
if entry.kind == EntryKind::File {
|
||||
let file_name = entry.name.clone();
|
||||
let remote_base = remote_path.clone().unwrap_or_else(|| "/".into());
|
||||
let remote_path_str =
|
||||
format!("{}/{}", remote_base.trim_end_matches('/'), file_name);
|
||||
let cid = tab_id.clone().unwrap_or_default();
|
||||
let task = TransferTask::new(
|
||||
TransferKind::Upload,
|
||||
cid,
|
||||
entry.path.clone(),
|
||||
remote_path_str,
|
||||
file_name,
|
||||
entry.size.unwrap_or(0),
|
||||
);
|
||||
if registry.get(&task.connection_id).is_some() {
|
||||
queue.push(task);
|
||||
state.status_message = "Upload queued".into();
|
||||
} else {
|
||||
state.status_message = "No connection".into();
|
||||
}
|
||||
} else {
|
||||
state.status_message = "Cannot upload directory".into();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let download_btn = egui::Button::new("\u{2193} Download")
|
||||
.min_size(egui::vec2(100.0, 24.0));
|
||||
ui.add_enabled_ui(is_connected && has_remote_selection && tab_exists, |ui| {
|
||||
if ui.add(download_btn).clicked()
|
||||
&& let Some(sel) = tab_remote_selected(state)
|
||||
{
|
||||
let file_name = sel.clone();
|
||||
let remote_base = remote_path.clone().unwrap_or_else(|| "/".into());
|
||||
let remote_path_str =
|
||||
format!("{}/{}", remote_base.trim_end_matches('/'), file_name);
|
||||
let local_path_str =
|
||||
format!("{}/{}", state.local_path.trim_end_matches('/'), file_name);
|
||||
let size = state
|
||||
.active_tab_ref()
|
||||
.map(|t| {
|
||||
t.remote_entries
|
||||
.iter()
|
||||
.find(|e| e.name == sel)
|
||||
.and_then(|e| e.size)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let cid = tab_id.clone().unwrap_or_default();
|
||||
let task = TransferTask::new(
|
||||
TransferKind::Download,
|
||||
cid,
|
||||
local_path_str,
|
||||
remote_path_str,
|
||||
file_name,
|
||||
size,
|
||||
);
|
||||
if registry.get(&task.connection_id).is_some() {
|
||||
queue.push(task);
|
||||
state.status_message = "Download queued".into();
|
||||
} else {
|
||||
state.status_message = "No connection".into();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let queue_label = if state.show_queue {
|
||||
"Queue \u{25BC}"
|
||||
} else {
|
||||
"Queue \u{25B2}"
|
||||
};
|
||||
if ui.button(queue_label).clicked() {
|
||||
state.show_queue = !state.show_queue;
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.separator();
|
||||
let task_count = state.queue_tasks.len();
|
||||
if task_count > 0 {
|
||||
ui.label(format!("{} tasks", task_count));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn tab_remote_selected(state: &AppState) -> Option<String> {
|
||||
let idx = state.active_tab.min(state.tabs.len().saturating_sub(1));
|
||||
state.tabs.get(idx).and_then(|t| t.remote_selected.clone())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue