diff --git a/src/ui/gui.rs b/src/ui/gui.rs index c8b1640..dabd53a 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -16,7 +16,7 @@ use crate::updater::{ }; use eframe::egui::{ self, Align, Button, CentralPanel, Color32, ColorImage, Frame, Layout, Margin, ProgressBar, - RichText, Rounding, ScrollArea, Stroke, TextureHandle, TextureOptions, Ui, Vec2, + RichText, Rounding, ScrollArea, SidePanel, Stroke, TextureHandle, TextureOptions, Ui, Vec2, }; use human_bytes::human_bytes; use qrcode::{Color as QrColor, QrCode}; @@ -82,7 +82,6 @@ pub struct ThemeColors { pub bg_card_active: Color32, pub bg_hover: Color32, pub stroke_border: Stroke, - #[allow(dead_code)] pub stroke_subtle: Stroke, pub text_primary: Color32, pub text_secondary: Color32, @@ -95,14 +94,14 @@ impl ThemeColors { if is_dark { Self { is_dark: true, - bg_app: Color32::from_rgb(15, 23, 42), + bg_app: Color32::from_rgb(11, 15, 25), bg_sidebar: Color32::from_rgb(15, 23, 42), bg_card: Color32::from_rgb(30, 41, 59), - bg_card_secondary: Color32::from_rgb(23, 32, 51), + bg_card_secondary: Color32::from_rgb(15, 23, 42), bg_card_active: Color32::from_rgb(30, 58, 138), bg_hover: Color32::from_rgb(51, 65, 85), stroke_border: Stroke::new(1.0_f32, Color32::from_rgb(51, 65, 85)), - stroke_subtle: Stroke::new(1.0_f32, Color32::from_rgb(30, 41, 59)), + stroke_subtle: Stroke::new(1.0_f32, Color32::from_rgba_premultiplied(255, 255, 255, 15)), text_primary: Color32::from_rgb(248, 250, 252), text_secondary: Color32::from_rgb(203, 213, 225), text_muted: Color32::from_rgb(148, 163, 184), @@ -111,14 +110,14 @@ impl ThemeColors { } else { Self { is_dark: false, - bg_app: Color32::from_rgb(248, 250, 252), - bg_sidebar: Color32::from_rgb(248, 250, 252), + bg_app: Color32::from_rgb(241, 245, 249), + bg_sidebar: Color32::from_rgb(255, 255, 255), bg_card: Color32::from_rgb(255, 255, 255), - bg_card_secondary: Color32::from_rgb(241, 245, 249), + bg_card_secondary: Color32::from_rgb(248, 250, 252), bg_card_active: Color32::from_rgb(219, 234, 254), bg_hover: Color32::from_rgb(226, 232, 240), - stroke_border: Stroke::new(1.0_f32, Color32::from_rgb(226, 232, 240)), - stroke_subtle: Stroke::new(1.0_f32, Color32::from_rgb(241, 245, 249)), + stroke_border: Stroke::new(1.0_f32, Color32::from_rgb(203, 213, 225)), + stroke_subtle: Stroke::new(1.0_f32, Color32::from_rgb(226, 232, 240)), text_primary: Color32::from_rgb(15, 23, 42), text_secondary: Color32::from_rgb(51, 65, 85), text_muted: Color32::from_rgb(100, 116, 139), @@ -960,48 +959,33 @@ impl ZeroSendApp { ui.separator(); ui.add_space(8.0); - // Network Status Badge Card (ZeroTier / Tailscale / LAN / VPN) - let (net_icon, net_badge_title, net_accent_color) = match self.current_iface.net_type { - NetworkType::ZeroTier => ("🪐", "ZeroTier Network", Color32::from_rgb(251, 146, 60)), - NetworkType::Tailscale => ("🪢", "Tailscale VPN", Color32::from_rgb(56, 189, 248)), - NetworkType::LAN => ("🏠", "Локальная сеть (LAN)", Color32::from_rgb(34, 197, 94)), - NetworkType::Radmin => ("🛡️", "Radmin VPN", Color32::from_rgb(168, 85, 247)), - _ => ("🌐", self.current_iface.net_type.as_str(), Color32::from_rgb(59, 130, 246)), - }; - + // Network Status Badge Card Frame::none() .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(241, 245, 249) }) .rounding(Rounding::same(8.0)) - .stroke(Stroke::new(1.0_f32, net_accent_color.linear_multiply(0.5))) - .inner_margin(Margin::symmetric(10.0, 8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new(net_icon).size(20.0)); + let dot_color = if self.current_iface.net_type == NetworkType::ZeroTier { + Color32::from_rgb(34, 197, 94) + } else { + Color32::from_rgb(59, 130, 246) + }; + ui.label(RichText::new("●").color(dot_color).size(14.0)); ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(net_badge_title) - .strong() - .size(11.5) - .color(theme.text_primary), - ); - ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(9.0)); - }); - ui.horizontal(|ui| { - let ip_str = format!("{}:{}", self.current_iface.ip, self.my_info.transfer_port); - ui.label( - RichText::new(&ip_str) - .monospace() - .size(10.0) - .color(theme.text_muted), - ); - if ui.small_button("📋").on_hover_text("Скопировать IP:Port в буфер обмена").clicked() { - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(&ip_str); - self.logs.push(format!("📋 IP скопирован: {}", ip_str)); - } - } - }); + ui.label( + RichText::new(self.current_iface.net_type.as_str()) + .strong() + .size(11.5) + .color(theme.text_primary), + ); + ui.label( + RichText::new(format!("{}:{}", self.current_iface.ip, self.my_info.transfer_port)) + .monospace() + .size(10.0) + .color(theme.text_muted), + ); }); }); }); @@ -1248,29 +1232,16 @@ impl ZeroSendApp { rename_target = Some((peer.peer_id.clone(), peer.name.clone(), curr)); } - let (net_badge_icon, net_badge_color) = if peer.net_type.contains("ZeroTier") { - ("🪐 ZeroTier", Color32::from_rgb(251, 146, 60)) - } else if peer.net_type.contains("Tailscale") { - ("🪢 Tailscale", Color32::from_rgb(56, 189, 248)) - } else if peer.net_type.contains("Radmin") { - ("🛡️ Radmin", Color32::from_rgb(168, 85, 247)) + let badge_color = if peer.net_type.contains("ZeroTier") { + Color32::from_rgb(34, 197, 94) } else { - ("🏠 LAN", Color32::from_rgb(34, 197, 94)) + Color32::from_rgb(59, 130, 246) }; - - Frame::none() - .fill(theme.bg_card_secondary) - .rounding(Rounding::same(4.0)) - .stroke(Stroke::new(1.0_f32, net_badge_color.linear_multiply(0.6))) - .inner_margin(Margin::symmetric(5.0, 1.5)) - .show(ui, |ui| { - ui.label( - RichText::new(net_badge_icon) - .size(9.5) - .color(net_badge_color) - .strong(), - ); - }); + ui.label( + RichText::new(format!("[{}]", peer.net_type)) + .size(10.5) + .color(badge_color), + ); if !peer.shared_folders.is_empty() { Frame::none() @@ -1390,12 +1361,12 @@ impl ZeroSendApp { } fn render_send_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { - // ── Top Mode Switcher Bar ── + // Mode Switcher Bar Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(14.0, 9.0)) + .inner_margin(Margin::symmetric(12.0, 8.0)) .show(ui, |ui| { ui.horizontal(|ui| { ui.label(RichText::new("📤").size(20.0)); @@ -1411,7 +1382,6 @@ impl ZeroSendApp { .color(if files_active { Color32::WHITE } else { theme.text_secondary }), ) .fill(if files_active { Color32::from_rgb(37, 99, 235) } else { theme.bg_card_secondary }) - .min_size(Vec2::new(140.0, 30.0)) .rounding(Rounding::same(6.0)); if ui.add(files_btn).clicked() { @@ -1425,7 +1395,6 @@ impl ZeroSendApp { .color(if text_active { Color32::WHITE } else { theme.text_secondary }), ) .fill(if text_active { Color32::from_rgb(37, 99, 235) } else { theme.bg_card_secondary }) - .min_size(Vec2::new(180.0, 30.0)) .rounding(Rounding::same(6.0)); if ui.add(text_btn).clicked() { @@ -1434,8 +1403,8 @@ impl ZeroSendApp { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("🔒 PIN-защита:").size(11.5).color(theme.text_muted)); - ui.add(egui::TextEdit::singleline(&mut self.recipient_pin).hint_text("PIN").password(true).desired_width(70.0)); + ui.label(RichText::new("🔒 PIN (опционально):").size(11.0).color(theme.text_muted)); + ui.add(egui::TextEdit::singleline(&mut self.recipient_pin).hint_text("PIN").desired_width(60.0)); }); }); }); @@ -1443,116 +1412,58 @@ impl ZeroSendApp { ui.add_space(8.0); - // ── Step 1: Multi-Recipient Modern Cards ── - let total_peers = self.peers.len(); - let selected_peers_count = self.selected_peer_ids.len(); - + // Step 1: Multi-Recipient Selector Card Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) .inner_margin(Margin::same(12.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label( - RichText::new(format!("Шаг 1: Выберите получателей ({}/{} выбрано)", selected_peers_count, total_peers)) - .strong() - .size(13.0) - .color(theme.text_primary), - ); + ui.label(RichText::new("Шаг 1: Выберите получателей (друзей в сети)").strong().size(13.0).color(theme.text_primary)); if !self.peers.is_empty() { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - let all_selected = selected_peers_count == total_peers; - if ui.button(RichText::new(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех друзей" }).size(11.5).color(theme.text_accent)).clicked() { - if all_selected { - self.selected_peer_ids.clear(); - } else { - self.selected_peer_ids = self.peers.iter().map(|p| p.peer_id.clone()).collect(); - } + let all_selected = self.selected_peer_ids.len() == self.peers.len(); + if ui.small_button(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех" }).clicked() { + if all_selected { + self.selected_peer_ids.clear(); + } else { + self.selected_peer_ids = self.peers.iter().map(|p| p.peer_id.clone()).collect(); } - }); + } } }); - ui.add_space(8.0); + ui.add_space(6.0); if self.peers.is_empty() { - Frame::none() - .fill(theme.bg_card_secondary) - .rounding(Rounding::same(8.0)) - .inner_margin(Margin::same(12.0)) - .show(ui, |ui| { - ui.label(RichText::new("В сети пока нет друзей онлайн. Подключитесь к ZeroTier / VPN или проверьте вкладку устройств.").color(theme.text_muted).size(12.0)); - }); + ui.label(RichText::new("В сети пока нет друзей онлайн. Подключитесь к ZeroTier или проверьте вкладку устройств.").color(theme.text_muted).size(11.5)); } else { ui.horizontal_wrapped(|ui| { for p in &self.peers { let is_selected = self.selected_peer_ids.contains(&p.peer_id); - let (name_label, has_alias) = self.get_peer_display_name(p); + let (name_label, _) = self.get_peer_display_name(p); + let chip_text = format!("{} ({})", name_label, p.ip); - let os_icon = match p.os.to_lowercase().as_str() { - "windows" => "🪟", - "linux" => "🐧", - "macos" => "🍎", - _ => "💻", - }; - - let (net_badge_icon, net_badge_color) = if p.net_type.contains("ZeroTier") { - ("🪐 ZeroTier", Color32::from_rgb(251, 146, 60)) - } else if p.net_type.contains("Tailscale") { - ("🪢 Tailscale", Color32::from_rgb(56, 189, 248)) - } else if p.net_type.contains("Radmin") { - ("🛡️ Radmin", Color32::from_rgb(168, 85, 247)) + let chip_btn = Button::new( + RichText::new(format!("{} {}", if is_selected { "✅" } else { "⬜" }, chip_text)) + .size(12.0) + .strong() + .color(if is_selected { Color32::WHITE } else { theme.text_secondary }), + ) + .fill(if is_selected { + Color32::from_rgb(37, 99, 235) } else { - ("🏠 LAN", Color32::from_rgb(34, 197, 94)) - }; - - let stroke = if is_selected { - Stroke::new(1.5_f32, Color32::from_rgb(56, 189, 248)) + theme.bg_card_secondary + }) + .stroke(if is_selected { + Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { - theme.stroke_border - }; + Stroke::NONE + }) + .rounding(Rounding::same(6.0)); - let card = Frame::none() - .fill(if is_selected { - if theme.is_dark { Color32::from_rgb(23, 37, 84) } else { Color32::from_rgb(219, 234, 254) } - } else { - theme.bg_card_secondary - }) - .rounding(Rounding::same(8.0)) - .stroke(stroke) - .inner_margin(Margin::symmetric(10.0, 6.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(if is_selected { "✅" } else { "⬜" }).size(13.0)); - ui.label(RichText::new(os_icon).size(14.0)); - ui.label( - RichText::new(&name_label) - .size(12.5) - .strong() - .color(if has_alias { Color32::from_rgb(250, 204, 21) } else { theme.text_primary }), - ); - - // Network Pill Badge - Frame::none() - .fill(theme.bg_card) - .rounding(Rounding::same(4.0)) - .stroke(Stroke::new(1.0_f32, net_badge_color.linear_multiply(0.6))) - .inner_margin(Margin::symmetric(5.0, 1.0)) - .show(ui, |ui| { - ui.label(RichText::new(net_badge_icon).size(9.5).color(net_badge_color).strong()); - }); - - ui.label(RichText::new(p.ip.to_string()).monospace().size(10.0).color(theme.text_muted)); - - if let Some(ping) = p.ping_ms { - ui.label(RichText::new(format!("⚡ {}ms", ping)).size(9.5).color(Color32::from_rgb(34, 197, 94))); - } - }); - }); - - if ui.interact(card.response.rect, card.response.id, egui::Sense::click()).clicked() { + if ui.add(chip_btn).clicked() { if is_selected { self.selected_peer_ids.remove(&p.peer_id); } else { @@ -1573,10 +1484,10 @@ impl ZeroSendApp { } fn render_send_files_subview(&mut self, ui: &mut Ui, theme: ThemeColors) { - // ── Step 2: Choose Files & Folders ── + // Step 2: Choose files Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) .inner_margin(Margin::same(12.0)) .show(ui, |ui| { @@ -1584,7 +1495,7 @@ impl ZeroSendApp { ui.label(RichText::new("Шаг 2: Выберите файлы для отправки").strong().size(13.0).color(theme.text_primary)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if !self.queued_files.is_empty() && ui.button(RichText::new("🗑 Очистить очередь").color(Color32::from_rgb(239, 68, 68))).clicked() { + if !self.queued_files.is_empty() && ui.button(RichText::new("🗑 Очистить список").color(Color32::from_rgb(239, 68, 68))).clicked() { self.queued_files.clear(); } }); @@ -1593,12 +1504,15 @@ impl ZeroSendApp { ui.add_space(8.0); ui.horizontal(|ui| { - let add_files_btn = Button::new(RichText::new("📄 Добавить файлы...").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)) - .min_size(Vec2::new(145.0, 32.0)) - .rounding(Rounding::same(6.0)); - - if ui.add(add_files_btn).clicked() { + if ui + .add( + Button::new(RichText::new("📄 Добавить файлы...").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(59, 130, 246)) + .min_size(Vec2::new(140.0, 32.0)) + .rounding(Rounding::same(6.0)), + ) + .clicked() + { if let Some(files) = FileDialog::new().pick_files() { for f in files { if !self.queued_files.contains(&f) { @@ -1608,12 +1522,15 @@ impl ZeroSendApp { } } - let add_folder_btn = Button::new(RichText::new("📁 Добавить папку...").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(79, 70, 229)) - .min_size(Vec2::new(145.0, 32.0)) - .rounding(Rounding::same(6.0)); - - if ui.add(add_folder_btn).clicked() { + if ui + .add( + Button::new(RichText::new("📁 Добавить папку...").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(79, 70, 229)) + .min_size(Vec2::new(140.0, 32.0)) + .rounding(Rounding::same(6.0)), + ) + .clicked() + { if let Some(folder) = FileDialog::new().pick_folder() { if !self.queued_files.contains(&folder) { self.queued_files.push(folder); @@ -1621,12 +1538,15 @@ impl ZeroSendApp { } } - let paste_btn = Button::new(RichText::new("📸 Вставить скриншот (Ctrl+V)").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(147, 51, 234)) - .min_size(Vec2::new(195.0, 32.0)) - .rounding(Rounding::same(6.0)); - - if ui.add(paste_btn).clicked() { + if ui + .add( + Button::new(RichText::new("📸 Вставить скриншот (Ctrl+V)").color(Color32::WHITE)) + .fill(Color32::from_rgb(147, 51, 234)) + .min_size(Vec2::new(190.0, 32.0)) + .rounding(Rounding::same(6.0)), + ) + .clicked() + { self.try_paste_clipboard_image(); } }); @@ -1642,7 +1562,7 @@ impl ZeroSendApp { ui.horizontal(|ui| { ui.label( RichText::new(format!( - "Выбрано: {} элементов ({}) • Получателей: {}", + "Выбрано: {} элементов ({}) | Получателей: {}", self.queued_files.len(), human_bytes(total_size as f64), self.selected_peer_ids.len() @@ -1652,19 +1572,13 @@ impl ZeroSendApp { .color(theme.text_primary), ); if self.auto_zip_folders { - Frame::none() - .fill(Color32::from_rgba_premultiplied(34, 197, 94, 25)) - .rounding(Rounding::same(4.0)) - .inner_margin(Margin::symmetric(6.0, 2.0)) - .show(ui, |ui| { - ui.label(RichText::new("⚡ Zero-Disk Streaming (tar.zst)").size(10.5).color(Color32::from_rgb(34, 197, 94)).strong()); - }); + ui.label(RichText::new("• (Папки стримятся как tar.zst — без диска)").size(11.0).color(Color32::from_rgb(34, 197, 94))); } }); ui.add_space(6.0); - // ── Files Dropzone Box ── + // Files Dropzone Box Frame::none() .fill(theme.bg_card_secondary) .rounding(Rounding::same(8.0)) @@ -1674,13 +1588,11 @@ impl ZeroSendApp { ScrollArea::vertical().max_height(200.0).show(ui, |ui| { if self.queued_files.is_empty() { ui.vertical_centered(|ui| { - ui.add_space(24.0); - ui.label(RichText::new("📂").size(32.0)); + ui.add_space(20.0); + ui.label(RichText::new("📂 Перетащите сюда файлы или папки (Drag & Drop)").size(13.5).strong().color(theme.text_muted)); ui.add_space(4.0); - ui.label(RichText::new("Перетащите файлы или папки сюда (Drag & Drop)").size(13.5).strong().color(theme.text_primary)); - ui.add_space(2.0); - ui.label(RichText::new("или нажмите Ctrl+V для быстрой вставки файлов и скриншотов из буфера обмена").size(11.5).color(theme.text_muted)); - ui.add_space(24.0); + ui.label(RichText::new("или вставьте скриншот из буфера обмена (Ctrl+V)").size(11.0).color(theme.text_muted)); + ui.add_space(20.0); }); } else { let mut to_remove = None; @@ -1712,7 +1624,7 @@ impl ZeroSendApp { ui.label(RichText::new(format!("({})", size_str)).size(11.0).color(theme.text_muted)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.small_button(RichText::new("✕").color(Color32::from_rgb(239, 68, 68))).on_hover_text("Удалить из очереди").clicked() { + if ui.small_button("✕").on_hover_text("Удалить из очереди").clicked() { to_remove = Some(idx); } }); @@ -1731,15 +1643,9 @@ impl ZeroSendApp { ui.add_space(10.0); - // ── Step 3: Big Send Button ── + // Step 3: Big Send Button let num_recipients = self.selected_peer_ids.len(); - let total_size: u64 = self - .queued_files - .iter() - .filter_map(|p| std::fs::metadata(p).ok().map(|m| m.len())) - .sum(); let can_send = !self.queued_files.is_empty() && num_recipients > 0; - let send_btn_text = if num_recipients == 1 { let target_name = self .peers @@ -1747,11 +1653,9 @@ impl ZeroSendApp { .find(|p| self.selected_peer_ids.contains(&p.peer_id)) .map(|p| self.get_peer_display_name(p).0) .unwrap_or_else(|| "...".to_string()); - format!("🚀 Отправить {} файл(ов) для {} ({})", self.queued_files.len(), target_name, human_bytes(total_size as f64)) - } else if num_recipients > 1 { - format!("🚀 Отправить {} файл(ов) для {} получателей ({})", self.queued_files.len(), num_recipients, human_bytes(total_size as f64)) + format!("🚀 Отправить {} файл(ов) для {}", self.queued_files.len(), target_name) } else { - "🚀 Выберите получателя и добавьте файлы для отправки".to_string() + format!("🚀 Отправить {} файл(ов) для {} получателей", self.queued_files.len(), num_recipients) }; let send_btn = Button::new(RichText::new(send_btn_text).size(14.5).strong().color(Color32::WHITE)) @@ -1760,8 +1664,7 @@ impl ZeroSendApp { } else { if theme.is_dark { Color32::from_rgb(51, 65, 85) } else { Color32::from_rgb(203, 213, 225) } }) - .stroke(if can_send { Stroke::new(1.0_f32, Color32::from_rgb(52, 211, 153)) } else { Stroke::NONE }) - .min_size(Vec2::new(ui.available_width(), 44.0)) + .min_size(Vec2::new(ui.available_width(), 42.0)) .rounding(Rounding::same(8.0)); if ui.add_enabled(can_send, send_btn).clicked() { @@ -2828,63 +2731,46 @@ impl ZeroSendApp { if online_peers_with_shares.is_empty() { Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(12.0)) + .rounding(Rounding::same(10.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(48.0)) + .inner_margin(Margin::same(40.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { - ui.label(RichText::new("🌐").size(48.0)); - ui.add_space(8.0); + ui.label(RichText::new("🌐").size(40.0)); ui.label( - RichText::new("В сети пока нет обнаруженных общих папок") - .size(17.0) + RichText::new("No shared folders discovered on your network.") + .size(16.0) .strong() .color(theme.text_primary), ); ui.add_space(6.0); ui.label( RichText::new( - "Когда ваши друзья в ZeroTier, Tailscale, Radmin VPN или LAN откроют доступ к папкам, они мгновенно появятся здесь с поддержкой стриминга и быстрой загрузки.", + "When your friends on ZeroTier / LAN add shared folders, they will automatically appear here with streaming and fast download support.", ) - .size(12.5) - .color(theme.text_secondary), + .size(12.0) + .color(theme.text_muted), ); - ui.add_space(14.0); - let refresh_btn = Button::new(RichText::new("🔄 Обновить список (Sync / Refresh)").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)) - .min_size(Vec2::new(200.0, 32.0)); - if ui.add(refresh_btn).clicked() { - self.sync_all_network_shares(); - self.refresh_current_share_tree(); - } }); }); return; } - let total_shared_folders_count: usize = online_peers_with_shares.iter().map(|p| p.shared_folders.len()).sum(); - ui.horizontal_top(|ui| { - // ── Left Column: Modern Friends & Shares Sidebar (270px) ── - ui.allocate_ui(Vec2::new(270.0, ui.available_height()), |ui| { + // Left Column: Peer & Folder Selector (250px) + ui.allocate_ui(Vec2::new(250.0, ui.available_height()), |ui| { Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(12.0, 10.0)) + .inner_margin(Margin::same(10.0)) .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new("🌐 Общие папки").strong().size(13.5).color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - Frame::none() - .fill(theme.bg_card_secondary) - .rounding(Rounding::same(10.0)) - .inner_margin(Margin::symmetric(6.0, 2.0)) - .show(ui, |ui| { - ui.label(RichText::new(format!("{}", total_shared_folders_count)).size(10.5).strong().color(Color32::from_rgb(56, 189, 248))); - }); - }); - }); + ui.label( + RichText::new("Friends & Shares") + .strong() + .size(13.0) + .color(theme.text_primary), + ); ui.add_space(6.0); ui.separator(); ui.add_space(6.0); @@ -2892,74 +2778,47 @@ impl ZeroSendApp { ScrollArea::vertical().show(ui, |ui| { for peer in &online_peers_with_shares { let (peer_display, _) = self.get_peer_display_name(peer); - - Frame::none() - .fill(theme.bg_card_secondary) - .rounding(Rounding::same(8.0)) - .stroke(theme.stroke_border) - .inner_margin(Margin::same(8.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(10.0)); - ui.label(RichText::new(&peer_display).strong().size(12.0).color(theme.text_primary)); - if let Some(ping) = peer.ping_ms { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.label(RichText::new(format!("⚡ {}ms", ping)).size(9.5).color(Color32::from_rgb(34, 197, 94))); - }); - } - }); - ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(11.0)); + ui.label(RichText::new(&peer_display).strong().color(theme.text_primary)); + }); - for folder in &peer.shared_folders { - let is_selected = self.selected_peer_id_for_share.as_deref() == Some(&peer.peer_id) - && self.selected_folder_id_for_share.as_deref() == Some(&folder.id); + for folder in &peer.shared_folders { + let is_selected = self.selected_peer_id_for_share.as_deref() == Some(&peer.peer_id) + && self.selected_folder_id_for_share.as_deref() == Some(&folder.id); - let badge_color = if folder.read_only { Color32::from_rgb(59, 130, 246) } else { Color32::from_rgb(34, 197, 94) }; - let badge_text = if folder.read_only { "RO" } else { "RW" }; + let badge = if folder.read_only { "RO" } else { "RW" }; + let btn_text = format!("📁 {} [{}]", folder.name, badge); - let btn = Button::new( - RichText::new(format!("📁 {}", folder.name)) - .size(11.5) - .strong() - .color(if is_selected { theme.text_accent } else { theme.text_primary }), - ) - .fill(if is_selected { theme.bg_card_active } else { Color32::TRANSPARENT }) - .stroke(if is_selected { Stroke::new(1.0_f32, theme.text_accent) } else { Stroke::NONE }) - .min_size(Vec2::new(ui.available_width(), 26.0)) - .rounding(Rounding::same(6.0)); + let btn = Button::new(RichText::new(btn_text).size(12.0).color(if is_selected { + theme.text_accent + } else { + theme.text_secondary + })) + .fill(if is_selected { + theme.bg_card_active + } else { + Color32::TRANSPARENT + }) + .min_size(Vec2::new(ui.available_width(), 26.0)) + .rounding(Rounding::same(4.0)); - ui.horizontal(|ui| { - if ui.add(btn).clicked() { - self.selected_peer_id_for_share = Some(peer.peer_id.clone()); - self.selected_folder_id_for_share = Some(folder.id.clone()); - self.current_share_subpath = String::new(); - self.refresh_current_share_tree(); - } - // Pill badge - Frame::none() - .fill(theme.bg_card) - .rounding(Rounding::same(4.0)) - .stroke(Stroke::new(1.0_f32, badge_color)) - .inner_margin(Margin::symmetric(4.0, 1.0)) - .show(ui, |ui| { - ui.label(RichText::new(badge_text).size(9.0).color(badge_color)); - }); - if folder.requires_pin { - ui.label(RichText::new("🔒").size(10.0)); - } - }); - ui.add_space(2.0); - } - }); + if ui.add(btn).clicked() { + self.selected_peer_id_for_share = Some(peer.peer_id.clone()); + self.selected_folder_id_for_share = Some(folder.id.clone()); + self.current_share_subpath = String::new(); + self.refresh_current_share_tree(); + } + } ui.add_space(6.0); } }); }); }); - ui.add_space(8.0); + ui.add_space(10.0); - // ── Right Column: Active Share Modern Explorer ── + // Right Column: Active Share Explorer ui.vertical(|ui| { if let (Some(peer_id), Some(folder_id)) = ( self.selected_peer_id_for_share.clone(), @@ -2969,63 +2828,39 @@ impl ZeroSendApp { let peer = match peer_opt { Some(p) => p, None => { - Frame::none() - .fill(theme.bg_card) - .rounding(Rounding::same(8.0)) - .stroke(theme.stroke_border) - .inner_margin(Margin::same(20.0)) - .show(ui, |ui| { - ui.label(RichText::new("⚠️ Выбранный узел временно не в сети.").color(Color32::from_rgb(239, 68, 68))); - }); + ui.label("Selected peer is offline."); return; } }; let (peer_name, _) = self.get_peer_display_name(&peer); - // ── Top Explorer Header & Interactive Breadcrumbs Bar ── + // Top Explorer Header & Breadcrumbs Frame::none() .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(10.0, 8.0)) + .inner_margin(Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - if ui.button(RichText::new("🏠 Корень").strong().color(theme.text_accent)).clicked() { + if ui.button("🏠 Root").clicked() { self.current_share_subpath = String::new(); self.refresh_current_share_tree(); } - let mut navigate_subpath = None; if !self.current_share_subpath.is_empty() { - let parts: Vec = self.current_share_subpath.split('/').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(); - let mut accumulated = String::new(); - for (idx, part) in parts.iter().enumerate() { - ui.label(RichText::new("/").color(theme.text_muted)); - if !accumulated.is_empty() { - accumulated.push('/'); - } - accumulated.push_str(part); - let is_last = idx == parts.len() - 1; - if is_last { - ui.label(RichText::new(part).strong().color(theme.text_primary)); - } else { - let acc_clone = accumulated.clone(); - if ui.link(RichText::new(part).color(theme.text_accent)).clicked() { - navigate_subpath = Some(acc_clone); - } - } - } - } - - if let Some(sub) = navigate_subpath { - self.current_share_subpath = sub; - self.refresh_current_share_tree(); + ui.label("/"); + ui.label( + RichText::new(&self.current_share_subpath) + .monospace() + .strong() + .color(theme.text_primary), + ); } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { // Grep search button - if ui.button(RichText::new("🔎 Grep по коду").color(Color32::from_rgb(56, 189, 248))).clicked() { + if ui.button(RichText::new("🔎 Поиск текста/кода").color(Color32::from_rgb(56, 189, 248))).clicked() { self.show_grep_modal = true; self.grep_query.clear(); self.grep_results.clear(); @@ -3038,7 +2873,7 @@ impl ZeroSendApp { let peer_name_clone = peer_name.clone(); let f_name = peer.shared_folders.iter().find(|f| f.id == folder_id_clone).map(|f| f.name.clone()).unwrap_or_else(|| "Shared".to_string()); - if ui.button(RichText::new("🔄 Синхронизация").color(Color32::from_rgb(168, 85, 247))).clicked() { + if ui.button(RichText::new("🔄 Авто-Синхронизация").color(Color32::from_rgb(168, 85, 247))).clicked() { self.new_sync_peer_id = peer_id_clone; self.new_sync_folder_id = folder_id_clone; self.new_sync_name = format!("Синхронизация {} ({})", f_name, peer_name_clone); @@ -3059,7 +2894,7 @@ impl ZeroSendApp { if let Some(tree) = &self.current_share_tree { if !tree.read_only { let upload_btn = Button::new( - RichText::new("⬆️ Загрузить файл") + RichText::new("⬆️ Upload File") .strong() .color(Color32::WHITE), ) @@ -3082,16 +2917,17 @@ impl ZeroSendApp { tokio::spawn(async move { for f in files { - let _ = log_tx.send(format!("⬆️ Загрузка {:?} в общую папку...", f.file_name())); + let _ = log_tx.send(format!("⬆️ Uploading {:?} to share...", f.file_name())); match client.upload_to_shared_folder(&peer_url, &f_id, &sub, &f, pin.as_deref()).await { Ok(_) => { - let _ = log_tx.send(format!("✅ Загружено {:?}", f.file_name())); + let _ = log_tx.send(format!("✅ Uploaded {:?}", f.file_name())); } Err(e) => { - let _ = log_tx.send(format!("❌ Ошибка загрузки: {}", e)); + let _ = log_tx.send(format!("❌ Upload failed: {}", e)); } } } + // Refresh tree after upload if let Ok(tree) = client.fetch_shared_tree(&peer_url, &f_id, Some(&sub), pin.as_deref()).await { let _ = tx.send(Some(tree)); } @@ -3104,9 +2940,9 @@ impl ZeroSendApp { }); }); - ui.add_space(6.0); + ui.add_space(8.0); - // ── Multi-select Batch Action Toolbar ── + // Multi-select batch action toolbar if !self.selected_share_items.is_empty() { Frame::none() .fill(if theme.is_dark { Color32::from_rgb(23, 37, 84) } else { Color32::from_rgb(219, 234, 254) }) @@ -3115,15 +2951,15 @@ impl ZeroSendApp { .inner_margin(Margin::symmetric(12.0, 8.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new(format!("Выбрано файлов: {}", self.selected_share_items.len())).strong().color(theme.text_primary)); + ui.label(RichText::new(format!("Selected {} item(s)", self.selected_share_items.len())).strong().color(theme.text_primary)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("✖ Снять выбор").clicked() { + if ui.button("Clear Selection").clicked() { self.selected_share_items.clear(); } let dl_btn = Button::new( - RichText::new(format!("⬇️ Скачать выбранное ({})", self.selected_share_items.len())) + RichText::new(format!("⬇️ Download Selected ({})", self.selected_share_items.len())) .strong() .color(Color32::WHITE), ) @@ -3131,8 +2967,10 @@ impl ZeroSendApp { .rounding(Rounding::same(4.0)); if ui.add(dl_btn).clicked() { + self.switch_tab(Tab::Transfers); let client = self.client.clone(); let peer_url = peer.transfer_url(); + let peer_name_clone = peer_name.clone(); let f_id = folder_id.clone(); let items: Vec = self.selected_share_items.iter().cloned().collect(); let dl_dir = self.download_dir.clone(); @@ -3144,10 +2982,6 @@ impl ZeroSendApp { let log_tx = self.log_tx.clone(); let transfer_tx = self.transfer_tx.clone(); let sound = self.sound_notifications; - let peer_display_name = peer_name.clone(); - - // Switch tab to Transfers! - self.switch_tab(Tab::Transfers); tokio::spawn(async move { for item_rel in items { @@ -3156,19 +2990,11 @@ impl ZeroSendApp { .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_else(|| "downloaded".to_string()); let dest = dl_dir.join(&fname); - let _ = log_tx.send(format!("⬇️ Загрузка {} от {}...", fname, peer_display_name)); - let t_tx = transfer_tx.clone(); - let _ = client.download_shared_file_with_progress( - &peer_url, - &f_id, - &item_rel, - &dest, - pin.as_deref(), - &peer_display_name, - move |prog| { - let _ = t_tx.send(prog); - }, - ).await; + let _ = log_tx.send(format!("⬇️ Downloading {}...", fname)); + let tx_clone = transfer_tx.clone(); + let _ = client.download_shared_file_with_progress(&peer_url, &f_id, &item_rel, &dest, pin.as_deref(), &peer_name_clone, move |p| { + let _ = tx_clone.send(p); + }).await; } if sound { crate::ui::sound::play_transfer_sound(); @@ -3182,12 +3008,12 @@ impl ZeroSendApp { ui.add_space(6.0); } - // ── Explorer File Table & Cards ── + // Explorer File Table if self.share_tree_loading { ui.vertical_centered(|ui| { ui.add_space(30.0); ui.spinner(); - ui.label("Загрузка содержимого папки..."); + ui.label("Loading shared contents..."); }); } else if let Some(tree) = self.current_share_tree.clone() { let is_writable_share = !tree.read_only; @@ -3201,8 +3027,8 @@ impl ZeroSendApp { .inner_margin(Margin::symmetric(10.0, 6.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label("📁 ⮤ .."); - if ui.button(RichText::new("На уровень выше").strong().color(theme.text_accent)).clicked() { + ui.label("📁 .."); + if ui.button("Parent Folder").clicked() { let p = std::path::Path::new(&self.current_share_subpath); let parent_str = p.parent().map(|p| p.to_string_lossy().replace('\\', "/")).unwrap_or_default(); self.current_share_subpath = parent_str; @@ -3225,7 +3051,7 @@ impl ZeroSendApp { .fill(theme.bg_card) .rounding(Rounding::same(6.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(10.0, 7.0)) + .inner_margin(Margin::symmetric(10.0, 6.0)) .show(ui, |ui| { ui.horizontal(|ui| { // Checkbox for multi-select @@ -3251,7 +3077,7 @@ impl ZeroSendApp { ui.label(RichText::new(&entry.name).strong().color(theme.text_primary)); } - // Category Badge + // Badge Frame::none() .fill(theme.bg_card_secondary) .rounding(Rounding::same(3.0)) @@ -3270,16 +3096,14 @@ impl ZeroSendApp { } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - // ── Action: Download ── + // Action: Download let is_dir = entry.is_dir; - let down_label = if is_dir { "⬇️ Скачать папку (Zip)" } else { "⬇️ Скачать" }; - let dl_btn = Button::new(RichText::new(down_label).strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)) - .rounding(Rounding::same(4.0)); - - if ui.add(dl_btn).clicked() { + let down_label = if is_dir { "⬇️ Zip" } else { "⬇️ Download" }; + if ui.button(down_label).clicked() { + self.switch_tab(Tab::Transfers); let client = self.client.clone(); let peer_url = peer.transfer_url(); + let peer_name_clone = peer_name.clone(); let f_id = folder_id.clone(); let sub = entry.relative_path.clone(); let out_name = if is_dir { format!("{}.zip", entry.name) } else { entry.name.clone() }; @@ -3292,41 +3116,27 @@ impl ZeroSendApp { let log_tx = self.log_tx.clone(); let transfer_tx = self.transfer_tx.clone(); let sound = self.sound_notifications; - let peer_display_name = peer_name.clone(); - - // Switch tab to Transfers immediately! - self.switch_tab(Tab::Transfers); tokio::spawn(async move { - let _ = log_tx.send(format!("⬇️ Загрузка {} от {}...", out_name, peer_display_name)); - let t_tx = transfer_tx.clone(); - let res = client.download_shared_file_with_progress( - &peer_url, - &f_id, - &sub, - &dest, - pin.as_deref(), - &peer_display_name, - move |prog| { - let _ = t_tx.send(prog); - }, - ).await; - - match res { + let _ = log_tx.send(format!("⬇️ Downloading {}...", out_name)); + let tx_clone = transfer_tx.clone(); + match client.download_shared_file_with_progress(&peer_url, &f_id, &sub, &dest, pin.as_deref(), &peer_name_clone, move |p| { + let _ = tx_clone.send(p); + }).await { Ok(bytes) => { - let _ = log_tx.send(format!("✅ Загружено {} ({})", out_name, human_bytes(bytes as f64))); + let _ = log_tx.send(format!("✅ Downloaded {} ({})", out_name, human_bytes(bytes as f64))); if sound { crate::ui::sound::play_transfer_sound(); } } Err(e) => { - let _ = log_tx.send(format!("❌ Ошибка загрузки {}: {}", out_name, e)); + let _ = log_tx.send(format!("❌ Download failed: {}", e)); } } }); } - // ── Action: Audio In-App Player ── + // Action: Audio In-App Player if badge_text == "AUDIO" { let stream_url = format!( "{}/api/shared/stream/{}/{}", @@ -3335,7 +3145,7 @@ impl ZeroSendApp { entry.relative_path.trim_start_matches('/') ); - if ui.button(RichText::new("🎵 Играть").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() { + if ui.button(RichText::new("🎵 Play in App").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() { self.active_audio = Some(AudioTrackInfo { name: entry.name.clone(), url: stream_url.clone(), @@ -3346,7 +3156,7 @@ impl ZeroSendApp { } } - // ── Action: Video Watch Party & Stream ── + // Action: Video Watch Party & Stream if badge_text == "VIDEO" { let stream_url = format!( "{}/api/shared/stream/{}/{}", @@ -3367,18 +3177,18 @@ impl ZeroSendApp { timestamp_ms: chrono::Local::now().timestamp_millis() as u64, }); self.is_in_watch_party = true; - self.logs.push(format!("🎬 Watch Party создана для '{}'", entry.name)); + self.logs.push(format!("🎬 Watch Party created for '{}'", entry.name)); let _ = std::process::Command::new("explorer").arg(&stream_url).spawn(); } - if ui.button(RichText::new("▶️ Стрим").color(Color32::from_rgb(56, 189, 248))).clicked() { + if ui.button(RichText::new("▶️ Stream").color(Color32::from_rgb(56, 189, 248))).clicked() { let _ = std::process::Command::new("explorer").arg(&stream_url).spawn(); } } - // ── Action: Vim View / Edit (for code & text) ── + // Action: Vim View / Edit (for code & text) if badge_text == "CODE" || badge_text == "DOC" || entry.name.ends_with(".txt") || entry.name.ends_with(".log") || entry.name.ends_with(".md") { - let edit_label = if is_writable_share { "✏️ Vim Редактор" } else { "👁 Vim Просмотр" }; + let edit_label = if is_writable_share { "✏️ Vim Edit" } else { "👁 Vim View" }; let btn_color = if is_writable_share { Color32::from_rgb(34, 197, 94) } else { Color32::from_rgb(20, 184, 166) }; if ui.button(RichText::new(edit_label).color(btn_color).strong()).clicked() { @@ -3414,17 +3224,15 @@ impl ZeroSendApp { .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(24.0)) + .inner_margin(Margin::same(20.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { - ui.label(RichText::new("🔒 Папка защищена PIN-кодом или пуста").strong().size(14.5).color(theme.text_primary)); - ui.add_space(8.0); + ui.label(RichText::new("🔒 Folder Locked or Empty").strong().size(14.0).color(theme.text_primary)); + ui.add_space(6.0); ui.horizontal(|ui| { - ui.label("Введите PIN-код:"); + ui.label("Enter PIN if required:"); ui.add(egui::TextEdit::singleline(&mut self.share_pin_input).password(true).desired_width(120.0)); - let unlock_btn = Button::new(RichText::new("🔓 Разблокировать / Обновить").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)); - if ui.add(unlock_btn).clicked() { + if ui.button("Unlock / Refresh").clicked() { self.refresh_current_share_tree(); } }); @@ -3434,16 +3242,12 @@ impl ZeroSendApp { } else { Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(48.0)) + .inner_margin(Margin::same(40.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { - ui.label(RichText::new("📂").size(40.0)); - ui.add_space(6.0); - ui.label(RichText::new("Выберите общую папку друга на панели слева").strong().size(15.0).color(theme.text_primary)); - ui.add_space(4.0); - ui.label(RichText::new("Вы сможете просматривать структуру файлов, стримить музыку и видео, открывать код во встроенном редакторе Vim и скачивать нужные файлы.").size(12.0).color(theme.text_muted)); + ui.label(RichText::new("👈 Select a shared folder on the left to browse files, watch streams, and view code in Vim.").color(theme.text_muted)); }); }); } @@ -4319,11 +4123,8 @@ impl ZeroSendApp { ui.label(RichText::new("v2.5.5").strong().color(Color32::from_rgb(34, 197, 94))); ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY)); }); - ui.label(" • 🎨 Редизайн вкладки «Отправить файлы»: Удобные карточки друзей с аватарками ОС, бейджами сетей (🪐 ZeroTier, 🪢 Tailscale, 🏠 LAN), пингом, стильной зоной Drag & Drop и новой большой кнопкой отправки."); - ui.label(" • 🪐 Премиальный статус-блок ZeroTier & IP: Новая фирменная плашка сети в боковой панели с индикатором онлайна, иконкой 🪐 ZeroTier и кнопкой копирования IP:Port в один клик."); - ui.label(" • 🚀 Мгновенный переход в «Передачи и файлы»: При скачивании любых файлов или архивов из общих папок друзей приложение сразу переключается во вкладку передач с отображением живой скорости (MB/s) и прогресса."); - ui.label(" • 🌐 Премиальный проводник Сетевых общих папок: Удобная боковая панель с пингом и статусами друзей, интерактивные хлебные крошки (breadcrumbs), красивые карточки файлов по категориям и мульти-загрузка."); - ui.label(" • 🔄 Все ключевые улучшения v2.5.4: авто-обнаружение папок в реальном времени, отслеживание скачиваний из общих папок, прозрачный стриминг папок и сохранение истории передач на диск."); + ui.label(" • 📥 Мгновенное перенаправление в передачу файлов: При начале скачивания из общей папки приложение сразу переключается во вкладку передач и отображает статус загрузки."); + ui.label(" • ⚡ Отслеживание скорости скачивания общих файлов в реальном времени с выводом прогресса в TransferProgress."); ui.add_space(8.0); ui.separator(); ui.add_space(8.0); @@ -4336,6 +4137,8 @@ impl ZeroSendApp { ui.label(" • 📥 Отслеживание скачиваний из общих папок: Когда друзья качают файлы или архивы из вашей общей папки, передача в реальном времени отображается в списке передач со скоростью."); ui.label(" • 📁 Прозрачный стриминг папок: Получатель видит настоящее имя папки, примерный размер, прогресс и скорость в MB/s с момента старта стрима."); ui.label(" • 💾 Постоянное сохранение истории передач: История завершенных и прошлых передач теперь надежно сохраняется на диск и не стирается при перезапуске приложения."); + ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню."); + ui.label(" • ⚡ TCP Socket Tuning (4MB буферы, TCP_NODELAY), Zero-Drop Auto-Recovery (до 8 попыток), mDNS/SSDP и Blake3 Delta-Sync."); ui.add_space(8.0); ui.separator(); ui.add_space(8.0); @@ -5003,148 +4806,124 @@ impl eframe::App for ZeroSendApp { style.visuals.dark_mode = is_dark; style.visuals.panel_fill = theme.bg_app; style.visuals.window_fill = theme.bg_card; - style.visuals.window_stroke = Stroke::NONE; - style.visuals.faint_bg_color = theme.bg_card_secondary; - style.visuals.extreme_bg_color = theme.bg_card_secondary; style.visuals.override_text_color = Some(theme.text_primary); style.visuals.widgets.noninteractive.bg_fill = theme.bg_card; - style.visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; - style.visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0_f32, theme.text_muted); style.visuals.widgets.inactive.bg_fill = theme.bg_card_secondary; - style.visuals.widgets.inactive.bg_stroke = Stroke::NONE; style.visuals.widgets.inactive.rounding = Rounding::same(6.0); style.visuals.widgets.hovered.bg_fill = theme.bg_hover; - style.visuals.widgets.hovered.bg_stroke = Stroke::NONE; style.visuals.widgets.hovered.rounding = Rounding::same(6.0); style.visuals.widgets.active.bg_fill = Color32::from_rgb(37, 99, 235); style.visuals.widgets.active.rounding = Rounding::same(6.0); ctx.set_style(style); + SidePanel::left("sidebar_panel") + .resizable(false) + .exact_width(195.0) + .frame( + Frame::none() + .fill(theme.bg_sidebar) + .stroke(theme.stroke_subtle) + .inner_margin(Margin::same(12.0)), + ) + .show(ctx, |ui| { + self.render_sidebar(ctx, ui, theme); + }); + CentralPanel::default() .frame( Frame::none() .fill(theme.bg_app) - .inner_margin(Margin::ZERO), + .inner_margin(Margin::same(20.0)), ) .show(ctx, |ui| { - ui.spacing_mut().item_spacing = Vec2::ZERO; + // Top Update Banner if update available on Gitea + if self.show_update_banner { + if let Some(info) = &self.update_info { + if info.has_update { + let mut dismiss = false; + let mut trigger_install = None; + let mut open_gitea = false; + let gitea_url = info.gitea_url.clone(); + let latest_ver = info.latest_version.clone(); + let download_url = info.download_url.clone(); - ui.horizontal(|ui| { - // ── Left Sidebar (200px) ── - ui.allocate_ui(Vec2::new(200.0, ui.available_height()), |ui| { - Frame::none() - .fill(theme.bg_sidebar) - .inner_margin(Margin::same(12.0)) - .show(ui, |ui| { - ui.set_min_width(176.0); - ui.set_min_height(ui.available_height()); - self.render_sidebar(ctx, ui, theme); - }); - }); + Frame::none() + .fill(if theme.is_dark { Color32::from_rgb(17, 43, 31) } else { Color32::from_rgb(220, 252, 231) }) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 197, 94))) + .rounding(Rounding::same(8.0)) + .inner_margin(Margin::symmetric(14.0, 10.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("✨").size(16.0)); + ui.label( + RichText::new(format!("Update Available: ZeroSend v{} is out on Gitea!", latest_ver)) + .strong() + .color(if theme.is_dark { Color32::WHITE } else { Color32::from_rgb(20, 83, 45) }) + ); - // ── Right Main Content Area ── - let content_width = ui.available_width(); - let content_height = ui.available_height(); - ui.allocate_ui(Vec2::new(content_width, content_height), |ui| { - Frame::none() - .fill(theme.bg_app) - .inner_margin(Margin::same(20.0)) - .show(ui, |ui| { - ui.set_min_width(content_width - 40.0); - ui.set_min_height(content_height - 40.0); - ui.spacing_mut().item_spacing = Vec2::new(8.0, 8.0); - - // Top Update Banner if update available on Gitea - if self.show_update_banner { - if let Some(info) = &self.update_info { - if info.has_update { - let mut dismiss = false; - let mut trigger_install = None; - let mut open_gitea = false; - let gitea_url = info.gitea_url.clone(); - let latest_ver = info.latest_version.clone(); - let download_url = info.download_url.clone(); - - Frame::none() - .fill(if theme.is_dark { Color32::from_rgb(17, 43, 31) } else { Color32::from_rgb(220, 252, 231) }) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 197, 94))) - .rounding(Rounding::same(8.0)) - .inner_margin(Margin::symmetric(14.0, 10.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new("✨").size(16.0)); - ui.label( - RichText::new(format!("Update Available: ZeroSend v{} is out on Gitea!", latest_ver)) - .strong() - .color(if theme.is_dark { Color32::WHITE } else { Color32::from_rgb(20, 83, 45) }) - ); - - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button(RichText::new("✕").size(12.0).color(theme.text_muted)).clicked() { - dismiss = true; - } - - if ui.button(RichText::new("🌐 View on Gitea").size(11.5)).clicked() { - open_gitea = true; - } - - if let Some(dl) = download_url { - if !self.is_updating { - let update_btn = Button::new( - RichText::new("🚀 Update Now").strong().size(12.0).color(Color32::WHITE) - ) - .fill(Color32::from_rgb(34, 197, 94)) - .rounding(Rounding::same(6.0)); - - if ui.add(update_btn).clicked() { - trigger_install = Some(dl); - } - } else { - ui.label(RichText::new("⏳ Updating...").color(Color32::from_rgb(34, 197, 94)).strong()); - } - } - }); - }); - }); - - if dismiss { - self.show_update_banner = false; - } - if open_gitea { - let _ = std::process::Command::new("explorer").arg(&gitea_url).spawn(); - } - if let Some(dl) = trigger_install { - self.trigger_install_update(dl); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button(RichText::new("✕").size(12.0).color(theme.text_muted)).clicked() { + dismiss = true; } - ui.add_space(10.0); - } - } - } + if ui.button(RichText::new("🌐 View on Gitea").size(11.5)).clicked() { + open_gitea = true; + } - // Fluid Tab Transition Animation (Smooth Slide-in & Fade-in) - let elapsed = self.last_tab_change.elapsed().as_secs_f32(); - let duration = 0.20; // 200 ms - let progress = (elapsed / duration).min(1.0); - if progress < 1.0 { - ctx.request_repaint(); - } - // Cubic ease-out curve - let ease = 1.0 - (1.0 - progress).powi(3); - let slide_offset = (1.0 - ease) * 12.0; - ui.add_space(slide_offset); + if let Some(dl) = download_url { + if !self.is_updating { + let update_btn = Button::new( + RichText::new("🚀 Update Now").strong().size(12.0).color(Color32::WHITE) + ) + .fill(Color32::from_rgb(34, 197, 94)) + .rounding(Rounding::same(6.0)); - match self.current_tab { - Tab::Nearby => self.render_nearby_tab(ui, theme), - Tab::Send => self.render_send_tab(ui, theme), - Tab::Receive => self.render_receive_tab(ui, theme), - Tab::Shared => self.render_shared_tab(ui, theme), - Tab::Transfers => self.render_transfers_tab(ui, theme), - Tab::Settings => self.render_settings_tab(ui, theme), - } - }); - }); - }); + if ui.add(update_btn).clicked() { + trigger_install = Some(dl); + } + } else { + ui.label(RichText::new("⏳ Updating...").color(Color32::from_rgb(34, 197, 94)).strong()); + } + } + }); + }); + }); + + if dismiss { + self.show_update_banner = false; + } + if open_gitea { + let _ = std::process::Command::new("explorer").arg(&gitea_url).spawn(); + } + if let Some(dl) = trigger_install { + self.trigger_install_update(dl); + } + + ui.add_space(10.0); + } + } + } + + // Fluid Tab Transition Animation (Smooth Slide-in & Fade-in) + let elapsed = self.last_tab_change.elapsed().as_secs_f32(); + let duration = 0.20; // 200 ms + let progress = (elapsed / duration).min(1.0); + if progress < 1.0 { + ctx.request_repaint(); + } + // Cubic ease-out curve + let ease = 1.0 - (1.0 - progress).powi(3); + let slide_offset = (1.0 - ease) * 12.0; + ui.add_space(slide_offset); + + match self.current_tab { + Tab::Nearby => self.render_nearby_tab(ui, theme), + Tab::Send => self.render_send_tab(ui, theme), + Tab::Receive => self.render_receive_tab(ui, theme), + Tab::Shared => self.render_shared_tab(ui, theme), + Tab::Transfers => self.render_transfers_tab(ui, theme), + Tab::Settings => self.render_settings_tab(ui, theme), + } }); self.render_modals(ctx, theme);