diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 341b91f..9f55710 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -959,33 +959,48 @@ impl ZeroSendApp { ui.separator(); ui.add_space(8.0); - // Network Status Badge Card + // 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)), + }; + 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(theme.stroke_border) - .inner_margin(Margin::same(10.0)) + .stroke(Stroke::new(1.0_f32, net_accent_color.linear_multiply(0.5))) + .inner_margin(Margin::symmetric(10.0, 8.0)) .show(ui, |ui| { ui.horizontal(|ui| { - 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.label(RichText::new(net_icon).size(20.0)); ui.vertical(|ui| { - 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), - ); + 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)); + } + } + }); }); }); }); @@ -1232,16 +1247,29 @@ impl ZeroSendApp { rename_target = Some((peer.peer_id.clone(), peer.name.clone(), curr)); } - let badge_color = if peer.net_type.contains("ZeroTier") { - Color32::from_rgb(34, 197, 94) + 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)) } else { - Color32::from_rgb(59, 130, 246) + ("🏠 LAN", Color32::from_rgb(34, 197, 94)) }; - ui.label( - RichText::new(format!("[{}]", peer.net_type)) - .size(10.5) - .color(badge_color), - ); + + 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(), + ); + }); if !peer.shared_folders.is_empty() { Frame::none() @@ -1361,12 +1389,12 @@ impl ZeroSendApp { } fn render_send_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { - // Mode Switcher Bar + // ── Top Mode Switcher Bar ── Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(8.0)) + .rounding(Rounding::same(10.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(12.0, 8.0)) + .inner_margin(Margin::symmetric(14.0, 9.0)) .show(ui, |ui| { ui.horizontal(|ui| { ui.label(RichText::new("📤").size(20.0)); @@ -1382,6 +1410,7 @@ 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() { @@ -1395,6 +1424,7 @@ 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() { @@ -1403,8 +1433,8 @@ impl ZeroSendApp { 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.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)); }); }); }); @@ -1412,58 +1442,116 @@ impl ZeroSendApp { ui.add_space(8.0); - // Step 1: Multi-Recipient Selector Card + // ── Step 1: Multi-Recipient Modern Cards ── + let total_peers = self.peers.len(); + let selected_peers_count = self.selected_peer_ids.len(); + Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(8.0)) + .rounding(Rounding::same(10.0)) .stroke(theme.stroke_border) .inner_margin(Margin::same(12.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new("Шаг 1: Выберите получателей (друзей в сети)").strong().size(13.0).color(theme.text_primary)); + ui.label( + RichText::new(format!("Шаг 1: Выберите получателей ({}/{} выбрано)", selected_peers_count, total_peers)) + .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 { "✕ Снять выбор со всех" } 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.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(); + } } - } + }); } }); - ui.add_space(6.0); + ui.add_space(8.0); if self.peers.is_empty() { - ui.label(RichText::new("В сети пока нет друзей онлайн. Подключитесь к ZeroTier или проверьте вкладку устройств.").color(theme.text_muted).size(11.5)); + 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)); + }); } else { ui.horizontal_wrapped(|ui| { for p in &self.peers { let is_selected = self.selected_peer_ids.contains(&p.peer_id); - let (name_label, _) = self.get_peer_display_name(p); - let chip_text = format!("{} ({})", name_label, p.ip); + let (name_label, has_alias) = self.get_peer_display_name(p); - 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 { - theme.bg_card_secondary - }) - .stroke(if is_selected { - Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) - } else { - Stroke::NONE - }) - .rounding(Rounding::same(6.0)); + let os_icon = match p.os.to_lowercase().as_str() { + "windows" => "🪟", + "linux" => "🐧", + "macos" => "🍎", + _ => "💻", + }; - if ui.add(chip_btn).clicked() { + 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)) + } else { + ("🏠 LAN", Color32::from_rgb(34, 197, 94)) + }; + + let stroke = if is_selected { + Stroke::new(1.5_f32, Color32::from_rgb(56, 189, 248)) + } else { + theme.stroke_border + }; + + 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 is_selected { self.selected_peer_ids.remove(&p.peer_id); } else { @@ -1484,10 +1572,10 @@ impl ZeroSendApp { } fn render_send_files_subview(&mut self, ui: &mut Ui, theme: ThemeColors) { - // Step 2: Choose files + // ── Step 2: Choose Files & Folders ── Frame::none() .fill(theme.bg_card) - .rounding(Rounding::same(8.0)) + .rounding(Rounding::same(10.0)) .stroke(theme.stroke_border) .inner_margin(Margin::same(12.0)) .show(ui, |ui| { @@ -1495,7 +1583,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(); } }); @@ -1504,15 +1592,12 @@ impl ZeroSendApp { 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() - { + 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 let Some(files) = FileDialog::new().pick_files() { for f in files { if !self.queued_files.contains(&f) { @@ -1522,15 +1607,12 @@ impl ZeroSendApp { } } - 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() - { + 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 let Some(folder) = FileDialog::new().pick_folder() { if !self.queued_files.contains(&folder) { self.queued_files.push(folder); @@ -1538,15 +1620,12 @@ impl ZeroSendApp { } } - 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() - { + 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() { self.try_paste_clipboard_image(); } }); @@ -1562,7 +1641,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() @@ -1572,13 +1651,19 @@ impl ZeroSendApp { .color(theme.text_primary), ); if self.auto_zip_folders { - ui.label(RichText::new("• (Папки стримятся как tar.zst — без диска)").size(11.0).color(Color32::from_rgb(34, 197, 94))); + 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.add_space(6.0); - // Files Dropzone Box + // ── Files Dropzone Box ── Frame::none() .fill(theme.bg_card_secondary) .rounding(Rounding::same(8.0)) @@ -1588,11 +1673,13 @@ impl ZeroSendApp { 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(24.0); + ui.label(RichText::new("📂").size(32.0)); ui.add_space(4.0); - ui.label(RichText::new("или вставьте скриншот из буфера обмена (Ctrl+V)").size(11.0).color(theme.text_muted)); - ui.add_space(20.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); }); } else { let mut to_remove = None; @@ -1624,7 +1711,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("✕").on_hover_text("Удалить из очереди").clicked() { + if ui.small_button(RichText::new("✕").color(Color32::from_rgb(239, 68, 68))).on_hover_text("Удалить из очереди").clicked() { to_remove = Some(idx); } }); @@ -1643,9 +1730,15 @@ 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 @@ -1653,9 +1746,11 @@ 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) + 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)) } else { - format!("🚀 Отправить {} файл(ов) для {} получателей", self.queued_files.len(), num_recipients) + "🚀 Выберите получателя и добавьте файлы для отправки".to_string() }; let send_btn = Button::new(RichText::new(send_btn_text).size(14.5).strong().color(Color32::WHITE)) @@ -1664,7 +1759,8 @@ impl ZeroSendApp { } 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(), 42.0)) + .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)) .rounding(Rounding::same(8.0)); if ui.add_enabled(can_send, send_btn).clicked() { @@ -4222,8 +4318,10 @@ 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(" • 🌐 Премиальный проводник Сетевых общих папок: Удобная боковая панель с пингом и статусами друзей, интерактивные хлебные крошки (breadcrumbs), красивые карточки файлов по категориям и мульти-загрузка."); ui.label(" • 🔄 Все ключевые улучшения v2.5.4: авто-обнаружение папок в реальном времени, отслеживание скачиваний из общих папок, прозрачный стриминг папок и сохранение истории передач на диск."); ui.add_space(8.0); ui.separator();