Update Send tab GUI, rich ZeroTier cards, and copyable IP in v2.5.5
This commit is contained in:
+184
-86
@@ -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.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(self.current_iface.net_type.as_str())
|
RichText::new(net_badge_title)
|
||||||
.strong()
|
.strong()
|
||||||
.size(11.5)
|
.size(11.5)
|
||||||
.color(theme.text_primary),
|
.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(
|
ui.label(
|
||||||
RichText::new(format!("{}:{}", self.current_iface.ip, self.my_info.transfer_port))
|
RichText::new(&ip_str)
|
||||||
.monospace()
|
.monospace()
|
||||||
.size(10.0)
|
.size(10.0)
|
||||||
.color(theme.text_muted),
|
.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))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(
|
ui.label(
|
||||||
RichText::new(format!("[{}]", peer.net_type))
|
RichText::new(net_badge_icon)
|
||||||
.size(10.5)
|
.size(9.5)
|
||||||
.color(badge_color),
|
.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 ui.button(RichText::new(if all_selected { "✕ Снять выбор со всех" } else { "✓ Выбрать всех друзей" }).size(11.5).color(theme.text_accent)).clicked() {
|
||||||
if all_selected {
|
if all_selected {
|
||||||
self.selected_peer_ids.clear();
|
self.selected_peer_ids.clear();
|
||||||
} else {
|
} else {
|
||||||
self.selected_peer_ids = self.peers.iter().map(|p| p.peer_id.clone()).collect();
|
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 }),
|
_ => "💻",
|
||||||
)
|
};
|
||||||
|
|
||||||
|
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 {
|
.fill(if is_selected {
|
||||||
Color32::from_rgb(37, 99, 235)
|
if theme.is_dark { Color32::from_rgb(23, 37, 84) } else { Color32::from_rgb(219, 234, 254) }
|
||||||
} else {
|
} else {
|
||||||
theme.bg_card_secondary
|
theme.bg_card_secondary
|
||||||
})
|
})
|
||||||
.stroke(if is_selected {
|
.rounding(Rounding::same(8.0))
|
||||||
Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250))
|
.stroke(stroke)
|
||||||
} else {
|
.inner_margin(Margin::symmetric(10.0, 6.0))
|
||||||
Stroke::NONE
|
.show(ui, |ui| {
|
||||||
})
|
ui.horizontal(|ui| {
|
||||||
.rounding(Rounding::same(6.0));
|
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 }),
|
||||||
|
);
|
||||||
|
|
||||||
if ui.add(chip_btn).clicked() {
|
// 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(
|
|
||||||
Button::new(RichText::new("📁 Добавить папку...").strong().color(Color32::WHITE))
|
|
||||||
.fill(Color32::from_rgb(79, 70, 229))
|
.fill(Color32::from_rgb(79, 70, 229))
|
||||||
.min_size(Vec2::new(140.0, 32.0))
|
.min_size(Vec2::new(145.0, 32.0))
|
||||||
.rounding(Rounding::same(6.0)),
|
.rounding(Rounding::same(6.0));
|
||||||
)
|
|
||||||
.clicked()
|
if ui.add(add_folder_btn).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(
|
|
||||||
Button::new(RichText::new("📸 Вставить скриншот (Ctrl+V)").color(Color32::WHITE))
|
|
||||||
.fill(Color32::from_rgb(147, 51, 234))
|
.fill(Color32::from_rgb(147, 51, 234))
|
||||||
.min_size(Vec2::new(190.0, 32.0))
|
.min_size(Vec2::new(195.0, 32.0))
|
||||||
.rounding(Rounding::same(6.0)),
|
.rounding(Rounding::same(6.0));
|
||||||
)
|
|
||||||
.clicked()
|
if ui.add(paste_btn).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();
|
||||||
|
|||||||
Reference in New Issue
Block a user