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:
loki5512344 2026-06-19 17:03:20 +02:00
parent f983f12bec
commit 42c147a13e
12 changed files with 509 additions and 298 deletions

View file

@ -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(&params.host, params.port, &params.username); self.state
.add_history(&params.host, params.port, &params.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)
@ -163,25 +164,25 @@ impl FileManagerApp {
fn apply_visuals(&self, ctx: &egui::Context) { fn apply_visuals(&self, ctx: &egui::Context) {
let mut vis = Visuals::dark(); let mut vis = Visuals::dark();
vis.window_fill = BG_PANEL; vis.window_fill = BG_PANEL;
vis.panel_fill = BG_CONTENT; vis.panel_fill = BG_CONTENT;
vis.extreme_bg_color = BG_BASE; vis.extreme_bg_color = BG_BASE;
vis.code_bg_color = BG_BASE; vis.code_bg_color = BG_BASE;
vis.override_text_color = Some(TEXT_PRIMARY); vis.override_text_color = Some(TEXT_PRIMARY);
vis.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, BORDER); vis.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, BORDER);
vis.widgets.inactive.bg_fill = Color32::from_rgb(36, 36, 40); vis.widgets.inactive.bg_fill = Color32::from_rgb(36, 36, 40);
vis.widgets.hovered.bg_fill = BG_ROW_HOVER; vis.widgets.hovered.bg_fill = BG_ROW_HOVER;
vis.widgets.active.bg_fill = ACCENT_DIM; vis.widgets.active.bg_fill = ACCENT_DIM;
vis.selection.bg_fill = BG_ROW_SEL; vis.selection.bg_fill = BG_ROW_SEL;
vis.selection.stroke = egui::Stroke::new(1.0, ACCENT); vis.selection.stroke = egui::Stroke::new(1.0, ACCENT);
vis.window_rounding = egui::Rounding::same(8.0); vis.window_rounding = egui::Rounding::same(8.0);
ctx.set_visuals(vis); ctx.set_visuals(vis);
let fonts = egui::FontDefinitions::default(); let fonts = egui::FontDefinitions::default();
ctx.set_fonts(fonts); ctx.set_fonts(fonts);
let mut style = ctx.style().as_ref().clone(); let mut style = ctx.style().as_ref().clone();
style.spacing.item_spacing = egui::vec2(4.0, 2.0); style.spacing.item_spacing = egui::vec2(4.0, 2.0);
style.spacing.button_padding = egui::vec2(8.0, 4.0); style.spacing.button_padding = egui::vec2(8.0, 4.0);
ctx.set_style(style); ctx.set_style(style);
} }
@ -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(
.fill(BG_PANEL) egui::Frame::none()
.stroke(egui::Stroke::new(1.0, BORDER))) .fill(BG_PANEL)
.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,9 +329,11 @@ 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(
.color(TEXT_HINT) "Connect to a server to browse files",
.size(12.0), )
.color(TEXT_HINT)
.size(12.0),
); );
ui.add_space(16.0); ui.add_space(16.0);
let btn = egui::Button::new( let btn = egui::Button::new(
@ -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(),
);
}); });
}); });

View file

@ -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,10 +58,16 @@ 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 =
.fill(Color32::TRANSPARENT); egui::Button::new(RichText::new("×").color(TEXT_HINT).size(14.0))
.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,13 +125,21 @@ 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(
.fill(Color32::from_rgb(40, 40, 44)) RichText::new("Browse").color(TEXT_DIM).size(11.0),
.min_size(egui::vec2(72.0, 32.0)) )
.rounding(4.0); .fill(Color32::from_rgb(40, 40, 44))
.min_size(egui::vec2(72.0, 32.0))
.rounding(4.0);
ui.add_enabled(false, browse); ui.add_enabled(false, browse);
}); });
} }
@ -135,10 +160,11 @@ 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 =
.fill(Color32::from_rgb(40, 40, 44)) egui::Button::new(RichText::new("Cancel").color(TEXT_DIM).size(12.0))
.rounding(6.0) .fill(Color32::from_rgb(40, 40, 44))
.min_size(egui::vec2(90.0, 34.0)); .rounding(6.0)
.min_size(egui::vec2(90.0, 34.0));
if ui.add(cancel).clicked() && !state.connect_loading { if ui.add(cancel).clicked() && !state.connect_loading {
state.show_connect_dialog = false; state.show_connect_dialog = false;
} }
@ -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)
@ -197,18 +226,22 @@ fn do_connect(
let port: u16 = state.connect_port.parse().unwrap_or(match protocol { let port: u16 = state.connect_port.parse().unwrap_or(match protocol {
Protocol::Sftp => 22, Protocol::Sftp => 22,
Protocol::Ftp => 21, Protocol::Ftp => 21,
Protocol::Ftps => 990, Protocol::Ftps => 990,
}); });
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!(
Protocol::Sftp => "SFTP", "{} ({})",
Protocol::Ftp => "FTP", state.connect_host,
Protocol::Ftps => "FTPS", match protocol {
}) Protocol::Sftp => "SFTP",
Protocol::Ftp => "FTP",
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,8 +294,13 @@ async fn connect_async(
) )
} else { } else {
Arc::new( Arc::new(
SftpClient::connect_password(&params.host, params.port, &params.username, &password) SftpClient::connect_password(
.map_err(|e| e.to_string())?, &params.host,
params.port,
&params.username,
&password,
)
.map_err(|e| e.to_string())?,
) )
} }
} }

View file

@ -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,15 +68,21 @@ 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()),
SortCol::Size => a.size.unwrap_or(0).cmp(&b.size.unwrap_or(0)), SortCol::Size => a.size.unwrap_or(0).cmp(&b.size.unwrap_or(0)),
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,18 +170,23 @@ 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),
EntryKind::Symlink => format!("🔗 {}", entry.name), EntryKind::Symlink => format!("🔗 {}", entry.name),
EntryKind::File => { EntryKind::File => {
let icon = file_icon(&entry.name); let icon = file_icon(&entry.name);
format!("{} {}", icon, entry.name) format!("{} {}", icon, 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));
@ -185,12 +221,12 @@ where
ui.add_space(6.0); ui.add_space(6.0);
let (type_str, type_col) = match entry.kind { let (type_str, type_col) = match entry.kind {
EntryKind::Dir => ("Dir", TEXT_DIM), EntryKind::Dir => ("Dir", TEXT_DIM),
EntryKind::File => ("File", TEXT_HINT), EntryKind::File => ("File", TEXT_HINT),
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)
@ -246,18 +289,18 @@ fn toggle_sort(col: SortCol, cur_col: SortCol, cur_dir: SortDir) -> (SortCol, So
fn file_icon(name: &str) -> &'static str { fn file_icon(name: &str) -> &'static str {
let ext = name.rsplit('.').next().unwrap_or("").to_lowercase(); let ext = name.rsplit('.').next().unwrap_or("").to_lowercase();
match ext.as_str() { match ext.as_str() {
"rs" => "🦀", "rs" => "🦀",
"toml" | "yaml" | "yml" | "json" => "⚙", "toml" | "yaml" | "yml" | "json" => "⚙",
"md" | "txt" | "log" => "📝", "md" | "txt" | "log" => "📝",
"png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" => "🖼", "png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" => "🖼",
"mp4" | "mkv" | "avi" | "mov" => "🎬", "mp4" | "mkv" | "avi" | "mov" => "🎬",
"mp3" | "ogg" | "flac" | "wav" => "🎵", "mp3" | "ogg" | "flac" | "wav" => "🎵",
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" => "📦", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" => "📦",
"pdf" => "📕", "pdf" => "📕",
"sh" | "bash" | "fish" | "zsh" => "⚡", "sh" | "bash" | "fish" | "zsh" => "⚡",
"html" | "css" | "js" | "ts" => "🌐", "html" | "css" | "js" | "ts" => "🌐",
"py" => "🐍", "py" => "🐍",
"java" | "class" | "jar" => "☕", "java" | "class" | "jar" => "☕",
_ => "📄", _ => "📄",
} }
} }

View file

@ -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,29 +88,24 @@ 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,12 +132,14 @@ 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 {
.rounding(4.0) egui::Color32::TRANSPARENT
.min_size(egui::vec2(22.0, 22.0)); })
.rounding(4.0)
.min_size(egui::vec2(22.0, 22.0));
if ui.add(home_btn).clicked() { if ui.add(home_btn).clicked() {
state.local_path = home; state.local_path = home;
refresh_local(state); refresh_local(state);
@ -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),
); );
} }

View file

@ -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),
);
} }
} }
}); });
@ -82,21 +99,26 @@ pub fn render(ui: &mut Ui, state: &mut AppState, tasks: &[TransferTask]) {
fn render_task(ui: &mut Ui, task: &TransferTask) { fn render_task(ui: &mut Ui, task: &TransferTask) {
let bg = match &task.state { let bg = match &task.state {
TaskState::Completed => Color32::from_rgb(22, 38, 24), TaskState::Completed => Color32::from_rgb(22, 38, 24),
TaskState::Failed(_) => Color32::from_rgb(38, 22, 22), TaskState::Failed(_) => Color32::from_rgb(38, 22, 22),
TaskState::Running => Color32::from_rgb(22, 28, 42), TaskState::Running => Color32::from_rgb(22, 28, 42),
TaskState::Queued => BG_QUEUE, TaskState::Queued => BG_QUEUE,
_ => BG_QUEUE, _ => BG_QUEUE,
}; };
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| {
// стрелка вида // стрелка вида
let (arrow, arrow_col) = match task.kind { let (arrow, arrow_col) = match task.kind {
TransferKind::Upload => ("↑", ACCENT), TransferKind::Upload => ("↑", ACCENT),
TransferKind::Download => ("↓", GREEN), TransferKind::Download => ("↓", GREEN),
}; };
ui.label(RichText::new(arrow).color(arrow_col).size(13.0).strong()); ui.label(RichText::new(arrow).color(arrow_col).size(13.0).strong());
@ -104,19 +126,21 @@ 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),
); );
// статус справа // статус справа
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 (state_str, state_col) = match &task.state { let (state_str, state_col) = match &task.state {
TaskState::Queued => ("Queued", TEXT_HINT), TaskState::Queued => ("Queued", TEXT_HINT),
TaskState::Running => ("Uploading", ACCENT), TaskState::Running => ("Uploading", ACCENT),
TaskState::Completed => ("✓ Done", GREEN), TaskState::Completed => ("✓ Done", GREEN),
TaskState::Failed(_) => ("× Failed", RED), TaskState::Failed(_) => ("× Failed", RED),
TaskState::Paused => ("Paused", YELLOW), TaskState::Paused => ("Paused", YELLOW),
TaskState::Cancelled => ("Cancelled", TEXT_HINT), TaskState::Cancelled => ("Cancelled", TEXT_HINT),
TaskState::Retrying(_) => ("Retrying", YELLOW), TaskState::Retrying(_) => ("Retrying", YELLOW),
}; };
ui.label(RichText::new(state_str).color(state_col).size(11.0)); ui.label(RichText::new(state_str).color(state_col).size(11.0));
}); });
@ -128,7 +152,7 @@ fn render_task(ui: &mut Ui, task: &TransferTask) {
let bar_col = match &task.state { let bar_col = match &task.state {
TaskState::Completed => GREEN, TaskState::Completed => GREEN,
TaskState::Failed(_) => RED, TaskState::Failed(_) => RED,
_ => ACCENT, _ => ACCENT,
}; };
let bar = egui::ProgressBar::new(pct) let bar = egui::ProgressBar::new(pct)
@ -144,26 +168,26 @@ 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( {
RichText::new(format!(" {}/s", format_size(Some(speed)))) ui.label(
.color(GREEN) RichText::new(format!(" {}/s", format_size(Some(speed))))
.size(11.0) .color(GREEN)
); .size(11.0),
} );
} }
if let Some(eta) = task.eta_secs { if let Some(eta) = task.eta_secs
if eta > 0 { && eta > 0
ui.label( {
RichText::new(format!(" ETA {}s", eta)) ui.label(
.color(TEXT_HINT) RichText::new(format!(" ETA {}s", eta))
.size(10.0) .color(TEXT_HINT)
); .size(10.0),
} );
} }
}); });

View file

@ -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,16 +156,21 @@ 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| {
// статус точка // статус точка
let (dot, dot_col) = match state.tabs[tab_idx].status { let (dot, dot_col) = match state.tabs[tab_idx].status {
ConnectionStatus::Connected => ("●", GREEN), ConnectionStatus::Connected => ("●", GREEN),
ConnectionStatus::Disconnected => ("○", TEXT_HINT), ConnectionStatus::Disconnected => ("○", TEXT_HINT),
ConnectionStatus::Connecting => ("◐", YELLOW), ConnectionStatus::Connecting => ("◐", YELLOW),
ConnectionStatus::Error(_) => ("×", RED), ConnectionStatus::Error(_) => ("×", RED),
}; };
ui.label(RichText::new(dot).color(dot_col).size(10.0)); ui.label(RichText::new(dot).color(dot_col).size(10.0));
ui.add_space(4.0); ui.add_space(4.0);
@ -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,10 +256,10 @@ 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 });
} }

View file

@ -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)
);
}); });
}); });
@ -131,14 +140,14 @@ fn sidebar_item(ui: &mut Ui, icon: &str, label: &str, active: bool, mut on_click
fn bookmark_icon(name: &str) -> &'static str { fn bookmark_icon(name: &str) -> &'static str {
match name { match name {
"Home" => "🏠", "Home" => "🏠",
"Desktop" => "🖥", "Desktop" => "🖥",
"Documents" => "📄", "Documents" => "📄",
"Downloads" => "⬇", "Downloads" => "⬇",
"Pictures" => "🖼", "Pictures" => "🖼",
"Music" => "🎵", "Music" => "🎵",
"Videos" => "🎬", "Videos" => "🎬",
_ => "📁", _ => "📁",
} }
} }

View file

@ -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),
); );
} }
}); });

View file

@ -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);
@ -21,19 +26,33 @@ pub fn render(ui: &mut Ui, state: &mut AppState) {
let is_active = i == state.active_tab; let is_active = i == state.active_tab;
let (dot_col, dot) = match &tab.status { let (dot_col, dot) = match &tab.status {
ConnectionStatus::Connected => (GREEN, "●"), ConnectionStatus::Connected => (GREEN, "●"),
ConnectionStatus::Disconnected => (TEXT_HINT, "○"), ConnectionStatus::Disconnected => (TEXT_HINT, "○"),
ConnectionStatus::Connecting => (YELLOW, "◐"), ConnectionStatus::Connecting => (YELLOW, "◐"),
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,19 +60,17 @@ 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);
} }

View file

@ -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,34 +112,44 @@ 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()
for entry in &state.history.clone() { .max_height(160.0)
let label = format!("{}@{}:{}", entry.user, entry.host, entry.port); .show(ui, |ui| {
let time = RichText::new(&entry.time).color(TEXT_HINT).size(10.0); for entry in &state.history.clone() {
if ui let label =
.add( format!("{}@{}:{}", entry.user, entry.host, entry.port);
egui::Button::new( let time =
RichText::new(&label).color(TEXT_PRIMARY).size(12.0), RichText::new(&entry.time).color(TEXT_HINT).size(10.0);
if ui
.add(
egui::Button::new(
RichText::new(&label)
.color(TEXT_PRIMARY)
.size(12.0),
)
.fill(egui::Color32::TRANSPARENT)
.min_size(egui::vec2(240.0, 24.0)),
) )
.fill(egui::Color32::TRANSPARENT) .clicked()
.min_size(egui::vec2(240.0, 24.0)), {
) state.connect_host = entry.host.clone();
.clicked() state.connect_user = entry.user.clone();
{ state.connect_port = entry.port.to_string();
state.connect_host = entry.host.clone(); state.show_connect_dialog = true;
state.connect_user = entry.user.clone(); state.show_history = false;
state.connect_port = entry.port.to_string(); }
state.show_connect_dialog = true; ui.label(time);
state.show_history = false;
} }
ui.label(time); });
}
});
} }
ui.add_space(4.0); ui.add_space(4.0);
if ui if ui
@ -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)

View file

@ -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() },
.map(|p| p.to_string_lossy().to_string()) Bookmark {
.unwrap_or_else(|| format!("{}/Downloads", home)) }, name: "Desktop".into(),
Bookmark { name: "Pictures".into(), path: format!("{}/Pictures", home) }, path: format!("{}/Desktop", home),
Bookmark { name: "Music".into(), path: format!("{}/Music", home) }, },
Bookmark { name: "Videos".into(), path: format!("{}/Videos", 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())
.unwrap_or_else(|| format!("{}/Downloads", home)),
},
Bookmark {
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);
}
} }
} }

View file

@ -2,39 +2,39 @@
use egui::Color32; use egui::Color32;
// Backgrounds // Backgrounds
pub const BG_BASE: Color32 = Color32::from_rgb(18, 18, 18); // самый тёмный фон pub const BG_BASE: Color32 = Color32::from_rgb(18, 18, 18); // самый тёмный фон
pub const BG_PANEL: Color32 = Color32::from_rgb(24, 24, 24); // боковая панель pub const BG_PANEL: Color32 = Color32::from_rgb(24, 24, 24); // боковая панель
pub const BG_CONTENT: Color32 = Color32::from_rgb(28, 28, 28); // основная область pub const BG_CONTENT: Color32 = Color32::from_rgb(28, 28, 28); // основная область
pub const BG_TOOLBAR: Color32 = Color32::from_rgb(22, 22, 22); // тулбар pub const BG_TOOLBAR: Color32 = Color32::from_rgb(22, 22, 22); // тулбар
pub const BG_TAB_ACTIVE: Color32 = Color32::from_rgb(38, 38, 42); // активная вкладка pub const BG_TAB_ACTIVE: Color32 = Color32::from_rgb(38, 38, 42); // активная вкладка
pub const BG_TAB_IDLE: Color32 = Color32::from_rgb(28, 28, 30); // неактивная pub const BG_TAB_IDLE: Color32 = Color32::from_rgb(28, 28, 30); // неактивная
pub const BG_ROW_HOVER: Color32 = Color32::from_rgb(35, 35, 38); pub const BG_ROW_HOVER: Color32 = Color32::from_rgb(35, 35, 38);
pub const BG_ROW_SEL: Color32 = Color32::from_rgb(42, 64, 95); // выделение — синеватое pub const BG_ROW_SEL: Color32 = Color32::from_rgb(42, 64, 95); // выделение — синеватое
pub const BG_QUEUE: Color32 = Color32::from_rgb(20, 20, 22); pub const BG_QUEUE: Color32 = Color32::from_rgb(20, 20, 22);
pub const BG_HEADER: Color32 = Color32::from_rgb(22, 22, 24); pub const BG_HEADER: Color32 = Color32::from_rgb(22, 22, 24);
// Accents // Accents
pub const ACCENT: Color32 = Color32::from_rgb(64, 130, 220); // синий акцент pub const ACCENT: Color32 = Color32::from_rgb(64, 130, 220); // синий акцент
pub const ACCENT_DIM: Color32 = Color32::from_rgb(45, 95, 165); pub const ACCENT_DIM: Color32 = Color32::from_rgb(45, 95, 165);
pub const GREEN: Color32 = Color32::from_rgb(60, 180, 100); pub const GREEN: Color32 = Color32::from_rgb(60, 180, 100);
pub const RED: Color32 = Color32::from_rgb(210, 70, 70); pub const RED: Color32 = Color32::from_rgb(210, 70, 70);
pub const YELLOW: Color32 = Color32::from_rgb(210, 160, 50); pub const YELLOW: Color32 = Color32::from_rgb(210, 160, 50);
pub const ORANGE: Color32 = Color32::from_rgb(210, 120, 50); pub const ORANGE: Color32 = Color32::from_rgb(210, 120, 50);
// Text // Text
pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(220, 220, 220); pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(220, 220, 220);
pub const TEXT_DIM: Color32 = Color32::from_rgb(130, 130, 135); pub const TEXT_DIM: Color32 = Color32::from_rgb(130, 130, 135);
pub const TEXT_HINT: Color32 = Color32::from_rgb(80, 80, 85); pub const TEXT_HINT: Color32 = Color32::from_rgb(80, 80, 85);
// Borders / separators // Borders / separators
pub const BORDER: Color32 = Color32::from_rgb(40, 40, 44); pub const BORDER: Color32 = Color32::from_rgb(40, 40, 44);
pub const SEPARATOR: Color32 = Color32::from_rgb(35, 35, 38); pub const SEPARATOR: Color32 = Color32::from_rgb(35, 35, 38);
// Size constants // Size constants
pub const SIDEBAR_W: f32 = 180.0; pub const SIDEBAR_W: f32 = 180.0;
pub const TOOLBAR_H: f32 = 40.0; pub const TOOLBAR_H: f32 = 40.0;
pub const TABBAR_H: f32 = 32.0; pub const TABBAR_H: f32 = 32.0;
pub const QUEUE_COLLAPSED_H: f32 = 32.0; pub const QUEUE_COLLAPSED_H: f32 = 32.0;
pub const QUEUE_EXPANDED_H: f32 = 160.0; pub const QUEUE_EXPANDED_H: f32 = 160.0;
pub const STATUS_H: f32 = 24.0; pub const STATUS_H: f32 = 24.0;
pub const ROW_H: f32 = 22.0; pub const ROW_H: f32 = 22.0;