Update Send tab GUI, rich ZeroTier cards, and copyable IP in v2.5.5

This commit is contained in:
RarDog
2026-08-28 14:15:48 +03:00
parent e019ddccc9
commit 124d9c2dd4
+207 -109
View File
@@ -959,33 +959,48 @@ impl ZeroSendApp {
ui.separator(); ui.separator();
ui.add_space(8.0); 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() Frame::none()
.fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(241, 245, 249) }) .fill(if theme.is_dark { Color32::from_rgb(15, 23, 42) } else { Color32::from_rgb(241, 245, 249) })
.rounding(Rounding::same(8.0)) .rounding(Rounding::same(8.0))
.stroke(theme.stroke_border) .stroke(Stroke::new(1.0_f32, net_accent_color.linear_multiply(0.5)))
.inner_margin(Margin::same(10.0)) .inner_margin(Margin::symmetric(10.0, 8.0))
.show(ui, |ui| { .show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let dot_color = if self.current_iface.net_type == NetworkType::ZeroTier { ui.label(RichText::new(net_icon).size(20.0));
Color32::from_rgb(34, 197, 94)
} else {
Color32::from_rgb(59, 130, 246)
};
ui.label(RichText::new("●").color(dot_color).size(14.0));
ui.vertical(|ui| { ui.vertical(|ui| {
ui.label( ui.horizontal(|ui| {
RichText::new(self.current_iface.net_type.as_str()) ui.label(
.strong() RichText::new(net_badge_title)
.size(11.5) .strong()
.color(theme.text_primary), .size(11.5)
); .color(theme.text_primary),
ui.label( );
RichText::new(format!("{}:{}", self.current_iface.ip, self.my_info.transfer_port)) ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(9.0));
.monospace() });
.size(10.0) ui.horizontal(|ui| {
.color(theme.text_muted), 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)); rename_target = Some((peer.peer_id.clone(), peer.name.clone(), curr));
} }
let badge_color = if peer.net_type.contains("ZeroTier") { let (net_badge_icon, net_badge_color) = if peer.net_type.contains("ZeroTier") {
Color32::from_rgb(34, 197, 94) ("🪐 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 { } else {
Color32::from_rgb(59, 130, 246) ("🏠 LAN", Color32::from_rgb(34, 197, 94))
}; };
ui.label(
RichText::new(format!("[{}]", peer.net_type)) Frame::none()
.size(10.5) .fill(theme.bg_card_secondary)
.color(badge_color), .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() { if !peer.shared_folders.is_empty() {
Frame::none() Frame::none()
@@ -1361,12 +1389,12 @@ impl ZeroSendApp {
} }
fn render_send_tab(&mut self, ui: &mut Ui, theme: ThemeColors) { fn render_send_tab(&mut self, ui: &mut Ui, theme: ThemeColors) {
// Mode Switcher Bar // ── Top Mode Switcher Bar ──
Frame::none() Frame::none()
.fill(theme.bg_card) .fill(theme.bg_card)
.rounding(Rounding::same(8.0)) .rounding(Rounding::same(10.0))
.stroke(theme.stroke_border) .stroke(theme.stroke_border)
.inner_margin(Margin::symmetric(12.0, 8.0)) .inner_margin(Margin::symmetric(14.0, 9.0))
.show(ui, |ui| { .show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("📤").size(20.0)); ui.label(RichText::new("📤").size(20.0));
@@ -1382,6 +1410,7 @@ impl ZeroSendApp {
.color(if files_active { Color32::WHITE } else { theme.text_secondary }), .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 }) .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)); .rounding(Rounding::same(6.0));
if ui.add(files_btn).clicked() { if ui.add(files_btn).clicked() {
@@ -1395,6 +1424,7 @@ impl ZeroSendApp {
.color(if text_active { Color32::WHITE } else { theme.text_secondary }), .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 }) .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)); .rounding(Rounding::same(6.0));
if ui.add(text_btn).clicked() { if ui.add(text_btn).clicked() {
@@ -1403,8 +1433,8 @@ impl ZeroSendApp {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("🔒 PIN (опционально):").size(11.0).color(theme.text_muted)); ui.label(RichText::new("🔒 PIN-защита:").size(11.5).color(theme.text_muted));
ui.add(egui::TextEdit::singleline(&mut self.recipient_pin).hint_text("PIN").desired_width(60.0)); 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); 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() Frame::none()
.fill(theme.bg_card) .fill(theme.bg_card)
.rounding(Rounding::same(8.0)) .rounding(Rounding::same(10.0))
.stroke(theme.stroke_border) .stroke(theme.stroke_border)
.inner_margin(Margin::same(12.0)) .inner_margin(Margin::same(12.0))
.show(ui, |ui| { .show(ui, |ui| {
ui.horizontal(|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() { if !self.peers.is_empty() {
let all_selected = self.selected_peer_ids.len() == self.peers.len(); ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.small_button(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех" }).clicked() { let all_selected = selected_peers_count == total_peers;
if all_selected { if ui.button(RichText::new(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех друзей" }).size(11.5).color(theme.text_accent)).clicked() {
self.selected_peer_ids.clear(); if all_selected {
} else { self.selected_peer_ids.clear();
self.selected_peer_ids = self.peers.iter().map(|p| p.peer_id.clone()).collect(); } 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() { 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 { } else {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
for p in &self.peers { for p in &self.peers {
let is_selected = self.selected_peer_ids.contains(&p.peer_id); let is_selected = self.selected_peer_ids.contains(&p.peer_id);
let (name_label, _) = self.get_peer_display_name(p); let (name_label, has_alias) = self.get_peer_display_name(p);
let chip_text = format!("{} ({})", name_label, p.ip);
let chip_btn = Button::new( let os_icon = match p.os.to_lowercase().as_str() {
RichText::new(format!("{} {}", if is_selected { "✅" } else { "⬜" }, chip_text)) "windows" => "🪟",
.size(12.0) "linux" => "🐧",
.strong() "macos" => "🍎",
.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));
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 { if is_selected {
self.selected_peer_ids.remove(&p.peer_id); self.selected_peer_ids.remove(&p.peer_id);
} else { } else {
@@ -1484,10 +1572,10 @@ impl ZeroSendApp {
} }
fn render_send_files_subview(&mut self, ui: &mut Ui, theme: ThemeColors) { fn render_send_files_subview(&mut self, ui: &mut Ui, theme: ThemeColors) {
// Step 2: Choose files // ── Step 2: Choose Files & Folders ──
Frame::none() Frame::none()
.fill(theme.bg_card) .fill(theme.bg_card)
.rounding(Rounding::same(8.0)) .rounding(Rounding::same(10.0))
.stroke(theme.stroke_border) .stroke(theme.stroke_border)
.inner_margin(Margin::same(12.0)) .inner_margin(Margin::same(12.0))
.show(ui, |ui| { .show(ui, |ui| {
@@ -1495,7 +1583,7 @@ impl ZeroSendApp {
ui.label(RichText::new("Шаг 2: Выберите файлы для отправки").strong().size(13.0).color(theme.text_primary)); ui.label(RichText::new("Шаг 2: Выберите файлы для отправки").strong().size(13.0).color(theme.text_primary));
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 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(); self.queued_files.clear();
} }
}); });
@@ -1504,15 +1592,12 @@ impl ZeroSendApp {
ui.add_space(8.0); ui.add_space(8.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui let add_files_btn = Button::new(RichText::new("📄 Добавить файлы...").strong().color(Color32::WHITE))
.add( .fill(Color32::from_rgb(37, 99, 235))
Button::new(RichText::new("📄 Добавить файлы...").strong().color(Color32::WHITE)) .min_size(Vec2::new(145.0, 32.0))
.fill(Color32::from_rgb(59, 130, 246)) .rounding(Rounding::same(6.0));
.min_size(Vec2::new(140.0, 32.0))
.rounding(Rounding::same(6.0)), if ui.add(add_files_btn).clicked() {
)
.clicked()
{
if let Some(files) = FileDialog::new().pick_files() { if let Some(files) = FileDialog::new().pick_files() {
for f in files { for f in files {
if !self.queued_files.contains(&f) { if !self.queued_files.contains(&f) {
@@ -1522,15 +1607,12 @@ impl ZeroSendApp {
} }
} }
if ui let add_folder_btn = Button::new(RichText::new("📁 Добавить папку...").strong().color(Color32::WHITE))
.add( .fill(Color32::from_rgb(79, 70, 229))
Button::new(RichText::new("📁 Добавить папку...").strong().color(Color32::WHITE)) .min_size(Vec2::new(145.0, 32.0))
.fill(Color32::from_rgb(79, 70, 229)) .rounding(Rounding::same(6.0));
.min_size(Vec2::new(140.0, 32.0))
.rounding(Rounding::same(6.0)), if ui.add(add_folder_btn).clicked() {
)
.clicked()
{
if let Some(folder) = FileDialog::new().pick_folder() { if let Some(folder) = FileDialog::new().pick_folder() {
if !self.queued_files.contains(&folder) { if !self.queued_files.contains(&folder) {
self.queued_files.push(folder); self.queued_files.push(folder);
@@ -1538,15 +1620,12 @@ impl ZeroSendApp {
} }
} }
if ui let paste_btn = Button::new(RichText::new("📸 Вставить скриншот (Ctrl+V)").strong().color(Color32::WHITE))
.add( .fill(Color32::from_rgb(147, 51, 234))
Button::new(RichText::new("📸 Вставить скриншот (Ctrl+V)").color(Color32::WHITE)) .min_size(Vec2::new(195.0, 32.0))
.fill(Color32::from_rgb(147, 51, 234)) .rounding(Rounding::same(6.0));
.min_size(Vec2::new(190.0, 32.0))
.rounding(Rounding::same(6.0)), if ui.add(paste_btn).clicked() {
)
.clicked()
{
self.try_paste_clipboard_image(); self.try_paste_clipboard_image();
} }
}); });
@@ -1562,7 +1641,7 @@ impl ZeroSendApp {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(format!( RichText::new(format!(
"Выбрано: {} элементов ({}) | Получателей: {}", "Выбрано: {} элементов ({}) • Получателей: {}",
self.queued_files.len(), self.queued_files.len(),
human_bytes(total_size as f64), human_bytes(total_size as f64),
self.selected_peer_ids.len() self.selected_peer_ids.len()
@@ -1572,13 +1651,19 @@ impl ZeroSendApp {
.color(theme.text_primary), .color(theme.text_primary),
); );
if self.auto_zip_folders { 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); ui.add_space(6.0);
// Files Dropzone Box // ── Files Dropzone Box ──
Frame::none() Frame::none()
.fill(theme.bg_card_secondary) .fill(theme.bg_card_secondary)
.rounding(Rounding::same(8.0)) .rounding(Rounding::same(8.0))
@@ -1588,11 +1673,13 @@ impl ZeroSendApp {
ScrollArea::vertical().max_height(200.0).show(ui, |ui| { ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
if self.queued_files.is_empty() { if self.queued_files.is_empty() {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(20.0); ui.add_space(24.0);
ui.label(RichText::new("📂 Перетащите сюда файлы или папки (Drag & Drop)").size(13.5).strong().color(theme.text_muted)); ui.label(RichText::new("📂").size(32.0));
ui.add_space(4.0); ui.add_space(4.0);
ui.label(RichText::new("или вставьте скриншот из буфера обмена (Ctrl+V)").size(11.0).color(theme.text_muted)); ui.label(RichText::new("Перетащите файлы или папки сюда (Drag & Drop)").size(13.5).strong().color(theme.text_primary));
ui.add_space(20.0); ui.add_space(2.0);
ui.label(RichText::new("или нажмите Ctrl+V для быстрой вставки файлов и скриншотов из буфера обмена").size(11.5).color(theme.text_muted));
ui.add_space(24.0);
}); });
} else { } else {
let mut to_remove = None; 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.label(RichText::new(format!("({})", size_str)).size(11.0).color(theme.text_muted));
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 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); to_remove = Some(idx);
} }
}); });
@@ -1643,9 +1730,15 @@ impl ZeroSendApp {
ui.add_space(10.0); ui.add_space(10.0);
// Step 3: Big Send Button // ── Step 3: Big Send Button ──
let num_recipients = self.selected_peer_ids.len(); 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 can_send = !self.queued_files.is_empty() && num_recipients > 0;
let send_btn_text = if num_recipients == 1 { let send_btn_text = if num_recipients == 1 {
let target_name = self let target_name = self
.peers .peers
@@ -1653,9 +1746,11 @@ impl ZeroSendApp {
.find(|p| self.selected_peer_ids.contains(&p.peer_id)) .find(|p| self.selected_peer_ids.contains(&p.peer_id))
.map(|p| self.get_peer_display_name(p).0) .map(|p| self.get_peer_display_name(p).0)
.unwrap_or_else(|| "...".to_string()); .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 { } 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)) let send_btn = Button::new(RichText::new(send_btn_text).size(14.5).strong().color(Color32::WHITE))
@@ -1664,7 +1759,8 @@ impl ZeroSendApp {
} else { } else {
if theme.is_dark { Color32::from_rgb(51, 65, 85) } else { Color32::from_rgb(203, 213, 225) } 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)); .rounding(Rounding::same(8.0));
if ui.add_enabled(can_send, send_btn).clicked() { 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("v2.5.5").strong().color(Color32::from_rgb(34, 197, 94)));
ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY)); 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(" • 🚀 Мгновенный переход в «Передачи и файлы»: При скачивании любых файлов или архивов из общих папок друзей приложение сразу переключается во вкладку передач с отображением живой скорости (MB/s) и прогресса.");
ui.label(" • 🎨 Премиальный редизайн Сетевых общих папок: Удобная боковая панель с пингом и статусами друзей, интерактивные хлебные крошки (breadcrumbs), красивые карточки файлов по категориям и мульти-загрузка."); ui.label(" • 🌐 Премиальный проводник Сетевых общих папок: Удобная боковая панель с пингом и статусами друзей, интерактивные хлебные крошки (breadcrumbs), красивые карточки файлов по категориям и мульти-загрузка.");
ui.label(" • 🔄 Все ключевые улучшения v2.5.4: авто-обнаружение папок в реальном времени, отслеживание скачиваний из общих папок, прозрачный стриминг папок и сохранение истории передач на диск."); ui.label(" • 🔄 Все ключевые улучшения v2.5.4: авто-обнаружение папок в реальном времени, отслеживание скачиваний из общих папок, прозрачный стриминг папок и сохранение истории передач на диск.");
ui.add_space(8.0); ui.add_space(8.0);
ui.separator(); ui.separator();