chore: apply clippy fixes and cargo fmt
- Fix 5 clippy warnings: collapsible ifs, useless format - Format all source files with cargo fmt
This commit is contained in:
parent
f983f12bec
commit
42c147a13e
12 changed files with 509 additions and 298 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use egui::{Color32, CentralPanel, RichText, SidePanel, TopBottomPanel, Visuals};
|
use egui::{CentralPanel, Color32, RichText, SidePanel, 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};
|
||||||
|
|
@ -8,9 +8,7 @@ use crate::domain::site::Site;
|
||||||
use crate::fs::remote::RemoteRegistry;
|
use crate::fs::remote::RemoteRegistry;
|
||||||
use crate::transfer::queue::TransferQueue;
|
use crate::transfer::queue::TransferQueue;
|
||||||
use crate::ui::dialogs::connection;
|
use crate::ui::dialogs::connection;
|
||||||
use crate::ui::panels::{
|
use crate::ui::panels::{local_pane, queue, remote_pane, sidebar, status_bar, tabs, toolbar};
|
||||||
local_pane, queue, remote_pane, sidebar, status_bar, tabs, toolbar,
|
|
||||||
};
|
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
|
||||||
pub struct FileManagerApp {
|
pub struct FileManagerApp {
|
||||||
|
|
@ -83,7 +81,8 @@ impl FileManagerApp {
|
||||||
self.state.status_message = format!("Connected to {}", params.host);
|
self.state.status_message = format!("Connected to {}", params.host);
|
||||||
self.state.connect_loading = false;
|
self.state.connect_loading = false;
|
||||||
self.state.show_connect_dialog = false;
|
self.state.show_connect_dialog = false;
|
||||||
self.state.add_history(¶ms.host, params.port, ¶ms.username);
|
self.state
|
||||||
|
.add_history(¶ms.host, params.port, ¶ms.username);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.state.status_message = format!("Connection failed: {}", e);
|
self.state.status_message = format!("Connection failed: {}", e);
|
||||||
|
|
@ -152,7 +151,9 @@ impl FileManagerApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_tab_idx(&self) -> Option<usize> {
|
fn active_tab_idx(&self) -> Option<usize> {
|
||||||
if self.state.tabs.is_empty() { return None; }
|
if self.state.tabs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let idx = self.state.active_tab.min(self.state.tabs.len() - 1);
|
let idx = self.state.active_tab.min(self.state.tabs.len() - 1);
|
||||||
if self.state.tabs[idx].status == ConnectionStatus::Connected {
|
if self.state.tabs[idx].status == ConnectionStatus::Connected {
|
||||||
Some(idx)
|
Some(idx)
|
||||||
|
|
@ -234,7 +235,11 @@ impl eframe::App for FileManagerApp {
|
||||||
QUEUE_COLLAPSED_H
|
QUEUE_COLLAPSED_H
|
||||||
};
|
};
|
||||||
TopBottomPanel::bottom("queue_panel")
|
TopBottomPanel::bottom("queue_panel")
|
||||||
.frame(egui::Frame::none().fill(BG_QUEUE).stroke(egui::Stroke::new(1.0, BORDER)))
|
.frame(
|
||||||
|
egui::Frame::none()
|
||||||
|
.fill(BG_QUEUE)
|
||||||
|
.stroke(egui::Stroke::new(1.0, BORDER)),
|
||||||
|
)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.exact_height(q_h)
|
.exact_height(q_h)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
|
|
@ -243,9 +248,11 @@ impl eframe::App for FileManagerApp {
|
||||||
|
|
||||||
// ── Sidebar ───────────────────────────────────────────────────────────
|
// ── Sidebar ───────────────────────────────────────────────────────────
|
||||||
SidePanel::left("sidebar")
|
SidePanel::left("sidebar")
|
||||||
.frame(egui::Frame::none()
|
.frame(
|
||||||
|
egui::Frame::none()
|
||||||
.fill(BG_PANEL)
|
.fill(BG_PANEL)
|
||||||
.stroke(egui::Stroke::new(1.0, BORDER)))
|
.stroke(egui::Stroke::new(1.0, BORDER)),
|
||||||
|
)
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
.default_width(SIDEBAR_W)
|
.default_width(SIDEBAR_W)
|
||||||
.width_range(120.0..=260.0)
|
.width_range(120.0..=260.0)
|
||||||
|
|
@ -295,10 +302,7 @@ impl eframe::App for FileManagerApp {
|
||||||
let idx = self.state.active_tab.min(self.state.tabs.len() - 1);
|
let idx = self.state.active_tab.min(self.state.tabs.len() - 1);
|
||||||
|
|
||||||
// Хедер панели
|
// Хедер панели
|
||||||
let label = self.state.tabs[idx]
|
let label = self.state.tabs[idx].params.host.clone();
|
||||||
.params
|
|
||||||
.host
|
|
||||||
.clone();
|
|
||||||
pane_header(ui, &format!("REMOTE · {}", label), half);
|
pane_header(ui, &format!("REMOTE · {}", label), half);
|
||||||
|
|
||||||
remote_pane::render(
|
remote_pane::render(
|
||||||
|
|
@ -313,9 +317,7 @@ impl eframe::App for FileManagerApp {
|
||||||
// Empty state
|
// Empty state
|
||||||
pane_header(ui, "REMOTE", half);
|
pane_header(ui, "REMOTE", half);
|
||||||
ui.with_layout(
|
ui.with_layout(
|
||||||
egui::Layout::centered_and_justified(
|
egui::Layout::centered_and_justified(egui::Direction::TopDown),
|
||||||
egui::Direction::TopDown,
|
|
||||||
),
|
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(ui.available_height() * 0.3);
|
ui.add_space(ui.available_height() * 0.3);
|
||||||
|
|
@ -327,7 +329,9 @@ impl eframe::App for FileManagerApp {
|
||||||
);
|
);
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new("Connect to a server to browse files")
|
RichText::new(
|
||||||
|
"Connect to a server to browse files",
|
||||||
|
)
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(12.0),
|
.size(12.0),
|
||||||
);
|
);
|
||||||
|
|
@ -365,16 +369,16 @@ impl eframe::App for FileManagerApp {
|
||||||
fn pane_header(ui: &mut egui::Ui, label: &str, width: f32) {
|
fn pane_header(ui: &mut egui::Ui, label: &str, width: f32) {
|
||||||
egui::Frame::none()
|
egui::Frame::none()
|
||||||
.fill(BG_BASE)
|
.fill(BG_BASE)
|
||||||
.inner_margin(egui::Margin { left: 10.0, right: 6.0, top: 4.0, bottom: 4.0 })
|
.inner_margin(egui::Margin {
|
||||||
|
left: 10.0,
|
||||||
|
right: 6.0,
|
||||||
|
top: 4.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
})
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.set_width(width);
|
ui.set_width(width);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(RichText::new(label).color(TEXT_DIM).size(10.0).strong());
|
||||||
RichText::new(label)
|
|
||||||
.color(TEXT_DIM)
|
|
||||||
.size(10.0)
|
|
||||||
.strong(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||||
use crate::domain::connection::{ConnectionParams, Protocol};
|
use crate::domain::connection::{ConnectionParams, Protocol};
|
||||||
use crate::domain::file_entry::FileEntry;
|
use crate::domain::file_entry::FileEntry;
|
||||||
use crate::fs::remote::RemoteRegistry;
|
use crate::fs::remote::RemoteRegistry;
|
||||||
use crate::protocols::{ftp::FtpClient, sftp::SftpClient, RemoteFs};
|
use crate::protocols::{RemoteFs, ftp::FtpClient, sftp::SftpClient};
|
||||||
use crate::storage::keychain;
|
use crate::storage::keychain;
|
||||||
use crate::ui::state::{AppState, PendingConnect};
|
use crate::ui::state::{AppState, PendingConnect};
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
|
@ -31,7 +31,8 @@ pub fn render(
|
||||||
.order(egui::Order::Background)
|
.order(egui::Order::Background)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
let r = ui.allocate_rect(screen, egui::Sense::click());
|
let r = ui.allocate_rect(screen, egui::Sense::click());
|
||||||
ui.painter().rect_filled(screen, 0.0, Color32::from_black_alpha(160));
|
ui.painter()
|
||||||
|
.rect_filled(screen, 0.0, Color32::from_black_alpha(160));
|
||||||
if r.clicked() && !state.connect_loading {
|
if r.clicked() && !state.connect_loading {
|
||||||
state.show_connect_dialog = false;
|
state.show_connect_dialog = false;
|
||||||
}
|
}
|
||||||
|
|
@ -57,9 +58,15 @@ pub fn render(
|
||||||
|
|
||||||
// Заголовок
|
// Заголовок
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("New Connection").color(TEXT_PRIMARY).size(16.0).strong());
|
ui.label(
|
||||||
|
RichText::new("New Connection")
|
||||||
|
.color(TEXT_PRIMARY)
|
||||||
|
.size(16.0)
|
||||||
|
.strong(),
|
||||||
|
);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let close = egui::Button::new(RichText::new("×").color(TEXT_HINT).size(14.0))
|
let close =
|
||||||
|
egui::Button::new(RichText::new("×").color(TEXT_HINT).size(14.0))
|
||||||
.fill(Color32::TRANSPARENT);
|
.fill(Color32::TRANSPARENT);
|
||||||
if ui.add(close).clicked() {
|
if ui.add(close).clicked() {
|
||||||
state.show_connect_dialog = false;
|
state.show_connect_dialog = false;
|
||||||
|
|
@ -73,7 +80,11 @@ pub fn render(
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for (i, p) in protocols.iter().enumerate() {
|
for (i, p) in protocols.iter().enumerate() {
|
||||||
let active = state.connect_protocol == i;
|
let active = state.connect_protocol == i;
|
||||||
let bg = if active { ACCENT } else { Color32::from_rgb(40, 40, 44) };
|
let bg = if active {
|
||||||
|
ACCENT
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(40, 40, 44)
|
||||||
|
};
|
||||||
let tc = if active { Color32::WHITE } else { TEXT_DIM };
|
let tc = if active { Color32::WHITE } else { TEXT_DIM };
|
||||||
let btn = egui::Button::new(RichText::new(*p).color(tc).size(12.0))
|
let btn = egui::Button::new(RichText::new(*p).color(tc).size(12.0))
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
|
|
@ -90,7 +101,13 @@ pub fn render(
|
||||||
|
|
||||||
// Поля ввода
|
// Поля ввода
|
||||||
let field_w = ui.available_width();
|
let field_w = ui.available_width();
|
||||||
field(ui, "Label (optional)", &mut state.connect_label, field_w, false);
|
field(
|
||||||
|
ui,
|
||||||
|
"Label (optional)",
|
||||||
|
&mut state.connect_label,
|
||||||
|
field_w,
|
||||||
|
false,
|
||||||
|
);
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -108,10 +125,18 @@ pub fn render(
|
||||||
if state.connect_protocol == 0 {
|
if state.connect_protocol == 0 {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
field(ui, "Key path", &mut state.connect_key_path, field_w - 80.0 - 8.0, false);
|
field(
|
||||||
|
ui,
|
||||||
|
"Key path",
|
||||||
|
&mut state.connect_key_path,
|
||||||
|
field_w - 80.0 - 8.0,
|
||||||
|
false,
|
||||||
|
);
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
// Browse placeholder — open system picker if rfd added
|
// Browse placeholder — open system picker if rfd added
|
||||||
let browse = egui::Button::new(RichText::new("Browse").color(TEXT_DIM).size(11.0))
|
let browse = egui::Button::new(
|
||||||
|
RichText::new("Browse").color(TEXT_DIM).size(11.0),
|
||||||
|
)
|
||||||
.fill(Color32::from_rgb(40, 40, 44))
|
.fill(Color32::from_rgb(40, 40, 44))
|
||||||
.min_size(egui::vec2(72.0, 32.0))
|
.min_size(egui::vec2(72.0, 32.0))
|
||||||
.rounding(4.0);
|
.rounding(4.0);
|
||||||
|
|
@ -135,7 +160,8 @@ pub fn render(
|
||||||
|
|
||||||
// Кнопки
|
// Кнопки
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let cancel = egui::Button::new(RichText::new("Cancel").color(TEXT_DIM).size(12.0))
|
let cancel =
|
||||||
|
egui::Button::new(RichText::new("Cancel").color(TEXT_DIM).size(12.0))
|
||||||
.fill(Color32::from_rgb(40, 40, 44))
|
.fill(Color32::from_rgb(40, 40, 44))
|
||||||
.rounding(6.0)
|
.rounding(6.0)
|
||||||
.min_size(egui::vec2(90.0, 34.0));
|
.min_size(egui::vec2(90.0, 34.0));
|
||||||
|
|
@ -151,7 +177,10 @@ pub fn render(
|
||||||
"Connect"
|
"Connect"
|
||||||
};
|
};
|
||||||
let connect_btn = egui::Button::new(
|
let connect_btn = egui::Button::new(
|
||||||
RichText::new(connect_label).color(Color32::WHITE).size(12.0).strong()
|
RichText::new(connect_label)
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.size(12.0)
|
||||||
|
.strong(),
|
||||||
)
|
)
|
||||||
.fill(ACCENT)
|
.fill(ACCENT)
|
||||||
.rounding(6.0)
|
.rounding(6.0)
|
||||||
|
|
@ -204,11 +233,15 @@ fn do_connect(
|
||||||
let params = ConnectionParams {
|
let params = ConnectionParams {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
label: if state.connect_label.is_empty() {
|
label: if state.connect_label.is_empty() {
|
||||||
format!("{} ({})", state.connect_host, match protocol {
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
state.connect_host,
|
||||||
|
match protocol {
|
||||||
Protocol::Sftp => "SFTP",
|
Protocol::Sftp => "SFTP",
|
||||||
Protocol::Ftp => "FTP",
|
Protocol::Ftp => "FTP",
|
||||||
Protocol::Ftps => "FTPS",
|
Protocol::Ftps => "FTPS",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
state.connect_label.clone()
|
state.connect_label.clone()
|
||||||
},
|
},
|
||||||
|
|
@ -216,8 +249,16 @@ fn do_connect(
|
||||||
host: state.connect_host.clone(),
|
host: state.connect_host.clone(),
|
||||||
port,
|
port,
|
||||||
username: state.connect_user.clone(),
|
username: state.connect_user.clone(),
|
||||||
password: if state.connect_pass.is_empty() { None } else { Some(state.connect_pass.clone()) },
|
password: if state.connect_pass.is_empty() {
|
||||||
key_path: if state.connect_key_path.is_empty() { None } else { Some(state.connect_key_path.clone()) },
|
None
|
||||||
|
} else {
|
||||||
|
Some(state.connect_pass.clone())
|
||||||
|
},
|
||||||
|
key_path: if state.connect_key_path.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(state.connect_key_path.clone())
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let registry = registry.clone();
|
let registry = registry.clone();
|
||||||
|
|
@ -253,7 +294,12 @@ async fn connect_async(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Arc::new(
|
Arc::new(
|
||||||
SftpClient::connect_password(¶ms.host, params.port, ¶ms.username, &password)
|
SftpClient::connect_password(
|
||||||
|
¶ms.host,
|
||||||
|
params.port,
|
||||||
|
¶ms.username,
|
||||||
|
&password,
|
||||||
|
)
|
||||||
.map_err(|e| e.to_string())?,
|
.map_err(|e| e.to_string())?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Переиспользуемая таблица файлов с нормальными колонками
|
//! Переиспользуемая таблица файлов с нормальными колонками
|
||||||
use egui::{Color32, Id, RichText, Ui};
|
|
||||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||||
use crate::ui::drag::DragPayload;
|
use crate::ui::drag::DragPayload;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::{Color32, Id, RichText, Ui};
|
||||||
|
|
||||||
pub fn format_size(bytes: Option<u64>) -> String {
|
pub fn format_size(bytes: Option<u64>) -> String {
|
||||||
match bytes {
|
match bytes {
|
||||||
|
|
@ -22,10 +22,18 @@ pub fn format_time(ts: Option<i64>) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum SortCol { Name, Size, Kind, Modified }
|
pub enum SortCol {
|
||||||
|
Name,
|
||||||
|
Size,
|
||||||
|
Kind,
|
||||||
|
Modified,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum SortDir { Asc, Desc }
|
pub enum SortDir {
|
||||||
|
Asc,
|
||||||
|
Desc,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FileTableResponse {
|
pub struct FileTableResponse {
|
||||||
pub double_clicked: Option<String>,
|
pub double_clicked: Option<String>,
|
||||||
|
|
@ -44,9 +52,9 @@ where
|
||||||
{
|
{
|
||||||
// Сортировка — читаем из egui memory
|
// Сортировка — читаем из egui memory
|
||||||
let sort_id = Id::new((id, "sort"));
|
let sort_id = Id::new((id, "sort"));
|
||||||
let (sort_col, sort_dir) = ui.ctx().data_mut(|d| {
|
let (sort_col, sort_dir) = ui
|
||||||
*d.get_temp_mut_or(sort_id, (SortCol::Name, SortDir::Asc))
|
.ctx()
|
||||||
});
|
.data_mut(|d| *d.get_temp_mut_or(sort_id, (SortCol::Name, SortDir::Asc)));
|
||||||
|
|
||||||
let mut sorted = entries.to_vec();
|
let mut sorted = entries.to_vec();
|
||||||
let dotdot_entry = sorted.iter().position(|e| e.name == "..");
|
let dotdot_entry = sorted.iter().position(|e| e.name == "..");
|
||||||
|
|
@ -60,7 +68,9 @@ where
|
||||||
(_, EntryKind::Dir) => std::cmp::Ordering::Less,
|
(_, EntryKind::Dir) => std::cmp::Ordering::Less,
|
||||||
_ => std::cmp::Ordering::Equal,
|
_ => std::cmp::Ordering::Equal,
|
||||||
};
|
};
|
||||||
if dir_cmp != std::cmp::Ordering::Equal { return dir_cmp; }
|
if dir_cmp != std::cmp::Ordering::Equal {
|
||||||
|
return dir_cmp;
|
||||||
|
}
|
||||||
|
|
||||||
let ord = match sort_col {
|
let ord = match sort_col {
|
||||||
SortCol::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
SortCol::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||||
|
|
@ -68,7 +78,11 @@ where
|
||||||
SortCol::Kind => format!("{:?}", a.kind).cmp(&format!("{:?}", b.kind)),
|
SortCol::Kind => format!("{:?}", a.kind).cmp(&format!("{:?}", b.kind)),
|
||||||
SortCol::Modified => a.modified.unwrap_or(0).cmp(&b.modified.unwrap_or(0)),
|
SortCol::Modified => a.modified.unwrap_or(0).cmp(&b.modified.unwrap_or(0)),
|
||||||
};
|
};
|
||||||
if sort_dir == SortDir::Desc { ord.reverse() } else { ord }
|
if sort_dir == SortDir::Desc {
|
||||||
|
ord.reverse()
|
||||||
|
} else {
|
||||||
|
ord
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(dd) = dotdot {
|
if let Some(dd) = dotdot {
|
||||||
|
|
@ -82,7 +96,12 @@ where
|
||||||
// Заголовки колонок
|
// Заголовки колонок
|
||||||
let header_frame = egui::Frame::none()
|
let header_frame = egui::Frame::none()
|
||||||
.fill(BG_HEADER)
|
.fill(BG_HEADER)
|
||||||
.inner_margin(egui::Margin { left: 6.0, right: 6.0, top: 0.0, bottom: 0.0 });
|
.inner_margin(egui::Margin {
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
header_frame.show(ui, |ui| {
|
header_frame.show(ui, |ui| {
|
||||||
ui.set_min_height(22.0);
|
ui.set_min_height(22.0);
|
||||||
|
|
@ -93,20 +112,34 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let mod_r = sort_header(ui, "Modified", 140.0, sort_col == SortCol::Modified, sort_dir);
|
let mod_r = sort_header(
|
||||||
if mod_r { new_sort = Some(toggle_sort(SortCol::Modified, sort_col, sort_dir)); }
|
ui,
|
||||||
|
"Modified",
|
||||||
|
140.0,
|
||||||
|
sort_col == SortCol::Modified,
|
||||||
|
sort_dir,
|
||||||
|
);
|
||||||
|
if mod_r {
|
||||||
|
new_sort = Some(toggle_sort(SortCol::Modified, sort_col, sort_dir));
|
||||||
|
}
|
||||||
|
|
||||||
let type_r = sort_header(ui, "Type", 40.0, sort_col == SortCol::Kind, sort_dir);
|
let type_r = sort_header(ui, "Type", 40.0, sort_col == SortCol::Kind, sort_dir);
|
||||||
if type_r { new_sort = Some(toggle_sort(SortCol::Kind, sort_col, sort_dir)); }
|
if type_r {
|
||||||
|
new_sort = Some(toggle_sort(SortCol::Kind, sort_col, sort_dir));
|
||||||
|
}
|
||||||
|
|
||||||
let size_r = sort_header(ui, "Size", 70.0, sort_col == SortCol::Size, sort_dir);
|
let size_r = sort_header(ui, "Size", 70.0, sort_col == SortCol::Size, sort_dir);
|
||||||
if size_r { new_sort = Some(toggle_sort(SortCol::Size, sort_col, sort_dir)); }
|
if size_r {
|
||||||
|
new_sort = Some(toggle_sort(SortCol::Size, sort_col, sort_dir));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(s) = new_sort {
|
if let Some(s) = new_sort {
|
||||||
ui.ctx().data_mut(|d| { d.insert_temp(sort_id, s); });
|
ui.ctx().data_mut(|d| {
|
||||||
|
d.insert_temp(sort_id, s);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// разделитель
|
// разделитель
|
||||||
|
|
@ -137,13 +170,18 @@ where
|
||||||
|
|
||||||
let response = egui::Frame::none()
|
let response = egui::Frame::none()
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.inner_margin(egui::Margin { left: 6.0, right: 6.0, top: 0.0, bottom: 0.0 })
|
.inner_margin(egui::Margin {
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
})
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.set_min_height(ROW_H);
|
ui.set_min_height(ROW_H);
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
// иконка + имя
|
// иконка + имя
|
||||||
let name_text = if entry.name == ".." {
|
let name_text = if entry.name == ".." {
|
||||||
format!("⬆ ..")
|
"⬆ ..".to_string()
|
||||||
} else {
|
} else {
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
EntryKind::Dir => format!("📁 {}", entry.name),
|
EntryKind::Dir => format!("📁 {}", entry.name),
|
||||||
|
|
@ -165,9 +203,7 @@ where
|
||||||
|
|
||||||
if let Some(payload) = make_drag_payload(entry) {
|
if let Some(payload) = make_drag_payload(entry) {
|
||||||
ui.dnd_drag_source(row_id, payload, |ui| {
|
ui.dnd_drag_source(row_id, payload, |ui| {
|
||||||
ui.label(
|
ui.label(RichText::new(&name_text).color(name_col).size(12.0));
|
||||||
RichText::new(&name_text).color(name_col).size(12.0)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ui.label(RichText::new(&name_text).color(name_col).size(12.0));
|
ui.label(RichText::new(&name_text).color(name_col).size(12.0));
|
||||||
|
|
@ -190,7 +226,7 @@ where
|
||||||
EntryKind::Symlink => ("Link", YELLOW),
|
EntryKind::Symlink => ("Link", YELLOW),
|
||||||
};
|
};
|
||||||
let type_label = egui::Label::new(
|
let type_label = egui::Label::new(
|
||||||
RichText::new(type_str).color(type_col).size(11.0)
|
RichText::new(type_str).color(type_col).size(11.0),
|
||||||
)
|
)
|
||||||
.truncate();
|
.truncate();
|
||||||
ui.add_sized([38.0, ROW_H], type_label);
|
ui.add_sized([38.0, ROW_H], type_label);
|
||||||
|
|
@ -217,7 +253,10 @@ where
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
FileTableResponse { double_clicked, clicked }
|
FileTableResponse {
|
||||||
|
double_clicked,
|
||||||
|
clicked,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_header(ui: &mut Ui, label: &str, min_w: f32, active: bool, dir: SortDir) -> bool {
|
fn sort_header(ui: &mut Ui, label: &str, min_w: f32, active: bool, dir: SortDir) -> bool {
|
||||||
|
|
@ -236,7 +275,11 @@ fn sort_header(ui: &mut Ui, label: &str, min_w: f32, active: bool, dir: SortDir)
|
||||||
|
|
||||||
fn toggle_sort(col: SortCol, cur_col: SortCol, cur_dir: SortDir) -> (SortCol, SortDir) {
|
fn toggle_sort(col: SortCol, cur_col: SortCol, cur_dir: SortDir) -> (SortCol, SortDir) {
|
||||||
if col == cur_col {
|
if col == cur_col {
|
||||||
let new_dir = if cur_dir == SortDir::Asc { SortDir::Desc } else { SortDir::Asc };
|
let new_dir = if cur_dir == SortDir::Asc {
|
||||||
|
SortDir::Desc
|
||||||
|
} else {
|
||||||
|
SortDir::Asc
|
||||||
|
};
|
||||||
(col, new_dir)
|
(col, new_dir)
|
||||||
} else {
|
} else {
|
||||||
(col, SortDir::Asc)
|
(col, SortDir::Asc)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use egui::{RichText, Ui};
|
|
||||||
use crate::domain::connection::ConnectionStatus;
|
use crate::domain::connection::ConnectionStatus;
|
||||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||||
use crate::domain::transfer::{TransferKind, TransferTask};
|
use crate::domain::transfer::{TransferKind, TransferTask};
|
||||||
use crate::fs::local;
|
use crate::fs::local;
|
||||||
use crate::fs::remote::RemoteRegistry;
|
use crate::fs::remote::RemoteRegistry;
|
||||||
use crate::transfer::queue::TransferQueue;
|
use crate::transfer::queue::TransferQueue;
|
||||||
use crate::ui::drag::{make_local_payload, DragPayload};
|
use crate::ui::drag::{DragPayload, make_local_payload};
|
||||||
use crate::ui::panels::file_pane::{file_table, FileTableResponse, format_size};
|
use crate::ui::panels::file_pane::{FileTableResponse, file_table, format_size};
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::{RichText, Ui};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn parent_path(path: &str) -> String {
|
fn parent_path(path: &str) -> String {
|
||||||
|
|
@ -88,30 +88,25 @@ fn render_inner(ui: &mut Ui, state: &mut AppState) {
|
||||||
.map(|t| t.status == ConnectionStatus::Connected)
|
.map(|t| t.status == ConnectionStatus::Connected)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let FileTableResponse { double_clicked, .. } = file_table(
|
let FileTableResponse { double_clicked, .. } =
|
||||||
ui,
|
file_table(ui, "local_table", &entries, &mut selected, |entry| {
|
||||||
"local_table",
|
|
||||||
&entries,
|
|
||||||
&mut selected,
|
|
||||||
|entry| {
|
|
||||||
if has_connection {
|
if has_connection {
|
||||||
make_local_payload(entry)
|
make_local_payload(entry)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
state.local_selected = selected;
|
state.local_selected = selected;
|
||||||
|
|
||||||
if let Some(name) = double_clicked {
|
if let Some(name) = double_clicked {
|
||||||
if name == ".." {
|
if name == ".." {
|
||||||
state.local_path = parent_path(&state.local_path);
|
state.local_path = parent_path(&state.local_path);
|
||||||
} else if let Some(entry) = state.local_entries.iter().find(|e| e.name == name) {
|
} else if let Some(entry) = state.local_entries.iter().find(|e| e.name == name)
|
||||||
if entry.kind == EntryKind::Dir {
|
&& entry.kind == EntryKind::Dir
|
||||||
|
{
|
||||||
state.local_path = entry.path.clone();
|
state.local_path = entry.path.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
refresh_local(state);
|
refresh_local(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +117,12 @@ fn render_inner(ui: &mut Ui, state: &mut AppState) {
|
||||||
fn render_path_bar(ui: &mut Ui, state: &mut AppState) {
|
fn render_path_bar(ui: &mut Ui, state: &mut AppState) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
.fill(BG_PANEL)
|
.fill(BG_PANEL)
|
||||||
.inner_margin(egui::Margin { left: 8.0, right: 8.0, top: 4.0, bottom: 4.0 });
|
.inner_margin(egui::Margin {
|
||||||
|
left: 8.0,
|
||||||
|
right: 8.0,
|
||||||
|
top: 4.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
});
|
||||||
|
|
||||||
frame.show(ui, |ui| {
|
frame.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -132,10 +132,12 @@ fn render_path_bar(ui: &mut Ui, state: &mut AppState) {
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
let is_home = state.local_path == home;
|
let is_home = state.local_path == home;
|
||||||
let home_btn = egui::Button::new(
|
let home_btn = egui::Button::new(RichText::new("🏠").size(13.0))
|
||||||
RichText::new("🏠").size(13.0)
|
.fill(if is_home {
|
||||||
)
|
BG_ROW_SEL
|
||||||
.fill(if is_home { BG_ROW_SEL } else { egui::Color32::TRANSPARENT })
|
} else {
|
||||||
|
egui::Color32::TRANSPARENT
|
||||||
|
})
|
||||||
.rounding(4.0)
|
.rounding(4.0)
|
||||||
.min_size(egui::vec2(22.0, 22.0));
|
.min_size(egui::vec2(22.0, 22.0));
|
||||||
if ui.add(home_btn).clicked() {
|
if ui.add(home_btn).clicked() {
|
||||||
|
|
@ -159,9 +161,8 @@ fn render_path_bar(ui: &mut Ui, state: &mut AppState) {
|
||||||
ui.label(RichText::new(*part).color(TEXT_PRIMARY).size(12.0).strong());
|
ui.label(RichText::new(*part).color(TEXT_PRIMARY).size(12.0).strong());
|
||||||
} else {
|
} else {
|
||||||
let path_snap = acc.clone();
|
let path_snap = acc.clone();
|
||||||
let link = egui::Label::new(
|
let link = egui::Label::new(RichText::new(*part).color(TEXT_DIM).size(12.0))
|
||||||
RichText::new(*part).color(TEXT_DIM).size(12.0)
|
.sense(egui::Sense::click());
|
||||||
).sense(egui::Sense::click());
|
|
||||||
if ui.add(link).clicked() {
|
if ui.add(link).clicked() {
|
||||||
state.local_path = path_snap;
|
state.local_path = path_snap;
|
||||||
refresh_local(state);
|
refresh_local(state);
|
||||||
|
|
@ -183,20 +184,24 @@ fn render_footer(ui: &mut Ui, state: &mut AppState) {
|
||||||
|
|
||||||
frame.show(ui, |ui| {
|
frame.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let count = state.local_entries.iter().filter(|e| e.name != "..").count();
|
let count = state
|
||||||
|
.local_entries
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.name != "..")
|
||||||
|
.count();
|
||||||
let total_size: u64 = state.local_entries.iter().filter_map(|e| e.size).sum();
|
let total_size: u64 = state.local_entries.iter().filter_map(|e| e.size).sum();
|
||||||
|
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{} items", count))
|
RichText::new(format!("{} items", count))
|
||||||
.color(TEXT_DIM)
|
.color(TEXT_DIM)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
if total_size > 0 {
|
if total_size > 0 {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("({})", format_size(Some(total_size))))
|
RichText::new(format!("({})", format_size(Some(total_size))))
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use egui::{Color32, RichText, Ui};
|
|
||||||
use crate::domain::transfer::{TaskState, TransferKind};
|
|
||||||
use crate::domain::transfer::TransferTask;
|
use crate::domain::transfer::TransferTask;
|
||||||
|
use crate::domain::transfer::{TaskState, TransferKind};
|
||||||
use crate::ui::panels::file_pane::format_size;
|
use crate::ui::panels::file_pane::format_size;
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::{Color32, RichText, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||||
// Заголовок очереди (всегда виден)
|
// Заголовок очереди (всегда виден)
|
||||||
|
|
@ -15,7 +15,10 @@ pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||||
ui.set_min_height(QUEUE_COLLAPSED_H);
|
ui.set_min_height(QUEUE_COLLAPSED_H);
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
let arrow = if state.show_queue { "▼" } else { "▲" };
|
let arrow = if state.show_queue { "▼" } else { "▲" };
|
||||||
let n_active = tasks.iter().filter(|t| matches!(t.state, TaskState::Running | TaskState::Queued)).count();
|
let n_active = tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|t| matches!(t.state, TaskState::Running | TaskState::Queued))
|
||||||
|
.count();
|
||||||
|
|
||||||
let title = if n_active > 0 {
|
let title = if n_active > 0 {
|
||||||
format!("{} Transfer Queue ({})", arrow, n_active)
|
format!("{} Transfer Queue ({})", arrow, n_active)
|
||||||
|
|
@ -24,7 +27,10 @@ pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let btn = egui::Button::new(
|
let btn = egui::Button::new(
|
||||||
RichText::new(&title).color(TEXT_PRIMARY).size(12.0).strong()
|
RichText::new(&title)
|
||||||
|
.color(TEXT_PRIMARY)
|
||||||
|
.size(12.0)
|
||||||
|
.strong(),
|
||||||
)
|
)
|
||||||
.fill(Color32::TRANSPARENT)
|
.fill(Color32::TRANSPARENT)
|
||||||
.min_size(egui::vec2(0.0, QUEUE_COLLAPSED_H));
|
.min_size(egui::vec2(0.0, QUEUE_COLLAPSED_H));
|
||||||
|
|
@ -35,7 +41,8 @@ pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||||
|
|
||||||
// Аггрегированная скорость справа
|
// Аггрегированная скорость справа
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let agg_speed: u64 = tasks.iter()
|
let agg_speed: u64 = tasks
|
||||||
|
.iter()
|
||||||
.filter(|t| t.state == TaskState::Running)
|
.filter(|t| t.state == TaskState::Running)
|
||||||
.filter_map(|t| t.speed)
|
.filter_map(|t| t.speed)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
@ -44,19 +51,29 @@ pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{}/s", format_size(Some(agg_speed))))
|
RichText::new(format!("{}/s", format_size(Some(agg_speed))))
|
||||||
.color(GREEN)
|
.color(GREEN)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
} else if !tasks.is_empty() {
|
} else if !tasks.is_empty() {
|
||||||
let done = tasks.iter().filter(|t| t.state == TaskState::Completed).count();
|
let done = tasks
|
||||||
let failed = tasks.iter().filter(|t| matches!(t.state, TaskState::Failed(_))).count();
|
.iter()
|
||||||
|
.filter(|t| t.state == TaskState::Completed)
|
||||||
|
.count();
|
||||||
|
let failed = tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|t| matches!(t.state, TaskState::Failed(_)))
|
||||||
|
.count();
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{}/{} done", done, tasks.len()))
|
RichText::new(format!("{}/{} done", done, tasks.len()))
|
||||||
.color(TEXT_DIM)
|
.color(TEXT_DIM)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.label(RichText::new(format!("{} failed", failed)).color(RED).size(11.0));
|
ui.label(
|
||||||
|
RichText::new(format!("{} failed", failed))
|
||||||
|
.color(RED)
|
||||||
|
.size(11.0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -91,7 +108,12 @@ fn render_task(ui: &mut Ui, task: &TransferTask) {
|
||||||
|
|
||||||
egui::Frame::none()
|
egui::Frame::none()
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.inner_margin(egui::Margin { left: 10.0, right: 10.0, top: 4.0, bottom: 4.0 })
|
.inner_margin(egui::Margin {
|
||||||
|
left: 10.0,
|
||||||
|
right: 10.0,
|
||||||
|
top: 4.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
})
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// стрелка вида
|
// стрелка вида
|
||||||
|
|
@ -104,7 +126,9 @@ fn render_task(ui: &mut Ui, task: &TransferTask) {
|
||||||
|
|
||||||
// имя файла
|
// имя файла
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(&task.file_name).color(TEXT_PRIMARY).size(12.0)
|
RichText::new(&task.file_name)
|
||||||
|
.color(TEXT_PRIMARY)
|
||||||
|
.size(12.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// статус справа
|
// статус справа
|
||||||
|
|
@ -144,27 +168,27 @@ fn render_task(ui: &mut Ui, task: &TransferTask) {
|
||||||
format_size(Some(task.total_bytes))
|
format_size(Some(task.total_bytes))
|
||||||
))
|
))
|
||||||
.color(TEXT_DIM)
|
.color(TEXT_DIM)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(speed) = task.speed {
|
if let Some(speed) = task.speed
|
||||||
if speed > 0 {
|
&& speed > 0
|
||||||
|
{
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!(" {}/s", format_size(Some(speed))))
|
RichText::new(format!(" {}/s", format_size(Some(speed))))
|
||||||
.color(GREEN)
|
.color(GREEN)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
if let Some(eta) = task.eta_secs
|
||||||
if let Some(eta) = task.eta_secs {
|
&& eta > 0
|
||||||
if eta > 0 {
|
{
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!(" ETA {}s", eta))
|
RichText::new(format!(" ETA {}s", eta))
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(10.0)
|
.size(10.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ошибка если есть
|
// ошибка если есть
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,23 @@ use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||||
use crate::domain::transfer::{TransferKind, TransferTask};
|
use crate::domain::transfer::{TransferKind, TransferTask};
|
||||||
use crate::fs::remote::RemoteRegistry;
|
use crate::fs::remote::RemoteRegistry;
|
||||||
use crate::transfer::queue::TransferQueue;
|
use crate::transfer::queue::TransferQueue;
|
||||||
use crate::ui::drag::{make_remote_payload, DragPayload};
|
use crate::ui::drag::{DragPayload, make_remote_payload};
|
||||||
use crate::ui::panels::file_pane::{file_table, FileTableResponse, format_size};
|
use crate::ui::panels::file_pane::{FileTableResponse, file_table, format_size};
|
||||||
use crate::ui::state::{AppState, PendingRemoteList};
|
use crate::ui::state::{AppState, PendingRemoteList};
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
|
||||||
pub fn remote_parent(path: &str) -> String {
|
pub fn remote_parent(path: &str) -> String {
|
||||||
let trimmed = path.trim_end_matches('/');
|
let trimmed = path.trim_end_matches('/');
|
||||||
if trimmed.is_empty() || trimmed == "/" { return "/".into(); }
|
if trimmed.is_empty() || trimmed == "/" {
|
||||||
|
return "/".into();
|
||||||
|
}
|
||||||
if let Some(idx) = trimmed.rfind('/') {
|
if let Some(idx) = trimmed.rfind('/') {
|
||||||
let p = &trimmed[..idx];
|
let p = &trimmed[..idx];
|
||||||
if p.is_empty() { "/".into() } else { p.to_string() }
|
if p.is_empty() {
|
||||||
|
"/".into()
|
||||||
|
} else {
|
||||||
|
p.to_string()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"/".into()
|
"/".into()
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +156,12 @@ fn render_path_bar(
|
||||||
) {
|
) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
.fill(BG_PANEL)
|
.fill(BG_PANEL)
|
||||||
.inner_margin(egui::Margin { left: 8.0, right: 8.0, top: 4.0, bottom: 4.0 });
|
.inner_margin(egui::Margin {
|
||||||
|
left: 8.0,
|
||||||
|
right: 8.0,
|
||||||
|
top: 4.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
});
|
||||||
|
|
||||||
frame.show(ui, |ui| {
|
frame.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -189,9 +200,8 @@ fn render_path_bar(
|
||||||
if is_last {
|
if is_last {
|
||||||
ui.label(RichText::new(*part).color(TEXT_PRIMARY).size(12.0).strong());
|
ui.label(RichText::new(*part).color(TEXT_PRIMARY).size(12.0).strong());
|
||||||
} else {
|
} else {
|
||||||
let link = egui::Label::new(
|
let link = egui::Label::new(RichText::new(*part).color(TEXT_DIM).size(12.0))
|
||||||
RichText::new(*part).color(TEXT_DIM).size(12.0)
|
.sense(egui::Sense::click());
|
||||||
).sense(egui::Sense::click());
|
|
||||||
if ui.add(link).clicked() {
|
if ui.add(link).clicked() {
|
||||||
state.tabs[tab_idx].remote_path = path_snap;
|
state.tabs[tab_idx].remote_path = path_snap;
|
||||||
if !state.tabs[tab_idx].loading {
|
if !state.tabs[tab_idx].loading {
|
||||||
|
|
@ -237,7 +247,7 @@ fn render_footer(
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("({})", format_size(Some(total_size))))
|
RichText::new(format!("({})", format_size(Some(total_size))))
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,11 +256,11 @@ fn render_footer(
|
||||||
let refresh = egui::Button::new(RichText::new("⟳").color(ACCENT).size(14.0))
|
let refresh = egui::Button::new(RichText::new("⟳").color(ACCENT).size(14.0))
|
||||||
.fill(egui::Color32::TRANSPARENT)
|
.fill(egui::Color32::TRANSPARENT)
|
||||||
.min_size(egui::vec2(20.0, 20.0));
|
.min_size(egui::vec2(20.0, 20.0));
|
||||||
if ui.add(refresh).on_hover_text("Refresh").clicked() {
|
if ui.add(refresh).on_hover_text("Refresh").clicked()
|
||||||
if !state.tabs[tab_idx].loading {
|
&& !state.tabs[tab_idx].loading
|
||||||
|
{
|
||||||
trigger_list(state, tab_idx, registry, rt_handle);
|
trigger_list(state, tab_idx, registry, rt_handle);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -289,5 +299,7 @@ pub fn trigger_list(
|
||||||
*result_clone.lock().unwrap() = Some(r);
|
*result_clone.lock().unwrap() = Some(r);
|
||||||
});
|
});
|
||||||
|
|
||||||
state.pending_remote_list.push(PendingRemoteList { tab_idx, result });
|
state
|
||||||
|
.pending_remote_list
|
||||||
|
.push(PendingRemoteList { tab_idx, result });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Левая боковая панель — bookmarks + места
|
//! Левая боковая панель — bookmarks + места
|
||||||
use egui::{RichText, Ui};
|
use crate::ui::panels::local_pane::refresh_local;
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
use crate::ui::panels::local_pane::refresh_local;
|
use egui::{RichText, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
|
|
@ -69,12 +69,12 @@ pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(" No bookmarks yet")
|
RichText::new(" No bookmarks yet")
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(" Use ★ in file list")
|
RichText::new(" Use ★ in file list")
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(10.0)
|
.size(10.0),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for bm in &custom {
|
for bm in &custom {
|
||||||
|
|
@ -104,23 +104,32 @@ fn section_divider(ui: &mut Ui) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sidebar_item(ui: &mut Ui, icon: &str, label: &str, active: bool, mut on_click: impl FnMut()) {
|
fn sidebar_item(ui: &mut Ui, icon: &str, label: &str, active: bool, mut on_click: impl FnMut()) {
|
||||||
let bg = if active { BG_ROW_SEL } else { egui::Color32::TRANSPARENT };
|
let bg = if active {
|
||||||
let text_col = if active { egui::Color32::WHITE } else { TEXT_PRIMARY };
|
BG_ROW_SEL
|
||||||
|
} else {
|
||||||
|
egui::Color32::TRANSPARENT
|
||||||
|
};
|
||||||
|
let text_col = if active {
|
||||||
|
egui::Color32::WHITE
|
||||||
|
} else {
|
||||||
|
TEXT_PRIMARY
|
||||||
|
};
|
||||||
|
|
||||||
let resp = egui::Frame::none()
|
let resp = egui::Frame::none()
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.rounding(4.0)
|
.rounding(4.0)
|
||||||
.inner_margin(egui::Margin { left: 10.0, right: 6.0, top: 2.0, bottom: 2.0 })
|
.inner_margin(egui::Margin {
|
||||||
|
left: 10.0,
|
||||||
|
right: 6.0,
|
||||||
|
top: 2.0,
|
||||||
|
bottom: 2.0,
|
||||||
|
})
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.set_min_width(SIDEBAR_W - 8.0);
|
ui.set_min_width(SIDEBAR_W - 8.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new(icon).size(12.0));
|
ui.label(RichText::new(icon).size(12.0));
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.label(
|
ui.label(RichText::new(label).color(text_col).size(12.0));
|
||||||
RichText::new(label)
|
|
||||||
.color(text_col)
|
|
||||||
.size(12.0)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use egui::RichText;
|
|
||||||
use crate::domain::connection::ConnectionStatus;
|
use crate::domain::connection::ConnectionStatus;
|
||||||
use crate::ui::panels::file_pane::format_size;
|
use crate::ui::panels::file_pane::format_size;
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::RichText;
|
||||||
|
|
||||||
pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
|
|
@ -16,12 +16,14 @@ pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(&state.status_message)
|
RichText::new(&state.status_message)
|
||||||
.color(TEXT_DIM)
|
.color(TEXT_DIM)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
// аггрегированная скорость
|
// аггрегированная скорость
|
||||||
let agg: u64 = state.queue_tasks.iter()
|
let agg: u64 = state
|
||||||
|
.queue_tasks
|
||||||
|
.iter()
|
||||||
.filter(|t| t.state == crate::domain::transfer::TaskState::Running)
|
.filter(|t| t.state == crate::domain::transfer::TaskState::Running)
|
||||||
.filter_map(|t| t.speed)
|
.filter_map(|t| t.speed)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
@ -30,13 +32,15 @@ pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("⬆⬇ {}/s", format_size(Some(agg))))
|
RichText::new(format!("⬆⬇ {}/s", format_size(Some(agg))))
|
||||||
.color(GREEN)
|
.color(GREEN)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// соединения
|
// соединения
|
||||||
let connected = state.tabs.iter()
|
let connected = state
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
.filter(|t| t.status == ConnectionStatus::Connected)
|
.filter(|t| t.status == ConnectionStatus::Connected)
|
||||||
.count();
|
.count();
|
||||||
let total = state.tabs.len();
|
let total = state.tabs.len();
|
||||||
|
|
@ -46,13 +50,13 @@ pub fn render(ui: &mut egui::Ui, state: &AppState) {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("● {}/{}", connected, total))
|
RichText::new(format!("● {}/{}", connected, total))
|
||||||
.color(dot_col)
|
.color(dot_col)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new("○ Not connected")
|
RichText::new("○ Not connected")
|
||||||
.color(TEXT_HINT)
|
.color(TEXT_HINT)
|
||||||
.size(11.0)
|
.size(11.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
use egui::{Color32, RichText, Ui};
|
|
||||||
use crate::domain::connection::ConnectionStatus;
|
use crate::domain::connection::ConnectionStatus;
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::{Color32, RichText, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
.fill(BG_BASE)
|
.fill(BG_BASE)
|
||||||
.inner_margin(egui::Margin { left: 6.0, right: 6.0, top: 0.0, bottom: 0.0 });
|
.inner_margin(egui::Margin {
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
frame.show(ui, |ui| {
|
frame.show(ui, |ui| {
|
||||||
ui.set_min_height(TABBAR_H);
|
ui.set_min_height(TABBAR_H);
|
||||||
|
|
@ -27,13 +32,27 @@ pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
ConnectionStatus::Error(_) => (RED, "×"),
|
ConnectionStatus::Error(_) => (RED, "×"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let bg = if is_active { BG_TAB_ACTIVE } else { BG_TAB_IDLE };
|
let bg = if is_active {
|
||||||
|
BG_TAB_ACTIVE
|
||||||
|
} else {
|
||||||
|
BG_TAB_IDLE
|
||||||
|
};
|
||||||
let tcol = if is_active { TEXT_PRIMARY } else { TEXT_DIM };
|
let tcol = if is_active { TEXT_PRIMARY } else { TEXT_DIM };
|
||||||
|
|
||||||
let tab_response = egui::Frame::none()
|
let tab_response = egui::Frame::none()
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.rounding(egui::Rounding { nw: 4.0, ne: 4.0, sw: 0.0, se: 0.0 })
|
.rounding(egui::Rounding {
|
||||||
.inner_margin(egui::Margin { left: 10.0, right: 4.0, top: 0.0, bottom: 0.0 })
|
nw: 4.0,
|
||||||
|
ne: 4.0,
|
||||||
|
sw: 0.0,
|
||||||
|
se: 0.0,
|
||||||
|
})
|
||||||
|
.inner_margin(egui::Margin {
|
||||||
|
left: 10.0,
|
||||||
|
right: 4.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
})
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.set_min_height(TABBAR_H);
|
ui.set_min_height(TABBAR_H);
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
|
|
@ -41,18 +60,16 @@ pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
ui.label(RichText::new(dot).color(dot_col).size(9.0));
|
ui.label(RichText::new(dot).color(dot_col).size(9.0));
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
// название вкладки
|
// название вкладки
|
||||||
let lbl = egui::Label::new(
|
let lbl =
|
||||||
RichText::new(&tab.label).color(tcol).size(12.0)
|
egui::Label::new(RichText::new(&tab.label).color(tcol).size(12.0))
|
||||||
)
|
|
||||||
.sense(egui::Sense::click());
|
.sense(egui::Sense::click());
|
||||||
if ui.add(lbl).clicked() {
|
if ui.add(lbl).clicked() {
|
||||||
// обрабатывается ниже
|
// обрабатывается ниже
|
||||||
}
|
}
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
// кнопка закрытия
|
// кнопка закрытия
|
||||||
let close = egui::Label::new(
|
let close =
|
||||||
RichText::new("×").color(TEXT_HINT).size(14.0)
|
egui::Label::new(RichText::new("×").color(TEXT_HINT).size(14.0))
|
||||||
)
|
|
||||||
.sense(egui::Sense::click());
|
.sense(egui::Sense::click());
|
||||||
if ui.add(close).clicked() {
|
if ui.add(close).clicked() {
|
||||||
to_close = Some(i);
|
to_close = Some(i);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use egui::{Align, Layout, RichText, Ui};
|
|
||||||
use crate::ui::state::AppState;
|
use crate::ui::state::AppState;
|
||||||
use crate::ui::theme::*;
|
use crate::ui::theme::*;
|
||||||
|
use egui::{Align, Layout, RichText, Ui};
|
||||||
|
|
||||||
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
pub fn render(ui: &mut Ui, state: &mut AppState) {
|
||||||
let frame = egui::Frame::none()
|
let frame = egui::Frame::none()
|
||||||
|
|
@ -112,19 +112,29 @@ fn render_history_popup(ui: &mut Ui, state: &mut AppState) {
|
||||||
.inner_margin(egui::Margin::same(8.0))
|
.inner_margin(egui::Margin::same(8.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.set_width(260.0);
|
ui.set_width(260.0);
|
||||||
ui.label(RichText::new("Recent Connections").color(TEXT_DIM).size(11.0));
|
ui.label(
|
||||||
|
RichText::new("Recent Connections")
|
||||||
|
.color(TEXT_DIM)
|
||||||
|
.size(11.0),
|
||||||
|
);
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
if state.history.is_empty() {
|
if state.history.is_empty() {
|
||||||
ui.label(RichText::new("No history yet").color(TEXT_HINT));
|
ui.label(RichText::new("No history yet").color(TEXT_HINT));
|
||||||
} else {
|
} else {
|
||||||
egui::ScrollArea::vertical().max_height(160.0).show(ui, |ui| {
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(160.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
for entry in &state.history.clone() {
|
for entry in &state.history.clone() {
|
||||||
let label = format!("{}@{}:{}", entry.user, entry.host, entry.port);
|
let label =
|
||||||
let time = RichText::new(&entry.time).color(TEXT_HINT).size(10.0);
|
format!("{}@{}:{}", entry.user, entry.host, entry.port);
|
||||||
|
let time =
|
||||||
|
RichText::new(&entry.time).color(TEXT_HINT).size(10.0);
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(
|
||||||
egui::Button::new(
|
egui::Button::new(
|
||||||
RichText::new(&label).color(TEXT_PRIMARY).size(12.0),
|
RichText::new(&label)
|
||||||
|
.color(TEXT_PRIMARY)
|
||||||
|
.size(12.0),
|
||||||
)
|
)
|
||||||
.fill(egui::Color32::TRANSPARENT)
|
.fill(egui::Color32::TRANSPARENT)
|
||||||
.min_size(egui::vec2(240.0, 24.0)),
|
.min_size(egui::vec2(240.0, 24.0)),
|
||||||
|
|
@ -156,7 +166,11 @@ fn render_history_popup(ui: &mut Ui, state: &mut AppState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar_btn(ui: &mut Ui, label: &str, active: bool, mut on_click: impl FnMut()) {
|
fn toolbar_btn(ui: &mut Ui, label: &str, active: bool, mut on_click: impl FnMut()) {
|
||||||
let fill = if active { BG_TAB_ACTIVE } else { egui::Color32::TRANSPARENT };
|
let fill = if active {
|
||||||
|
BG_TAB_ACTIVE
|
||||||
|
} else {
|
||||||
|
egui::Color32::TRANSPARENT
|
||||||
|
};
|
||||||
let text_col = if active { ACCENT } else { TEXT_PRIMARY };
|
let text_col = if active { ACCENT } else { TEXT_PRIMARY };
|
||||||
let btn = egui::Button::new(RichText::new(label).color(text_col).size(12.0))
|
let btn = egui::Button::new(RichText::new(label).color(text_col).size(12.0))
|
||||||
.fill(fill)
|
.fill(fill)
|
||||||
|
|
|
||||||
|
|
@ -98,15 +98,36 @@ impl Default for AppState {
|
||||||
show_bookmarks: false,
|
show_bookmarks: false,
|
||||||
show_history: false,
|
show_history: false,
|
||||||
bookmarks: vec![
|
bookmarks: vec![
|
||||||
Bookmark { name: "Home".into(), path: home.clone() },
|
Bookmark {
|
||||||
Bookmark { name: "Desktop".into(), path: format!("{}/Desktop", home) },
|
name: "Home".into(),
|
||||||
Bookmark { name: "Documents".into(), path: format!("{}/Documents", home) },
|
path: home.clone(),
|
||||||
Bookmark { name: "Downloads".into(), path: dirs::download_dir()
|
},
|
||||||
|
Bookmark {
|
||||||
|
name: "Desktop".into(),
|
||||||
|
path: format!("{}/Desktop", home),
|
||||||
|
},
|
||||||
|
Bookmark {
|
||||||
|
name: "Documents".into(),
|
||||||
|
path: format!("{}/Documents", home),
|
||||||
|
},
|
||||||
|
Bookmark {
|
||||||
|
name: "Downloads".into(),
|
||||||
|
path: dirs::download_dir()
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| format!("{}/Downloads", home)) },
|
.unwrap_or_else(|| format!("{}/Downloads", home)),
|
||||||
Bookmark { name: "Pictures".into(), path: format!("{}/Pictures", home) },
|
},
|
||||||
Bookmark { name: "Music".into(), path: format!("{}/Music", home) },
|
Bookmark {
|
||||||
Bookmark { name: "Videos".into(), path: format!("{}/Videos", home) },
|
name: "Pictures".into(),
|
||||||
|
path: format!("{}/Pictures", home),
|
||||||
|
},
|
||||||
|
Bookmark {
|
||||||
|
name: "Music".into(),
|
||||||
|
path: format!("{}/Music", home),
|
||||||
|
},
|
||||||
|
Bookmark {
|
||||||
|
name: "Videos".into(),
|
||||||
|
path: format!("{}/Videos", home),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
history: Vec::new(),
|
history: Vec::new(),
|
||||||
connect_label: String::new(),
|
connect_label: String::new(),
|
||||||
|
|
@ -134,13 +155,17 @@ impl Default for AppState {
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn active_tab_mut(&mut self) -> Option<&mut ConnectionTab> {
|
pub fn active_tab_mut(&mut self) -> Option<&mut ConnectionTab> {
|
||||||
if self.tabs.is_empty() { return None; }
|
if self.tabs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let idx = self.active_tab.min(self.tabs.len() - 1);
|
let idx = self.active_tab.min(self.tabs.len() - 1);
|
||||||
Some(&mut self.tabs[idx])
|
Some(&mut self.tabs[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_tab_ref(&self) -> Option<&ConnectionTab> {
|
pub fn active_tab_ref(&self) -> Option<&ConnectionTab> {
|
||||||
if self.tabs.is_empty() { return None; }
|
if self.tabs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let idx = self.active_tab.min(self.tabs.len() - 1);
|
let idx = self.active_tab.min(self.tabs.len() - 1);
|
||||||
Some(&self.tabs[idx])
|
Some(&self.tabs[idx])
|
||||||
}
|
}
|
||||||
|
|
@ -148,9 +173,17 @@ impl AppState {
|
||||||
pub fn add_history(&mut self, host: &str, port: u16, user: &str) {
|
pub fn add_history(&mut self, host: &str, port: u16, user: &str) {
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
let now = Local::now().format("%H:%M %d.%m").to_string();
|
let now = Local::now().format("%H:%M %d.%m").to_string();
|
||||||
self.history.insert(0, HistoryEntry {
|
self.history.insert(
|
||||||
host: host.into(), port, user: user.into(), time: now,
|
0,
|
||||||
});
|
HistoryEntry {
|
||||||
if self.history.len() > 20 { self.history.truncate(20); }
|
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