From 9f9044646f6d6669fcb078b0760037f62c8c9f43 Mon Sep 17 00:00:00 2001 From: RarDog Date: Thu, 27 Aug 2026 13:09:37 +0300 Subject: [PATCH] UI: Complete modernization of all GUI tabs (Send, Receive, Transfers, Settings) --- src/ui/gui.rs | 1306 ++++++++++++++++++++++++++++++------------------- 1 file changed, 791 insertions(+), 515 deletions(-) diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 028e03c..f26bb7a 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -853,20 +853,20 @@ impl ZeroSendApp { ui.horizontal(|ui| { ui.add_space(4.0); - ui.label(RichText::new("⚡").size(24.0).color(theme.text_accent)); + ui.label(RichText::new("⚡").size(26.0).color(theme.text_accent)); ui.vertical(|ui| { ui.label(RichText::new("ZeroSend").size(18.0).strong().color(theme.text_primary)); - ui.label(RichText::new("ZeroTier & VPN Sharing").size(10.0).color(theme.text_muted)); + ui.label(RichText::new("P2P Mesh & LAN Sharing").size(10.5).color(theme.text_muted)); }); }); - ui.add_space(14.0); + ui.add_space(12.0); ui.separator(); - ui.add_space(10.0); + ui.add_space(8.0); - // Network Badge Card + // Network Status Badge Card Frame::none() - .fill(theme.bg_card) + .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(241, 245, 249) }) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) .inner_margin(Margin::same(10.0)) @@ -882,11 +882,12 @@ impl ZeroSendApp { ui.label( RichText::new(self.current_iface.net_type.as_str()) .strong() - .size(12.0) + .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), ); @@ -894,7 +895,7 @@ impl ZeroSendApp { }); }); - ui.add_space(16.0); + ui.add_space(14.0); let active_count = self .transfers @@ -906,12 +907,12 @@ impl ZeroSendApp { let total_shares = self.shared_folders.len() + friend_shares_count; let tabs = [ - (Tab::Nearby, "📡 Nearby Friends", self.peers.len(), false), - (Tab::Send, "📤 Send (Files/Text)", self.queued_files.len(), false), - (Tab::Receive, "📥 Receive / Web", 0, false), - (Tab::Shared, "📁 Shared Folders", total_shares, false), - (Tab::Transfers, "📜 Transfers", active_count, active_count > 0), - (Tab::Settings, "⚙ Settings & About", 0, false), + (Tab::Nearby, "📡 Устройства в сети", self.peers.len(), false), + (Tab::Send, "📤 Отправить файлы", self.queued_files.len(), false), + (Tab::Receive, "📥 Прием / Web-ссылка", 0, false), + (Tab::Shared, "📁 Общие папки", total_shares, false), + (Tab::Transfers, "📜 Передачи и файлы", active_count, active_count > 0), + (Tab::Settings, "⚙ Настройки и инфо", 0, false), ]; for (tab, title, count, is_active_pulse) in tabs { @@ -943,59 +944,98 @@ impl ZeroSendApp { Color32::TRANSPARENT }; - let btn = Button::new(RichText::new(btn_text).size(13.0).color(text_color).strong()) + let stroke = if is_selected { + Stroke::new(1.0_f32, if theme.is_dark { Color32::from_rgb(56, 189, 248) } else { Color32::from_rgb(2, 132, 199) }) + } else { + Stroke::NONE + }; + + let btn = Button::new(RichText::new(btn_text).size(12.5).color(text_color).strong()) .fill(bg_color) - .min_size(Vec2::new(ui.available_width(), 36.0)) + .stroke(stroke) + .min_size(Vec2::new(ui.available_width(), 34.0)) .rounding(Rounding::same(6.0)); if ui.add(btn).clicked() { self.current_tab = tab; } - ui.add_space(4.0); + ui.add_space(3.0); } ui.with_layout(Layout::bottom_up(Align::Center), |ui| { ui.add_space(10.0); ui.label( RichText::new(format!("v{}", env!("CARGO_PKG_VERSION"))) - .size(10.0) + .monospace() + .size(10.5) .color(theme.text_muted), ); }); } fn render_nearby_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { - ui.horizontal(|ui| { - ui.heading(RichText::new("📡 Discovered Nodes (ZeroTier / VPN)").color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button(RichText::new("🔍 Direct Connect (IP)").strong()).clicked() { - self.show_direct_ip_modal = true; - self.direct_ip_status.clear(); - } - }); - }); + // Hero Top Banner + Frame::none() + .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(238, 242, 255) }) + .rounding(Rounding::same(8.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(56, 189, 248))) + .inner_margin(Margin::symmetric(14.0, 10.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("📡").size(24.0)); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label( + RichText::new("Устройства и друзья в сети") + .strong() + .size(14.0) + .color(theme.text_primary), + ); + Frame::none() + .fill(Color32::from_rgba_premultiplied(34, 197, 94, 30)) + .rounding(Rounding::same(4.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 197, 94))) + .inner_margin(Margin::symmetric(6.0, 2.0)) + .show(ui, |ui| { + ui.label(RichText::new("● Онлайн в сети").size(10.5).color(Color32::from_rgb(34, 197, 94)).strong()); + }); + }); + ui.label( + RichText::new(format!("Ваше имя: {} • IP: {}:{}", self.custom_device_name, self.current_iface.ip, self.my_info.transfer_port)) + .size(11.0) + .color(theme.text_muted), + ); + }); - ui.label( - RichText::new("Devices in your ZeroTier network running ZeroSend appear here automatically.") - .size(12.0) - .color(theme.text_muted), - ); - ui.add_space(12.0); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let direct_btn = Button::new(RichText::new("🔍 Прямое подключение (IP)").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(37, 99, 235)) + .rounding(Rounding::same(6.0)); + + if ui.add(direct_btn).clicked() { + self.show_direct_ip_modal = true; + self.direct_ip_status.clear(); + } + }); + }); + }); + + ui.add_space(10.0); if self.peers.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(40.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { ui.label(RichText::new("🔍").size(42.0)); ui.add_space(8.0); - ui.label(RichText::new("Scanning ZeroTier network for friends...").size(16.0).strong().color(theme.text_primary)); + ui.label(RichText::new("Поиск устройств в сети ZeroTier / LAN...").size(16.0).strong().color(theme.text_primary)); ui.add_space(4.0); ui.label( - RichText::new("Ensure your friends are connected to your ZeroTier network or use Direct Connect.") + RichText::new("Убедитесь, что друзья подключены к вашей сети ZeroTier/VPN или воспользуйтесь кнопкой «Прямое подключение (IP)» выше.") .size(12.0) .color(theme.text_muted), ); @@ -1012,11 +1052,11 @@ impl ZeroSendApp { Frame::none() .fill(theme.bg_card_secondary) .rounding(Rounding::same(8.0)) - .stroke(Stroke::new(1.5_f32, theme.text_accent)) + .stroke(Stroke::new(1.5_f32, Color32::from_rgb(34, 197, 94))) .inner_margin(Margin::same(10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("🎯 Drop files directly onto any friend's card below to send instantly!").size(13.0).strong().color(theme.text_accent)); + ui.label(RichText::new("🎯 Перетащите файлы на карточку любого друга для мгновенной отправки!").size(13.0).strong().color(Color32::from_rgb(34, 197, 94))); }); }); ui.add_space(8.0); @@ -1025,6 +1065,7 @@ impl ZeroSendApp { let mut toggle_fav: Option = None; let mut rename_target: Option<(String, String, String)> = None; let mut send_to_peer: Option = None; + let mut open_shares_peer: Option = None; let mut quick_drop_target: Option<(PeerInfo, Vec)> = None; ScrollArea::vertical().show(ui, |ui| { @@ -1045,7 +1086,7 @@ impl ZeroSendApp { } else { theme.stroke_border }) - .inner_margin(Margin::same(14.0)) + .inner_margin(Margin::same(12.0)) .show(ui, |ui| { ui.horizontal(|ui| { let os_icon = match peer.os.to_lowercase().as_str() { @@ -1055,8 +1096,8 @@ impl ZeroSendApp { _ => "💻", }; - ui.label(RichText::new(os_icon).size(28.0)); - ui.add_space(8.0); + ui.label(RichText::new(os_icon).size(30.0)); + ui.add_space(6.0); ui.vertical(|ui| { ui.horizontal(|ui| { @@ -1069,11 +1110,11 @@ impl ZeroSendApp { let is_fav = self.config.favorite_peers.contains(&peer.peer_id); let star_icon = if is_fav { "⭐" } else { "☆" }; - if ui.small_button(star_icon).on_hover_text(if is_fav { "Unpin from Favorites" } else { "Pin to Favorites" }).clicked() { + if ui.small_button(star_icon).on_hover_text(if is_fav { "Убрать из избранного" } else { "Закрепить в избранном" }).clicked() { toggle_fav = Some(peer.peer_id.clone()); } - if ui.small_button("✏").on_hover_text("Rename / Set Alias").clicked() { + if ui.small_button("✏").on_hover_text("Переименовать / задать псевдоним").clicked() { let curr = self.config.peer_aliases.get(&peer.peer_id).cloned().unwrap_or_default(); rename_target = Some((peer.peer_id.clone(), peer.name.clone(), curr)); } @@ -1085,14 +1126,31 @@ impl ZeroSendApp { }; ui.label( RichText::new(format!("[{}]", peer.net_type)) - .size(11.0) + .size(10.5) .color(badge_color), ); + + if !peer.shared_folders.is_empty() { + Frame::none() + .fill(Color32::from_rgba_premultiplied(168, 85, 247, 30)) + .rounding(Rounding::same(4.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(168, 85, 247))) + .inner_margin(Margin::symmetric(5.0, 1.5)) + .show(ui, |ui| { + ui.label( + RichText::new(format!("📁 {} папок", peer.shared_folders.len())) + .size(10.0) + .color(Color32::from_rgb(168, 85, 247)) + .strong(), + ); + }); + } }); ui.horizontal(|ui| { ui.label( RichText::new(format!("{}:{} • {}", peer.ip, peer.transfer_port, peer.os)) + .monospace() .size(11.0) .color(theme.text_muted), ); @@ -1115,13 +1173,24 @@ impl ZeroSendApp { }); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - let send_btn = Button::new(RichText::new("📤 Send").strong().color(Color32::WHITE)) + let send_btn = Button::new(RichText::new("📤 Отправить").strong().color(Color32::WHITE)) .fill(Color32::from_rgb(37, 99, 235)) .rounding(Rounding::same(6.0)) - .min_size(Vec2::new(90.0, 32.0)); + .min_size(Vec2::new(95.0, 32.0)); if ui.add(send_btn).clicked() { - send_to_peer = Some(peer_id); + send_to_peer = Some(peer_id.clone()); + } + + if !peer.shared_folders.is_empty() { + let shares_btn = Button::new(RichText::new("📁 Общие папки").color(Color32::from_rgb(168, 85, 247))) + .fill(theme.bg_card_secondary) + .rounding(Rounding::same(6.0)) + .min_size(Vec2::new(105.0, 32.0)); + + if ui.add(shares_btn).clicked() { + open_shares_peer = Some(peer_id.clone()); + } } }); }); @@ -1148,7 +1217,7 @@ impl ZeroSendApp { }); if let Some((target_peer, files)) = quick_drop_target { - self.logs.push(format!("🎯 Quick Drop: Sending {} item(s) to {}...", files.len(), self.get_peer_display_name(&target_peer).0)); + self.logs.push(format!("🎯 Быстрая отправка: {} файлов для {}...", files.len(), self.get_peer_display_name(&target_peer).0)); self.start_sending_files(vec![target_peer], files); } @@ -1170,19 +1239,67 @@ impl ZeroSendApp { self.selected_peer_ids.insert(pid); self.current_tab = Tab::Send; } + + if let Some(pid) = open_shares_peer { + self.selected_peer_id_for_share = Some(pid); + self.selected_share_subtab = SharedSubTab::NetworkShares; + self.current_tab = Tab::Shared; + } } fn render_send_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { - ui.horizontal(|ui| { - ui.heading(RichText::new("📤 Send to Friends").color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.selectable_value(&mut self.send_mode, SendMode::QuickText, "📋 Quick Text / Clipboard"); - ui.selectable_value(&mut self.send_mode, SendMode::Files, "📁 Files & Folders"); + // Mode Switcher Bar + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::symmetric(12.0, 8.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("📤").size(20.0)); + ui.label(RichText::new("Режим отправки:").strong().size(13.5).color(theme.text_primary)); + + let files_active = self.send_mode == SendMode::Files; + let text_active = self.send_mode == SendMode::QuickText; + + let files_btn = Button::new( + RichText::new("📁 Файлы и папки") + .size(12.5) + .strong() + .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 }) + .rounding(Rounding::same(6.0)); + + if ui.add(files_btn).clicked() { + self.send_mode = SendMode::Files; + } + + let text_btn = Button::new( + RichText::new("💬 Быстрый текст / Ссылка") + .size(12.5) + .strong() + .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 }) + .rounding(Rounding::same(6.0)); + + if ui.add(text_btn).clicked() { + self.send_mode = SendMode::QuickText; + } + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.horizontal(|ui| { + 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)); + }); + }); + }); }); - }); + ui.add_space(8.0); - // Multi-Recipient Selector Card + // Step 1: Multi-Recipient Selector Card Frame::none() .fill(theme.bg_card) .rounding(Rounding::same(8.0)) @@ -1190,11 +1307,11 @@ impl ZeroSendApp { .inner_margin(Margin::same(12.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("Recipients:").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() { let all_selected = self.selected_peer_ids.len() == self.peers.len(); - if ui.small_button(if all_selected { "Deselect All" } else { "Select All" }).clicked() { + if ui.small_button(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех" }).clicked() { if all_selected { self.selected_peer_ids.clear(); } else { @@ -1202,19 +1319,12 @@ impl ZeroSendApp { } } } - - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new("PIN (optional):").size(11.0).color(theme.text_muted)); - ui.add(egui::TextEdit::singleline(&mut self.recipient_pin).desired_width(60.0)); - }); - }); }); ui.add_space(6.0); if self.peers.is_empty() { - ui.label(RichText::new("No online peers found yet. Discovering...").color(theme.text_muted)); + ui.label(RichText::new("В сети пока нет друзей онлайн. Подключитесь к ZeroTier или проверьте вкладку устройств.").color(theme.text_muted).size(11.5)); } else { ui.horizontal_wrapped(|ui| { for p in &self.peers { @@ -1225,6 +1335,7 @@ impl ZeroSendApp { 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 { @@ -1232,7 +1343,12 @@ impl ZeroSendApp { } else { theme.bg_card_secondary }) - .rounding(Rounding::same(14.0)); + .stroke(if is_selected { + Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) + } else { + Stroke::NONE + }) + .rounding(Rounding::same(6.0)); if ui.add(chip_btn).clicked() { if is_selected { @@ -1246,7 +1362,7 @@ impl ZeroSendApp { } }); - ui.add_space(14.0); + ui.add_space(8.0); match self.send_mode { SendMode::Files => self.render_send_files_subview(ui, theme), @@ -1255,149 +1371,166 @@ impl ZeroSendApp { } fn render_send_files_subview(&mut self, ui: &mut Ui, theme: ThemeColors) { - ui.horizontal(|ui| { - if ui - .add( - Button::new(RichText::new("📄 Add Files").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(59, 130, 246)) - .min_size(Vec2::new(120.0, 34.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) { - self.queued_files.push(f); + // Step 2: Choose files + Frame::none() + .fill(theme.bg_card) + .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("Шаг 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() { + self.queued_files.clear(); + } + }); + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + 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) { + self.queued_files.push(f); + } + } } } - } - } - if ui - .add( - Button::new(RichText::new("📁 Add Folder").strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(79, 70, 229)) - .min_size(Vec2::new(120.0, 34.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); + 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); + } + } } - } - } - if ui - .add( - Button::new(RichText::new("📸 Paste Screenshot (Ctrl+V)").color(Color32::WHITE)) - .fill(Color32::from_rgb(147, 51, 234)) - .min_size(Vec2::new(180.0, 34.0)) - .rounding(Rounding::same(6.0)), - ) - .clicked() - { - self.try_paste_clipboard_image(); - } + 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(); + } + }); - if !self.queued_files.is_empty() { - if ui.button("Clear list").clicked() { - self.queued_files.clear(); - } - } - }); + ui.add_space(10.0); + + let total_size: u64 = self + .queued_files + .iter() + .filter_map(|p| std::fs::metadata(p).ok().map(|m| m.len())) + .sum(); + + ui.horizontal(|ui| { + ui.label( + RichText::new(format!( + "Выбрано: {} элементов ({}) | Получателей: {}", + self.queued_files.len(), + human_bytes(total_size as f64), + self.selected_peer_ids.len() + )) + .size(12.5) + .strong() + .color(theme.text_primary), + ); + if self.auto_zip_folders { + ui.label(RichText::new("• (Папки сжимаются в ZIP на лету)").size(11.0).color(Color32::from_rgb(34, 197, 94))); + } + }); + + ui.add_space(6.0); + + // Files Dropzone Box + 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| { + ScrollArea::vertical().max_height(200.0).show(ui, |ui| { + if self.queued_files.is_empty() { + ui.vertical_centered(|ui| { + 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("или вставьте скриншот из буфера обмена (Ctrl+V)").size(11.0).color(theme.text_muted)); + ui.add_space(20.0); + }); + } else { + let mut to_remove = None; + for (idx, path) in self.queued_files.iter().enumerate() { + let is_dir = path.is_dir(); + let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let (icon, badge_color, category) = get_file_badge_info(&name, is_dir); + let size_str = std::fs::metadata(path).ok().map(|m| human_bytes(m.len() as f64)).unwrap_or_default(); + + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(6.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::symmetric(10.0, 6.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(icon).size(18.0)); + + // Category Badge Pill + Frame::none() + .fill(badge_color.linear_multiply(0.2)) + .rounding(Rounding::same(4.0)) + .inner_margin(Margin::symmetric(6.0, 2.0)) + .show(ui, |ui| { + ui.label(RichText::new(category).size(9.5).color(badge_color).strong()); + }); + + ui.label(RichText::new(&name).strong().color(theme.text_primary)); + 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("✕").on_hover_text("Удалить из очереди").clicked() { + to_remove = Some(idx); + } + }); + }); + }); + ui.add_space(4.0); + } + + if let Some(i) = to_remove { + self.queued_files.remove(i); + } + } + }); + }); + }); ui.add_space(10.0); - let total_size: u64 = self - .queued_files - .iter() - .filter_map(|p| std::fs::metadata(p).ok().map(|m| m.len())) - .sum(); - - ui.horizontal(|ui| { - ui.label( - RichText::new(format!( - "Selected Items: {} ({}) | Recipients: {}", - self.queued_files.len(), - human_bytes(total_size as f64), - self.selected_peer_ids.len() - )) - .size(13.0) - .strong() - .color(theme.text_primary), - ); - if self.auto_zip_folders { - ui.label(RichText::new("• (Folders auto-compressed to ZIP)").size(11.0).color(Color32::from_rgb(34, 197, 94))); - } - }); - ui.add_space(6.0); - - 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| { - 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("📂 Drag & Drop files or folders here").size(14.0).strong().color(theme.text_muted)); - ui.add_space(4.0); - ui.label(RichText::new("or paste screenshots directly with Ctrl+V").size(11.0).color(theme.text_muted)); - ui.add_space(24.0); - }); - } else { - let mut to_remove = None; - for (idx, path) in self.queued_files.iter().enumerate() { - let is_dir = path.is_dir(); - let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); - let (icon, badge_color, category) = get_file_badge_info(&name, is_dir); - let size_str = std::fs::metadata(path).ok().map(|m| human_bytes(m.len() as f64)).unwrap_or_default(); - - Frame::none() - .fill(theme.bg_card) - .rounding(Rounding::same(6.0)) - .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(10.0, 6.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(icon).size(16.0)); - - // Category Badge Pill - Frame::none() - .fill(badge_color.linear_multiply(0.2)) - .rounding(Rounding::same(4.0)) - .inner_margin(Margin::symmetric(6.0, 2.0)) - .show(ui, |ui| { - ui.label(RichText::new(category).size(9.0).color(badge_color).strong()); - }); - - ui.label(RichText::new(&name).strong().color(theme.text_primary)); - 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("✕").on_hover_text("Remove from queue").clicked() { - to_remove = Some(idx); - } - }); - }); - }); - ui.add_space(4.0); - } - - if let Some(i) = to_remove { - self.queued_files.remove(i); - } - } - }); - }); - - ui.add_space(16.0); - + // Step 3: Big Send Button let num_recipients = self.selected_peer_ids.len(); let can_send = !self.queued_files.is_empty() && num_recipients > 0; let send_btn_text = if num_recipients == 1 { @@ -1407,18 +1540,18 @@ 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!("🚀 Send {} Item(s) to {}", self.queued_files.len(), target_name) + format!("🚀 Отправить {} файл(ов) для {}", self.queued_files.len(), target_name) } else { - format!("🚀 Broadcast {} Item(s) to {} Recipients", self.queued_files.len(), num_recipients) + format!("🚀 Отправить {} файл(ов) для {} получателей", self.queued_files.len(), num_recipients) }; - let send_btn = Button::new(RichText::new(send_btn_text).size(15.0).strong().color(Color32::WHITE)) + let send_btn = Button::new(RichText::new(send_btn_text).size(14.5).strong().color(Color32::WHITE)) .fill(if can_send { Color32::from_rgb(16, 185, 129) } else { if theme.is_dark { Color32::from_rgb(51, 65, 85) } else { Color32::from_rgb(203, 213, 225) } }) - .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() { @@ -1434,56 +1567,64 @@ impl ZeroSendApp { } fn render_send_text_subview(&mut self, ui: &mut Ui, theme: ThemeColors) { - ui.horizontal(|ui| { - ui.label(RichText::new("Enter or paste text to send:").strong().color(theme.text_primary)); + Frame::none() + .fill(theme.bg_card) + .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("Шаг 2: Введите текст или ссылку для отправки:").strong().size(13.0).color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("📋 Paste from Clipboard").clicked() { - if let Ok(mut clipboard) = arboard::Clipboard::new() { - if let Ok(text) = clipboard.get_text() { - self.text_input = text; - self.analyzed_preview = analyze_text(&self.text_input); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("📋 Вставить из буфера обмена").clicked() { + if let Ok(mut clipboard) = arboard::Clipboard::new() { + if let Ok(text) = clipboard.get_text() { + self.text_input = text; + self.analyzed_preview = analyze_text(&self.text_input); + } + } } - } - } - }); - }); - - ui.add_space(6.0); - - let response = ui.add( - egui::TextEdit::multiline(&mut self.text_input) - .desired_rows(6) - .desired_width(ui.available_width()) - .font(egui::TextStyle::Monospace), - ); - - if response.changed() { - self.analyzed_preview = analyze_text(&self.text_input); - } - - ui.add_space(8.0); - - if !self.text_input.trim().is_empty() { - let (r, g, b) = self.analyzed_preview.content_type.badge_color_rgb(); - Frame::none() - .fill(theme.bg_card_secondary) - .rounding(Rounding::same(6.0)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(r, g, b))) - .inner_margin(Margin::same(8.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new("Auto-Detected:").size(11.0).color(theme.text_muted)); - ui.label( - RichText::new(self.analyzed_preview.content_type.label()) - .size(12.0) - .strong() - .color(Color32::from_rgb(r, g, b)), - ); }); }); - ui.add_space(10.0); - } + + ui.add_space(8.0); + + let response = ui.add( + egui::TextEdit::multiline(&mut self.text_input) + .desired_rows(6) + .desired_width(ui.available_width()) + .font(egui::TextStyle::Monospace), + ); + + if response.changed() { + self.analyzed_preview = analyze_text(&self.text_input); + } + + ui.add_space(8.0); + + if !self.text_input.trim().is_empty() { + let (r, g, b) = self.analyzed_preview.content_type.badge_color_rgb(); + Frame::none() + .fill(theme.bg_card_secondary) + .rounding(Rounding::same(6.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(r, g, b))) + .inner_margin(Margin::same(8.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("Тип содержимого:").size(11.0).color(theme.text_muted)); + ui.label( + RichText::new(self.analyzed_preview.content_type.label()) + .size(12.0) + .strong() + .color(Color32::from_rgb(r, g, b)), + ); + }); + }); + } + }); + + ui.add_space(10.0); let num_recipients = self.selected_peer_ids.len(); let can_send_text = !self.text_input.trim().is_empty() && num_recipients > 0; @@ -1494,9 +1635,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!("🚀 Send Text to {}", target_name) + format!("🚀 Отправить текст для {}", target_name) } else { - format!("🚀 Broadcast Text to {} Recipients", num_recipients) + format!("🚀 Отправить текст для {} получателей", num_recipients) }; let send_btn = Button::new(RichText::new(btn_label).size(14.0).strong().color(Color32::WHITE)) @@ -1539,13 +1680,13 @@ impl ZeroSendApp { match client_clone.send_text(&peer_url, &my_info_clone, &text_clone, pin_clone).await { Ok(analyzed) => { let _ = log_tx_clone.send(format!( - "🎉 Sent text [{}] to {}!", + "🎉 Текст [{}] доставлен для {}!", analyzed.content_type.label(), peer_name )); } Err(e) => { - let _ = log_tx_clone.send(format!("⚠️ Text send failed to {}: {}", peer_name, e)); + let _ = log_tx_clone.send(format!("⚠️ Ошибка отправки текста для {}: {}", peer_name, e)); } } }); @@ -1577,42 +1718,62 @@ impl ZeroSendApp { fn render_receive_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { ScrollArea::vertical().show(ui, |ui| { - ui.horizontal(|ui| { - ui.heading(RichText::new("📥 Receive & Web Access").color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("📂 Open Downloads Folder").clicked() { - let _ = std::process::Command::new("explorer").arg(&self.download_dir).spawn(); - } - }); - }); - ui.add_space(8.0); - - // General Storage & Auto-Accept Settings Card + // Hero Top Banner Frame::none() - .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) - .stroke(theme.stroke_border) - .inner_margin(Margin::same(14.0)) + .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(238, 242, 255) }) + .rounding(Rounding::same(8.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(99, 102, 241))) + .inner_margin(Margin::symmetric(14.0, 10.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("📁 Save Directory:").strong().color(theme.text_primary)); - ui.label(RichText::new(format!("{:?}", self.download_dir)).color(theme.text_secondary)); + ui.label(RichText::new("📥").size(24.0)); + ui.vertical(|ui| { + ui.label( + RichText::new("Прием файлов и Web Dropzone") + .strong() + .size(14.0) + .color(theme.text_primary), + ); + ui.label( + RichText::new("Вы можете отправлять фото, видео и файлы с телефонов и ноутбуков прямо через браузер без установки приложения!") + .size(11.5) + .color(theme.text_secondary), + ); + }); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button(RichText::new("📂 Открыть папку загрузок").strong()).clicked() { + let _ = std::process::Command::new("explorer").arg(&self.download_dir).spawn(); + } + }); + }); + }); + + ui.add_space(10.0); + + // Storage & Auto-Accept Settings Card + Frame::none() + .fill(theme.bg_card) + .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("📁 Папка для сохранения:").strong().color(theme.text_primary)); + ui.label(RichText::new(format!("{:?}", self.download_dir)).monospace().size(11.5).color(theme.text_secondary)); }); ui.add_space(6.0); ui.horizontal(|ui| { - if ui.checkbox(&mut self.auto_accept, "Auto-accept incoming transfers from friends").changed() { + if ui.checkbox(&mut self.auto_accept, "Автоматически принимать входящие файлы от друзей").changed() { self.persist_config(); } - if ui.checkbox(&mut self.auto_extract_zip, "Auto-extract received ZIP folders").changed() { + if ui.checkbox(&mut self.auto_extract_zip, "Автораспаковка ZIP архивов").changed() { self.persist_config(); } }); }); - ui.add_space(14.0); - ui.label(RichText::new("📱 Send from Phone / Browser (Web Dropzone)").strong().size(15.0).color(theme.text_primary)); - ui.label(RichText::new("Scan the QR code with your phone camera or open the link in any mobile browser to send photos, videos, and files directly to this PC:").size(12.0).color(theme.text_muted)); - ui.add_space(10.0); + ui.add_space(12.0); let port = self.my_info.transfer_port; @@ -1644,7 +1805,7 @@ impl ZeroSendApp { ui.horizontal(|ui| { ui.vertical(|ui| { ui.horizontal(|ui| { - ui.label(RichText::new("🌐 ZeroTier / VPN Mesh Link").size(15.0).strong().color(theme.text_primary)); + ui.label(RichText::new("🌐 Ссылка для ZeroTier / VPN Mesh").size(14.5).strong().color(theme.text_primary)); Frame::none() .fill(Color32::from_rgba_premultiplied(168, 85, 247, 40)) .rounding(Rounding::same(4.0)) @@ -1654,15 +1815,15 @@ impl ZeroSendApp { }); }); ui.add_space(3.0); - ui.label(RichText::new("For phones or laptops connected via ZeroTier, Tailscale, or Radmin VPN mesh:").size(11.0).color(theme.text_muted)); + ui.label(RichText::new("Для телефонов или ноутбуков, подключенных через сеть ZeroTier, Tailscale или Radmin VPN:").size(11.0).color(theme.text_muted)); ui.add_space(8.0); ui.horizontal(|ui| { - ui.label(RichText::new(&url).size(14.0).strong().color(theme.text_primary)); - if ui.button("📋 Copy").on_hover_text("Copy ZeroTier Web Link").clicked() { + ui.label(RichText::new(&url).monospace().size(13.5).strong().color(theme.text_accent)); + if ui.button("📋 Скопировать").on_hover_text("Скопировать ссылку").clicked() { ui.output_mut(|o| o.copied_text = url.clone()); } - if ui.button("🌐 Open").on_hover_text("Open in browser").clicked() { + if ui.button("🌐 Открыть").on_hover_text("Открыть в браузере").clicked() { ui.ctx().open_url(egui::OpenUrl::same_tab(&url)); } }); @@ -1699,25 +1860,25 @@ impl ZeroSendApp { ui.horizontal(|ui| { ui.vertical(|ui| { ui.horizontal(|ui| { - ui.label(RichText::new("🏠 Home Wi-Fi / Local LAN Link").size(15.0).strong().color(theme.text_primary)); + ui.label(RichText::new("🏠 Ссылка для домашней сети Wi-Fi / LAN").size(14.5).strong().color(theme.text_primary)); Frame::none() .fill(Color32::from_rgba_premultiplied(56, 189, 248, 40)) .rounding(Rounding::same(4.0)) .inner_margin(Margin::symmetric(6.0, 2.0)) .show(ui, |ui| { - ui.label(RichText::new("Local Wi-Fi").size(10.0).color(Color32::from_rgb(56, 189, 248)).strong()); + ui.label(RichText::new("Локальный Wi-Fi").size(10.0).color(Color32::from_rgb(56, 189, 248)).strong()); }); }); ui.add_space(3.0); - ui.label(RichText::new("For phones, tablets, and devices connected to the same home/office Wi-Fi router:").size(11.0).color(theme.text_muted)); + ui.label(RichText::new("Для смартфонов и планшетов, подключенных к одному домашнему Wi-Fi роутеру:").size(11.0).color(theme.text_muted)); ui.add_space(8.0); ui.horizontal(|ui| { - ui.label(RichText::new(&url).size(14.0).strong().color(theme.text_primary)); - if ui.button("📋 Copy").on_hover_text("Copy Local Wi-Fi Link").clicked() { + ui.label(RichText::new(&url).monospace().size(13.5).strong().color(theme.text_accent)); + if ui.button("📋 Скопировать").on_hover_text("Скопировать ссылку").clicked() { ui.output_mut(|o| o.copied_text = url.clone()); } - if ui.button("🌐 Open").on_hover_text("Open in browser").clicked() { + if ui.button("🌐 Открыть").on_hover_text("Открыть в браузере").clicked() { ui.ctx().open_url(egui::OpenUrl::same_tab(&url)); } }); @@ -1737,6 +1898,7 @@ impl ZeroSendApp { } }); }); + ui.add_space(10.0); } else if vpn_iface.is_none() { // Fallback if neither was separately classified let url = format!("http://{}:{}", self.current_iface.ip, port); @@ -1750,16 +1912,16 @@ impl ZeroSendApp { .show(ui, |ui| { ui.horizontal(|ui| { ui.vertical(|ui| { - ui.label(RichText::new("🌐 Web Dropzone Link").size(15.0).strong().color(theme.text_primary)); + ui.label(RichText::new("🌐 Web Dropzone Ссылка").size(14.5).strong().color(theme.text_primary)); ui.add_space(3.0); - ui.label(RichText::new("Open this link or scan QR code on mobile:").size(11.0).color(theme.text_muted)); + ui.label(RichText::new("Откройте эту ссылку в браузере или отсканируйте QR-код на телефоне:").size(11.0).color(theme.text_muted)); ui.add_space(8.0); ui.horizontal(|ui| { - ui.label(RichText::new(&url).size(14.0).strong().color(theme.text_primary)); - if ui.button("📋 Copy").clicked() { + ui.label(RichText::new(&url).monospace().size(13.5).strong().color(theme.text_accent)); + if ui.button("📋 Скопировать").clicked() { ui.output_mut(|o| o.copied_text = url.clone()); } - if ui.button("🌐 Open").clicked() { + if ui.button("🌐 Открыть").clicked() { ui.ctx().open_url(egui::OpenUrl::same_tab(&url)); } }); @@ -2967,22 +3129,107 @@ impl ZeroSendApp { } fn render_transfers_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { - ui.horizontal(|ui| { - ui.heading(RichText::new("📜 Active & Completed Transfers").color(theme.text_primary)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("📂 Open Downloads").clicked() { - let _ = std::process::Command::new("explorer").arg(&self.download_dir).spawn(); - } - if !self.transfers.is_empty() && ui.button("🗑️ Clear finished").clicked() { - self.transfers.retain(|t| t.state == TransferState::InProgress || t.state == TransferState::PendingConfirmation); - let srv = self.server.clone(); - tokio::spawn(async move { - srv.clear_finished().await; + let total_count = self.transfers.len(); + let active_count = self.transfers.iter().filter(|t| t.state == TransferState::InProgress || t.state == TransferState::PendingConfirmation).count(); + let completed_count = self.transfers.iter().filter(|t| t.state == TransferState::Completed).count(); + let total_speed_bps: u64 = self.transfers.iter().filter(|t| t.state == TransferState::InProgress).map(|t| t.speed_bps).sum(); + let total_speed_mb = (total_speed_bps as f64) / 1_048_576.0; + + // Top Metrics Summary Dashboard + Frame::none() + .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(238, 242, 255) }) + .rounding(Rounding::same(8.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(56, 189, 248))) + .inner_margin(Margin::symmetric(14.0, 10.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("📜").size(24.0)); + ui.vertical(|ui| { + ui.label( + RichText::new("История и мониторинг передач") + .strong() + .size(14.0) + .color(theme.text_primary), + ); + ui.label( + RichText::new("Отслеживание скорости в реальном времени, криптографическая проверка SHA-256 и управление файлами.") + .size(11.5) + .color(theme.text_secondary), + ); }); - } + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if !self.transfers.is_empty() { + let clear_btn = Button::new(RichText::new("🗑 Очистить завершенные").color(Color32::from_rgb(239, 68, 68))) + .fill(theme.bg_card_secondary) + .rounding(Rounding::same(6.0)); + + if ui.add(clear_btn).clicked() { + self.transfers.retain(|t| t.state == TransferState::InProgress || t.state == TransferState::PendingConfirmation); + let srv = self.server.clone(); + tokio::spawn(async move { + srv.clear_finished().await; + }); + } + } + + let open_btn = Button::new(RichText::new("📂 Папка загрузок").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(37, 99, 235)) + .rounding(Rounding::same(6.0)); + + if ui.add(open_btn).clicked() { + let _ = std::process::Command::new("explorer").arg(&self.download_dir).spawn(); + } + }); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(6.0); + + // 4 Metric Pill Cards + ui.horizontal(|ui| { + let card_w = (ui.available_width() - 30.0) / 4.0; + + // Metric 1: Total + Frame::none().fill(theme.bg_card).rounding(Rounding::same(6.0)).stroke(theme.stroke_border).inner_margin(Margin::symmetric(10.0, 6.0)).show(ui, |ui| { + ui.set_width(card_w); + ui.vertical(|ui| { + ui.label(RichText::new("Всего передач").size(10.5).color(theme.text_muted)); + ui.label(RichText::new(format!("{}", total_count)).strong().size(15.0).color(theme.text_primary)); + }); + }); + + // Metric 2: Active + Frame::none().fill(theme.bg_card).rounding(Rounding::same(6.0)).stroke(theme.stroke_border).inner_margin(Margin::symmetric(10.0, 6.0)).show(ui, |ui| { + ui.set_width(card_w); + ui.vertical(|ui| { + ui.label(RichText::new("В процессе").size(10.5).color(theme.text_muted)); + ui.label(RichText::new(format!("{}", active_count)).strong().size(15.0).color(Color32::from_rgb(56, 189, 248))); + }); + }); + + // Metric 3: Completed + Frame::none().fill(theme.bg_card).rounding(Rounding::same(6.0)).stroke(theme.stroke_border).inner_margin(Margin::symmetric(10.0, 6.0)).show(ui, |ui| { + ui.set_width(card_w); + ui.vertical(|ui| { + ui.label(RichText::new("Завершено").size(10.5).color(theme.text_muted)); + ui.label(RichText::new(format!("{}", completed_count)).strong().size(15.0).color(Color32::from_rgb(34, 197, 94))); + }); + }); + + // Metric 4: Speed + Frame::none().fill(theme.bg_card).rounding(Rounding::same(6.0)).stroke(theme.stroke_border).inner_margin(Margin::symmetric(10.0, 6.0)).show(ui, |ui| { + ui.set_width(card_w); + ui.vertical(|ui| { + ui.label(RichText::new("Общая скорость").size(10.5).color(theme.text_muted)); + ui.label(RichText::new(format!("{:.1} MB/s", total_speed_mb)).strong().size(15.0).color(if total_speed_mb > 0.0 { Color32::from_rgb(250, 204, 21) } else { theme.text_secondary })); + }); + }); + }); }); - }); - ui.add_space(8.0); + + ui.add_space(10.0); if self.transfers.is_empty() { Frame::none() @@ -2993,9 +3240,10 @@ impl ZeroSendApp { .show(ui, |ui| { ui.vertical_centered(|ui| { ui.label(RichText::new("📦").size(36.0)); - ui.label(RichText::new("No active or past transfers.").size(15.0).color(theme.text_secondary)); + ui.add_space(6.0); + ui.label(RichText::new("История передач пока пуста.").size(15.0).strong().color(theme.text_secondary)); ui.add_space(4.0); - ui.label(RichText::new("Transfers will appear here with live speed and progress.").size(12.0).color(theme.text_muted)); + ui.label(RichText::new("Отправьте файлы друзьям или примите передачу — здесь появится прогресс, скорость и SHA-256 отчет.").size(12.0).color(theme.text_muted)); }); }); return; @@ -3025,14 +3273,14 @@ impl ZeroSendApp { } else { theme.stroke_border }) - .inner_margin(Margin::same(14.0)) + .inner_margin(Margin::same(12.0)) .show(ui, |ui| { // Top row: Direction, Peer, and Status Badge ui.horizontal(|ui| { let (dir_badge, dir_color) = if t.is_incoming { - (format!("📥 Received from {}", t.peer_name), Color32::from_rgb(192, 132, 252)) + (format!("📥 Получено от {}", t.peer_name), Color32::from_rgb(192, 132, 252)) } else { - (format!("📤 Sent to {}", t.peer_name), Color32::from_rgb(96, 165, 250)) + (format!("📤 Отправлено для {}", t.peer_name), Color32::from_rgb(96, 165, 250)) }; ui.label(RichText::new(dir_badge).size(13.0).strong().color(dir_color)); @@ -3041,7 +3289,7 @@ impl ZeroSendApp { match &t.state { TransferState::Completed => { ui.horizontal(|ui| { - ui.label(RichText::new("✅ Completed").color(Color32::from_rgb(34, 197, 94)).strong().size(13.0)); + ui.label(RichText::new("✅ Завершено").color(Color32::from_rgb(34, 197, 94)).strong().size(13.0)); if t.checksum_verified { Frame::none() .fill(Color32::from_rgba_premultiplied(34, 197, 94, 35)) @@ -3049,8 +3297,8 @@ impl ZeroSendApp { .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 197, 94))) .inner_margin(Margin::symmetric(6.0, 2.0)) .show(ui, |ui| { - ui.label(RichText::new("🛡️ SHA-256 Verified").size(10.5).color(Color32::from_rgb(74, 222, 128)).strong()) - .on_hover_text("Cryptographic SHA-256 checksum verified on the fly. 100% integrity guaranteed."); + ui.label(RichText::new("🛡️ SHA-256 Проверено").size(10.5).color(Color32::from_rgb(74, 222, 128)).strong()) + .on_hover_text("Криптографическая контрольная сумма SHA-256 проверена на лету. 100% гарантия целостности."); }); } }); @@ -3065,22 +3313,22 @@ impl ZeroSendApp { ); } TransferState::PendingConfirmation => { - ui.label(RichText::new("⏳ Waiting for confirmation...").color(Color32::YELLOW).size(13.0)); + ui.label(RichText::new("⏳ Ожидание подтверждения...").color(Color32::YELLOW).size(13.0)); } TransferState::Failed(e) => { - ui.label(RichText::new(format!("❌ Failed: {}", e)).color(Color32::RED).size(12.0)); + ui.label(RichText::new(format!("❌ Ошибка: {}", e)).color(Color32::RED).size(12.0)); } TransferState::Rejected => { - ui.label(RichText::new("⛔ Transfer rejected").color(Color32::RED).size(12.0)); + ui.label(RichText::new("⛔ Передача отклонена").color(Color32::RED).size(12.0)); } TransferState::Canceled => { - ui.label(RichText::new("🛑 Canceled").color(theme.text_muted).size(12.0)); + ui.label(RichText::new("🛑 Отменено").color(theme.text_muted).size(12.0)); } } }); }); - ui.add_space(8.0); + ui.add_space(6.0); // Middle row: File Icon, Detailed Filename, Size, and Buttons ui.horizontal(|ui| { @@ -3090,7 +3338,7 @@ impl ZeroSendApp { ui.vertical(|ui| { ui.label( RichText::new(&t.current_file_name) - .size(15.0) + .size(14.5) .strong() .color(theme.text_primary), ); @@ -3106,11 +3354,11 @@ impl ZeroSendApp { let remaining_bytes = t.total_bytes.saturating_sub(t.bytes_transferred); let eta_secs = remaining_bytes / speed; let eta_str = if eta_secs < 60 { - format!("~{}s left", eta_secs) + format!("~{}с осталось", eta_secs) } else if eta_secs < 3600 { - format!("~{}m {}s left", eta_secs / 60, eta_secs % 60) + format!("~{}м {}с осталось", eta_secs / 60, eta_secs % 60) } else { - format!("~{}h {}m left", eta_secs / 3600, (eta_secs % 3600) / 60) + format!("~{}ч {}м осталось", eta_secs / 3600, (eta_secs % 3600) / 60) }; let speed_mb = (t.speed_bps as f64) / 1_048_576.0; @@ -3124,12 +3372,12 @@ impl ZeroSendApp { ) } else if is_done { if t.checksum_verified { - format!("Delivered • Total Size: {} • 🛡️ SHA-256 Verified", human_bytes(t.total_bytes as f64)) + format!("Доставлено • Размер: {} • 🛡️ SHA-256 Проверено", human_bytes(t.total_bytes as f64)) } else { - format!("Delivered • Total Size: {}", human_bytes(t.total_bytes as f64)) + format!("Доставлено • Размер: {}", human_bytes(t.total_bytes as f64)) } } else { - format!("Total Size: {}", human_bytes(t.total_bytes as f64)) + format!("Размер: {}", human_bytes(t.total_bytes as f64)) }; ui.label(RichText::new(size_details).size(11.0).color(theme.text_muted)); @@ -3141,7 +3389,7 @@ impl ZeroSendApp { let exists = target_file.exists(); if ui .add( - Button::new(RichText::new("📂 Show in Folder").strong().color(Color32::WHITE)) + Button::new(RichText::new("📂 В проводнике").strong().color(Color32::WHITE)) .fill(Color32::from_rgb(37, 99, 235)) .rounding(Rounding::same(6.0)), ) @@ -3158,7 +3406,7 @@ impl ZeroSendApp { if exists && ui .add( - Button::new(RichText::new("📄 Open").color(if theme.is_dark { Color32::WHITE } else { theme.text_primary })) + Button::new(RichText::new("📄 Открыть").color(if theme.is_dark { Color32::WHITE } else { theme.text_primary })) .fill(if theme.is_dark { Color32::from_rgb(51, 65, 85) } else { Color32::from_rgb(226, 232, 240) }) .rounding(Rounding::same(6.0)), ) @@ -3170,7 +3418,7 @@ impl ZeroSendApp { } }); - ui.add_space(8.0); + ui.add_space(6.0); // Progress Bar let progress_ratio = if t.total_bytes > 0 { @@ -3188,37 +3436,61 @@ impl ZeroSendApp { }); ui.add(pb); }); - ui.add_space(8.0); + ui.add_space(6.0); } }); } fn render_settings_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { ScrollArea::vertical().show(ui, |ui| { - ui.heading(RichText::new("⚙️ Settings & Preferences").color(theme.text_primary)); - ui.add_space(8.0); + // Header Banner + Frame::none() + .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(238, 242, 255) }) + .rounding(Rounding::same(8.0)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(99, 102, 241))) + .inner_margin(Margin::symmetric(14.0, 10.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("⚙️").size(24.0)); + ui.vertical(|ui| { + ui.label( + RichText::new("Настройки приложения и информация") + .strong() + .size(14.0) + .color(theme.text_primary), + ); + ui.label( + RichText::new("Конфигурация сети, папок загрузки, шифрования, темы оформления и обновлений.") + .size(11.5) + .color(theme.text_secondary), + ); + }); + }); + }); + ui.add_space(10.0); + + // Card 1: Theme & Appearance Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(10.0)) + .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(16.0)) + .inner_margin(Margin::same(14.0)) .show(ui, |ui| { - // Theme Mode Selector - ui.label(RichText::new("🎨 Theme & Appearance").strong().color(theme.text_primary)); - ui.add_space(4.0); + ui.label(RichText::new("🎨 Внешний вид и тема").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); ui.horizontal(|ui| { let themes = [ - (ThemeMode::Auto, "🖥 Auto (System Theme)"), - (ThemeMode::Dark, "🌙 Dark Theme"), - (ThemeMode::Light, "☀️ Light Theme"), + (ThemeMode::Auto, "🖥 Системная (Авто)"), + (ThemeMode::Dark, "🌙 Темная тема"), + (ThemeMode::Light, "☀️ Светлая тема"), ]; for (mode, label) in themes { let is_active = self.theme_mode == mode; let btn = Button::new( RichText::new(label) - .size(12.5) + .size(12.0) .color(if is_active { Color32::WHITE } else { theme.text_secondary }) .strong(), ) @@ -3238,93 +3510,117 @@ impl ZeroSendApp { let current_status = match self.theme_mode { ThemeMode::Auto => { if is_system_dark_mode() { - "Currently automatically matching Windows Dark Theme" + "Автоматически повторяет темную тему Windows" } else { - "Currently automatically matching Windows Light Theme" + "Автоматически повторяет светлую тему Windows" } } - ThemeMode::Dark => "Always Dark Theme", - ThemeMode::Light => "Always Light Theme", + ThemeMode::Dark => "Всегда используется темная тема", + ThemeMode::Light => "Всегда используется светлая тема", }; ui.label(RichText::new(current_status).size(11.0).color(theme.text_muted)); + }); - ui.add_space(12.0); - ui.separator(); + ui.add_space(10.0); + + // Card 2: Network & Identity + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("🌐 Сеть и идентификация").strong().size(13.5).color(theme.text_accent)); ui.add_space(8.0); - ui.label(RichText::new("Device Name:").strong().color(theme.text_primary)); - if ui.text_edit_singleline(&mut self.custom_device_name).lost_focus() { - self.persist_config(); - } - - ui.add_space(10.0); - ui.label(RichText::new("Network Interface:").strong().color(theme.text_primary)); - - let mut iface_changed = false; - let ifaces = self.available_interfaces.clone(); - egui::ComboBox::from_id_salt("iface_combo") - .selected_text(format!("{} - {}", self.current_iface.net_type.as_str(), self.current_iface.ip)) - .width(360.0) - .show_ui(ui, |ui| { - for iface in &ifaces { - let label = format!("{} (IP: {})", iface.name, iface.ip); - if ui.selectable_value(&mut self.current_iface, iface.clone(), label).changed() { - iface_changed = true; - } - } - }); - - if iface_changed { - self.persist_config(); - } - - ui.add_space(10.0); - ui.label(RichText::new("Download Location:").strong().color(theme.text_primary)); ui.horizontal(|ui| { - ui.label(RichText::new(format!("{:?}", self.download_dir)).color(theme.text_secondary)); - if ui.button("Change Folder...").clicked() { + ui.label("Имя устройства в сети:"); + if ui.text_edit_singleline(&mut self.custom_device_name).lost_focus() { + self.persist_config(); + } + }); + + ui.add_space(8.0); + ui.horizontal(|ui| { + ui.label("Сетевой адаптер (IP):"); + + let mut iface_changed = false; + let ifaces = self.available_interfaces.clone(); + egui::ComboBox::from_id_salt("iface_combo") + .selected_text(format!("{} - {}", self.current_iface.net_type.as_str(), self.current_iface.ip)) + .width(360.0) + .show_ui(ui, |ui| { + for iface in &ifaces { + let label = format!("{} (IP: {})", iface.name, iface.ip); + if ui.selectable_value(&mut self.current_iface, iface.clone(), label).changed() { + iface_changed = true; + } + } + }); + + if iface_changed { + self.persist_config(); + } + }); + }); + + ui.add_space(10.0); + + // Card 3: Storage & Downloads + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("💾 Папка сохранения файлов").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); + + ui.horizontal(|ui| { + ui.label(RichText::new(format!("{:?}", self.download_dir)).monospace().size(11.5).color(theme.text_secondary)); + + let change_btn = Button::new(RichText::new("📁 Изменить папку...").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(59, 130, 246)) + .rounding(Rounding::same(6.0)); + + if ui.add(change_btn).clicked() { if let Some(dir) = FileDialog::new().pick_folder() { self.download_dir = dir; self.persist_config(); } } + + if ui.button("📂 Открыть папку").clicked() { + let _ = std::process::Command::new("explorer").arg(&self.download_dir).spawn(); + } }); + }); - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); + ui.add_space(10.0); - // Folder Compression Settings - ui.label(RichText::new("📦 Ultra-Fast Folder ZIP Compression").strong().color(theme.text_primary)); - ui.add_space(4.0); - if ui - .checkbox( - &mut self.auto_zip_folders, - "Compress folders into ZIP on the fly (10x faster for code, git, photo folders)", - ) - .changed() - { + // Card 4: Compression & Speed Cap + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("⚡ Производительность, сжатие и лимиты скорости").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); + + if ui.checkbox(&mut self.auto_zip_folders, "Сжимать папки в ZIP на лету (в 10 раз быстрее для папок с кодом и фото)").changed() { self.persist_config(); } - if ui - .checkbox( - &mut self.auto_extract_zip, - "Automatically extract received ZIP folder archives into Downloads folder", - ) - .changed() - { + if ui.checkbox(&mut self.auto_extract_zip, "Автоматически распаковывать полученные ZIP архивы").changed() { self.persist_config(); } - ui.add_space(12.0); + ui.add_space(8.0); ui.separator(); ui.add_space(8.0); - // Bandwidth Limiter Settings - ui.label(RichText::new("🚀 Bandwidth Limiter (Upload Speed Cap)").strong().color(theme.text_primary)); - ui.add_space(4.0); ui.horizontal(|ui| { - ui.label(RichText::new("Speed limit:").color(theme.text_secondary)); + ui.label("Ограничение скорости отдачи:"); if ui .add( egui::DragValue::new(&mut self.max_upload_speed_mbps) @@ -3332,7 +3628,7 @@ impl ZeroSendApp { .speed(1.0) .custom_formatter(|n, _| { if n == 0.0 { - "Unlimited (0 MB/s)".to_string() + "Без ограничений (0 MB/s)".to_string() } else { format!("{:.0} MB/s", n) } @@ -3344,23 +3640,15 @@ impl ZeroSendApp { } ui.add_space(8.0); - ui.label( - RichText::new("(Enter 0 for unlimited, or type any custom MB/s)") - .size(11.0) - .color(theme.text_muted), - ); + ui.label(RichText::new("(0 = максимальная скорость канала)").size(11.0).color(theme.text_muted)); }); ui.add_space(4.0); ui.horizontal(|ui| { - ui.label( - RichText::new("Quick presets:") - .size(12.0) - .color(theme.text_muted), - ); + ui.label(RichText::new("Быстрые пресеты:").size(11.5).color(theme.text_muted)); for preset in [0, 5, 10, 25, 50, 100] { let label = if preset == 0 { - "Unlimited".to_string() + "Без лимита".to_string() } else { format!("{} MB/s", preset) }; @@ -3383,86 +3671,28 @@ impl ZeroSendApp { } } }); + }); - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); + ui.add_space(10.0); - // Audio & Sound Notification Settings - ui.label(RichText::new("🔔 Audio & Notifications").strong().color(theme.text_primary)); - ui.add_space(4.0); - if ui - .checkbox( - &mut self.sound_notifications, - "Play soft audio chime upon transfer completion", - ) - .changed() - { - self.persist_config(); - } + // Card 5: Security & PIN + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("🔒 Безопасность и PIN-код").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); - - // System Tray Options - ui.label(RichText::new("🎛 Window & System Tray Behavior").strong().color(theme.text_primary)); - ui.add_space(4.0); - if ui - .checkbox( - &mut self.close_to_tray, - "Close to System Tray (keeps ZeroSend running in background)", - ) - .changed() - { - self.persist_config(); - } - - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); - - // Windows Startup Autostart - ui.label(RichText::new("🚀 Windows Startup Behavior").strong().color(theme.text_primary)); - ui.add_space(4.0); - ui.horizontal(|ui| { - if self.autostart_enabled { - ui.label(RichText::new("Status: Starts with Windows (Minimized to Tray)").color(Color32::from_rgb(34, 197, 94))); - if ui.button("➖ Disable Autostart").clicked() { - let _ = unregister_autostart(); - self.autostart_enabled = is_autostart_registered(); - } - } else { - ui.label(RichText::new("Status: Disabled").color(theme.text_muted)); - if ui.button("➕ Start with Windows (Tray)").clicked() { - let _ = register_autostart(); - self.autostart_enabled = is_autostart_registered(); - } - } - }); - - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); - - - - // PIN Security Settings - ui.label(RichText::new("🔒 Security / Anti-Spam PIN Protection").strong().color(theme.text_primary)); - ui.add_space(4.0); - if ui - .checkbox( - &mut self.require_pin, - "Require PIN code for incoming files and text messages", - ) - .changed() - { + if ui.checkbox(&mut self.require_pin, "Требовать 4-значный PIN-код для входящих передач и сообщений").changed() { self.persist_config(); } if self.require_pin { + ui.add_space(4.0); ui.horizontal(|ui| { - ui.label(RichText::new("4-Digit PIN Code:").color(theme.text_secondary)); + ui.label("Установите PIN-код:"); if ui .add( egui::TextEdit::singleline(&mut self.pin_code) @@ -3475,30 +3705,66 @@ impl ZeroSendApp { } }); } + }); + + ui.add_space(10.0); + + // Card 6: System & Tray Behavior + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("🎛 Системное поведение и уведомления").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); + + if ui.checkbox(&mut self.close_to_tray, "Сворачивать в системный трей при закрытии крестиком").changed() { + self.persist_config(); + } + if ui.checkbox(&mut self.sound_notifications, "Воспроизводить звуковой сигнал при завершении передачи").changed() { + self.persist_config(); + } - ui.add_space(12.0); - ui.separator(); ui.add_space(8.0); + ui.horizontal(|ui| { + if self.autostart_enabled { + ui.label(RichText::new("Автозапуск с Windows: Включен (в трее)").color(Color32::from_rgb(34, 197, 94))); + if ui.button("➖ Отключить автозапуск").clicked() { + let _ = unregister_autostart(); + self.autostart_enabled = is_autostart_registered(); + } + } else { + ui.label(RichText::new("Автозапуск с Windows: Отключен").color(theme.text_muted)); + if ui.button("➕ Запускать вместе с Windows").clicked() { + let _ = register_autostart(); + self.autostart_enabled = is_autostart_registered(); + } + } + }); + }); - // Gitea Updates & Sync Settings - ui.label(RichText::new("🔄 Gitea Updates & Repository").strong().color(theme.text_primary)); - ui.add_space(4.0); + ui.add_space(10.0); + + // Card 7: Gitea Auto-Updater + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(8.0)) + .stroke(theme.stroke_border) + .inner_margin(Margin::same(14.0)) + .show(ui, |ui| { + ui.label(RichText::new("🔄 Обновления через Gitea").strong().size(13.5).color(theme.text_accent)); + ui.add_space(6.0); ui.horizontal(|ui| { - ui.label(RichText::new("Gitea Repo:").color(theme.text_secondary)); + ui.label("Репозиторий Gitea:"); if ui.link(RichText::new(GITEA_DEFAULT_REPO_URL).color(Color32::from_rgb(56, 189, 248))).clicked() { let _ = std::process::Command::new("explorer").arg(GITEA_DEFAULT_REPO_URL).spawn(); } }); ui.add_space(4.0); - if ui - .checkbox( - &mut self.check_updates_on_startup, - "Automatically check for updates on application startup", - ) - .changed() - { + if ui.checkbox(&mut self.check_updates_on_startup, "Автоматически проверять обновления при запуске").changed() { self.config.check_updates_on_startup = self.check_updates_on_startup; self.persist_config(); } @@ -3507,7 +3773,7 @@ impl ZeroSendApp { let mut check_clicked = false; ui.horizontal(|ui| { let check_btn = Button::new( - RichText::new(if self.is_checking_update { "⏳ Checking Gitea..." } else { "🔍 Check for Updates Now" }) + RichText::new(if self.is_checking_update { "⏳ Проверка Gitea..." } else { "🔍 Проверить обновления сейчас" }) .color(theme.text_primary) ) .fill(theme.bg_card_secondary) @@ -3526,7 +3792,6 @@ impl ZeroSendApp { self.trigger_update_check(); } - // Show active update info / download card let mut trigger_install_url = None; let mut open_gitea_url = None; @@ -3540,9 +3805,9 @@ impl ZeroSendApp { .inner_margin(Margin::same(12.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("✨ New Version Available:").strong().color(Color32::from_rgb(34, 197, 94))); + ui.label(RichText::new("✨ Доступна новая версия:").strong().color(Color32::from_rgb(34, 197, 94))); ui.label(RichText::new(format!("v{}", info.latest_version)).strong().size(14.0).color(theme.text_primary)); - ui.label(RichText::new(format!("(Current: v{})", info.current_version)).size(11.0).color(theme.text_muted)); + ui.label(RichText::new(format!("(Текущая: v{})", info.current_version)).size(11.0).color(theme.text_muted)); }); if !info.release_notes.is_empty() { @@ -3557,23 +3822,23 @@ impl ZeroSendApp { UpdateProgress::Downloading { percent, downloaded, total } => { let pct_100 = (percent * 100.0) as u32; ui.label( - RichText::new(format!("Downloading update: {} / {} ({}%)", human_bytes(*downloaded as f64), human_bytes(*total as f64), pct_100)) + RichText::new(format!("Загрузка обновления: {} / {} ({}%)", human_bytes(*downloaded as f64), human_bytes(*total as f64), pct_100)) .size(11.5) .color(theme.text_primary) ); ui.add(ProgressBar::new(*percent).animate(true)); } UpdateProgress::Verifying => { - ui.label(RichText::new("🛡️ Verifying downloaded binary...").color(Color32::from_rgb(56, 189, 248))); + ui.label(RichText::new("🛡️ Проверка целостности файла...").color(Color32::from_rgb(56, 189, 248))); } UpdateProgress::Installing => { - ui.label(RichText::new("⚡ Installing update and restarting ZeroSend...").color(Color32::from_rgb(34, 197, 94)).strong()); + ui.label(RichText::new("⚡ Установка обновления и перезапуск ZeroSend...").color(Color32::from_rgb(34, 197, 94)).strong()); } UpdateProgress::Completed => { - ui.label(RichText::new("🎉 Update completed! Relaunching...").color(Color32::from_rgb(34, 197, 94)).strong()); + ui.label(RichText::new("🎉 Обновление успешно установлено! Перезапуск...").color(Color32::from_rgb(34, 197, 94)).strong()); } UpdateProgress::Error(err) => { - ui.label(RichText::new(format!("❌ Update failed: {}", err)).color(Color32::from_rgb(239, 68, 68))); + ui.label(RichText::new(format!("❌ Ошибка обновления: {}", err)).color(Color32::from_rgb(239, 68, 68))); } } } @@ -3581,7 +3846,7 @@ impl ZeroSendApp { ui.horizontal(|ui| { if let Some(dl_url) = &info.download_url { let install_btn = Button::new( - RichText::new("🚀 Download & Install Update Now") + RichText::new("🚀 Скачать и установить обновление") .strong() .color(Color32::WHITE) ) @@ -3594,7 +3859,7 @@ impl ZeroSendApp { } let gitea_btn = Button::new( - RichText::new("🌐 View on Gitea") + RichText::new("🌐 Страница релиза на Gitea") .color(theme.text_primary) ) .fill(theme.bg_card_secondary) @@ -3617,7 +3882,7 @@ impl ZeroSendApp { } }); - ui.add_space(14.0); + ui.add_space(10.0); // Collapsible Credits & About Card egui::CollapsingHeader::new(RichText::new("👥 About & Credits").strong().size(14.0).color(theme.text_primary)) @@ -3669,10 +3934,21 @@ impl ZeroSendApp { .stroke(theme.stroke_border) .inner_margin(Margin::same(14.0)) .show(ui, |ui| { + // v2.5.1 + ui.horizontal(|ui| { + ui.label(RichText::new("v2.5.1").strong().color(Color32::from_rgb(34, 197, 94))); + ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY)); + }); + ui.label(" • 🎨 Полная модернизация интерфейса: Единый стиль карточек, дашборд статистики передач, пошаговые мастера отправки и автосинхронизации, баннеры подсказок."); + ui.label(" • 🔄 Улучшенный мастер Auto-Sync: Наглядный 3-шаговый визард выбора папок и онлайн-друзей с интуитивным управлением синхронизацией."); + ui.label(" • 🚀 Gitea Self-Updater: Интеграция автопроверки и бесшовного обновления приложения прямо из встроенного репозитория Gitea."); + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + // v2.5.0 ui.horizontal(|ui| { - ui.label(RichText::new("v2.5.0").strong().color(Color32::from_rgb(34, 197, 94))); - ui.label(RichText::new("(Current Release)").size(11.0).color(Color32::GRAY)); + ui.label(RichText::new("v2.5.0").strong().color(Color32::from_rgb(56, 189, 248))); }); ui.label(" • 🔄 Live Folder Two-Way Auto-Sync: Real-time background sync engine that keeps local directories and remote shared folders mirrored without user interaction."); ui.label(" • 🖧 Windows Explorer Network Drive (WebDAV): 1-click mounting of all shared folders directly into Windows Explorer as drive Z:\\.");