diff --git a/src/ui/gui.rs b/src/ui/gui.rs index dabd53a..8d5e6f5 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -1366,11 +1366,11 @@ impl ZeroSendApp { .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::symmetric(12.0, 8.0)) + .inner_margin(Margin::symmetric(14.0, 10.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)); + 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; @@ -1403,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.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.0).color(theme.text_muted)); + ui.add(egui::TextEdit::singleline(&mut self.recipient_pin).hint_text("PIN").desired_width(65.0)); }); }); }); @@ -1424,46 +1424,85 @@ impl ZeroSendApp { 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| { + if ui.button(RichText::new(if all_selected { "✕ Снять выбор" } else { "✓ Выбрать всех" }).color(theme.text_accent).size(11.5)).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(6.0)) + .inner_margin(Margin::same(12.0)) + .show(ui, |ui| { + ui.label(RichText::new("ℹ️ В сети пока нет обнаруженных друзей. Подключитесь к ZeroTier/LAN для автоматического поиска.").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, _) = self.get_peer_display_name(p); - let chip_text = format!("{} ({})", name_label, p.ip); + let os_icon = match p.os.to_lowercase().as_str() { + s if s.contains("win") => "🪟", + s if s.contains("linux") => "🐧", + s if s.contains("mac") || s.contains("darwin") => "🍎", + s if s.contains("android") || s.contains("ios") => "📱", + _ => "💻", + }; - 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 (net_badge, net_color) = match p.net_type.to_lowercase().as_str() { + s if s.contains("zerotier") => ("🪐 ZeroTier", Color32::from_rgb(251, 146, 60)), + s if s.contains("tailscale") => ("🪢 Tailscale", Color32::from_rgb(56, 189, 248)), + s if s.contains("lan") || s.contains("wi-fi") || s.contains("eth") => ("🏠 LAN", Color32::from_rgb(34, 197, 94)), + s if s.contains("radmin") => ("🛡️ Radmin", Color32::from_rgb(168, 85, 247)), + _ => ("🌐 Direct", Color32::from_rgb(100, 116, 139)), + }; - if ui.add(chip_btn).clicked() { + let card = Frame::none() + .fill(if is_selected { theme.bg_card_active } else { theme.bg_card_secondary }) + .rounding(Rounding::same(8.0)) + .stroke(if is_selected { + Stroke::new(1.5_f32, Color32::from_rgb(96, 165, 250)) + } else { + theme.stroke_border + }) + .inner_margin(Margin::symmetric(10.0, 8.0)); + + let res = card.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.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(&name_label).strong().size(12.0).color(if is_selected { Color32::WHITE } else { theme.text_primary })); + if let Some(ping) = p.ping_ms { + ui.label(RichText::new(format!("⚡ {}ms", ping)).size(10.0).color(Color32::from_rgb(34, 197, 94))); + } + }); + ui.horizontal(|ui| { + ui.label(RichText::new(p.ip.to_string()).size(10.0).monospace().color(theme.text_muted)); + Frame::none() + .fill(theme.bg_card) + .rounding(Rounding::same(3.0)) + .inner_margin(Margin::symmetric(4.0, 1.0)) + .show(ui, |ui| { + ui.label(RichText::new(net_badge).size(8.5).color(net_color).strong()); + }); + }); + }); + }); + }); + + if res.response.interact(egui::Sense::click()).clicked() { if is_selected { self.selected_peer_ids.remove(&p.peer_id); } else { @@ -1664,7 +1703,7 @@ 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)) + .min_size(Vec2::new(ui.available_width(), 44.0)) .rounding(Rounding::same(8.0)); if ui.add_enabled(can_send, send_btn).clicked() { @@ -2738,7 +2777,7 @@ impl ZeroSendApp { ui.vertical_centered(|ui| { ui.label(RichText::new("🌐").size(40.0)); ui.label( - RichText::new("No shared folders discovered on your network.") + RichText::new("В сети пока нет обнаруженных общих папок.") .size(16.0) .strong() .color(theme.text_primary), @@ -2746,7 +2785,7 @@ impl ZeroSendApp { ui.add_space(6.0); ui.label( RichText::new( - "When your friends on ZeroTier / LAN add shared folders, they will automatically appear here with streaming and fast download support.", + "Когда друзья в ZeroTier/LAN откроют доступ к своим папкам, они мгновенно появятся здесь со стримингом и скачиванием.", ) .size(12.0) .color(theme.text_muted), @@ -2756,21 +2795,29 @@ impl ZeroSendApp { 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: Peer & Folder Selector (250px) - ui.allocate_ui(Vec2::new(250.0, ui.available_height()), |ui| { + // ── Left Column: Modern Friends & Shares Sidebar (260px) ── + ui.allocate_ui(Vec2::new(260.0, ui.available_height()), |ui| { Frame::none() .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(10.0)) + .inner_margin(Margin::symmetric(12.0, 10.0)) .show(ui, |ui| { - ui.label( - RichText::new("Friends & Shares") - .strong() - .size(13.0) - .color(theme.text_primary), - ); + 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.add_space(6.0); ui.separator(); ui.add_space(6.0); @@ -2778,47 +2825,74 @@ impl ZeroSendApp { ScrollArea::vertical().show(ui, |ui| { for peer in &online_peers_with_shares { let (peer_display, _) = self.get_peer_display_name(peer); - 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)); - }); + + 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); - 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 = if folder.read_only { "RO" } else { "RW" }; - let btn_text = format!("📁 {} [{}]", folder.name, badge); + 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 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)); + 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)); - 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.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); + } + }); ui.add_space(6.0); } }); }); }); - ui.add_space(10.0); + ui.add_space(8.0); - // Right Column: Active Share Explorer + // ── Right Column: Active Share Explorer ── ui.vertical(|ui| { if let (Some(peer_id), Some(folder_id)) = ( self.selected_peer_id_for_share.clone(), @@ -2828,39 +2902,70 @@ impl ZeroSendApp { let peer = match peer_opt { Some(p) => p, None => { - ui.label("Selected peer is offline."); + 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))); + }); return; } }; let (peer_name, _) = self.get_peer_display_name(&peer); - // Top Explorer Header & Breadcrumbs + // Top Explorer Header & Interactive Breadcrumbs Bar Frame::none() .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(10.0)) + .inner_margin(Margin::symmetric(10.0, 8.0)) .show(ui, |ui| { ui.horizontal(|ui| { - if ui.button("🏠 Root").clicked() { + if ui.button(RichText::new("🏠 Корень").strong().color(theme.text_accent)).clicked() { self.current_share_subpath = String::new(); self.refresh_current_share_tree(); } + let mut navigate_subpath = None; if !self.current_share_subpath.is_empty() { - ui.label("/"); - ui.label( - RichText::new(&self.current_share_subpath) - .monospace() - .strong() - .color(theme.text_primary), - ); + 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(target) = navigate_subpath { + self.current_share_subpath = target; + self.refresh_current_share_tree(); } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + // Search filter box + ui.add( + egui::TextEdit::singleline(&mut self.share_search_query) + .hint_text("🔍 Фильтр...") + .desired_width(120.0), + ); + // Grep search button - if ui.button(RichText::new("🔎 Поиск текста/кода").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(); @@ -2873,7 +2978,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); @@ -2883,18 +2988,11 @@ impl ZeroSendApp { } } - // Search filter box - ui.add( - egui::TextEdit::singleline(&mut self.share_search_query) - .hint_text("🔍 Фильтр файлов...") - .desired_width(130.0), - ); - // Upload button if folder is Read-Write if let Some(tree) = &self.current_share_tree { if !tree.read_only { let upload_btn = Button::new( - RichText::new("⬆️ Upload File") + RichText::new("⬆️ Загрузить") .strong() .color(Color32::WHITE), ) @@ -2927,7 +3025,6 @@ impl ZeroSendApp { } } } - // 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)); } @@ -2940,7 +3037,7 @@ impl ZeroSendApp { }); }); - ui.add_space(8.0); + ui.add_space(6.0); // Multi-select batch action toolbar if !self.selected_share_items.is_empty() { @@ -2951,15 +3048,15 @@ impl ZeroSendApp { .inner_margin(Margin::symmetric(12.0, 8.0)) .show(ui, |ui| { ui.horizontal(|ui| { - ui.label(RichText::new(format!("Selected {} item(s)", self.selected_share_items.len())).strong().color(theme.text_primary)); + ui.label(RichText::new(format!("Выбрано: {} элементов", self.selected_share_items.len())).strong().color(theme.text_primary)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("Clear Selection").clicked() { + if ui.button("✕ Снять выбор").clicked() { self.selected_share_items.clear(); } let dl_btn = Button::new( - RichText::new(format!("⬇️ Download Selected ({})", self.selected_share_items.len())) + RichText::new(format!("⬇️ Скачать выбранные ({})", self.selected_share_items.len())) .strong() .color(Color32::WHITE), ) @@ -3013,7 +3110,7 @@ impl ZeroSendApp { ui.vertical_centered(|ui| { ui.add_space(30.0); ui.spinner(); - ui.label("Loading shared contents..."); + ui.label("Загрузка содержимого папки..."); }); } else if let Some(tree) = self.current_share_tree.clone() { let is_writable_share = !tree.read_only; @@ -3028,7 +3125,7 @@ impl ZeroSendApp { .show(ui, |ui| { ui.horizontal(|ui| { ui.label("📁 .."); - if ui.button("Parent Folder").clicked() { + if ui.button("Вверх (Родительская папка)").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; @@ -3069,7 +3166,7 @@ impl ZeroSendApp { ui.label(RichText::new(icon).size(16.0)); if entry.is_dir { - if ui.link(RichText::new(&entry.name).strong().color(theme.text_primary)).clicked() { + if ui.link(RichText::new(&entry.name).strong().color(theme.text_accent)).clicked() { self.current_share_subpath = entry.relative_path.clone(); self.refresh_current_share_tree(); } @@ -3098,7 +3195,7 @@ impl ZeroSendApp { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { // Action: Download let is_dir = entry.is_dir; - let down_label = if is_dir { "⬇️ Zip" } else { "⬇️ Download" }; + let down_label = if is_dir { "⬇️ Zip" } else { "⬇️ Скачать" }; if ui.button(down_label).clicked() { self.switch_tab(Tab::Transfers); let client = self.client.clone(); @@ -3145,7 +3242,7 @@ impl ZeroSendApp { entry.relative_path.trim_start_matches('/') ); - if ui.button(RichText::new("🎵 Play in App").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() { + if ui.button(RichText::new("🎵 Слушать").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() { self.active_audio = Some(AudioTrackInfo { name: entry.name.clone(), url: stream_url.clone(), @@ -3181,7 +3278,7 @@ impl ZeroSendApp { let _ = std::process::Command::new("explorer").arg(&stream_url).spawn(); } - if ui.button(RichText::new("▶️ Stream").color(Color32::from_rgb(56, 189, 248))).clicked() { + if ui.button(RichText::new("▶️ Смотреть").color(Color32::from_rgb(56, 189, 248))).clicked() { let _ = std::process::Command::new("explorer").arg(&stream_url).spawn(); } } @@ -3227,12 +3324,12 @@ impl ZeroSendApp { .inner_margin(Margin::same(20.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { - ui.label(RichText::new("🔒 Folder Locked or Empty").strong().size(14.0).color(theme.text_primary)); + ui.label(RichText::new("🔒 Папка защищена или пуста").strong().size(14.0).color(theme.text_primary)); ui.add_space(6.0); ui.horizontal(|ui| { - ui.label("Enter PIN if required:"); + ui.label("Введите PIN если требуется:"); ui.add(egui::TextEdit::singleline(&mut self.share_pin_input).password(true).desired_width(120.0)); - if ui.button("Unlock / Refresh").clicked() { + if ui.button("Разблокировать / Обновить").clicked() { self.refresh_current_share_tree(); } }); @@ -3244,10 +3341,12 @@ impl ZeroSendApp { .fill(theme.bg_card) .rounding(Rounding::same(8.0)) .stroke(theme.stroke_border) - .inner_margin(Margin::same(40.0)) + .inner_margin(Margin::same(30.0)) .show(ui, |ui| { ui.vertical_centered(|ui| { - 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)); + ui.label(RichText::new("👈 Выберите общую папку слева").size(15.0).strong().color(theme.text_primary)); + ui.add_space(4.0); + ui.label(RichText::new("Выберите узел и папку в списке слева для просмотра и скачивания файлов.").size(12.0).color(theme.text_muted)); }); }); }