From b751d8274882d9bf55ba460778b39827f12b44dc Mon Sep 17 00:00:00 2001 From: RarDog Date: Thu, 27 Aug 2026 17:35:15 +0300 Subject: [PATCH] Release v2.5.3-beta.2: fluid tab transition animations with cubic ease-out and animated sidebar indicator --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/ui/gui.rs | 81 ++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b74ffb..246069d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6453,7 +6453,7 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerosend" -version = "2.5.3-beta.1.1" +version = "2.5.3-beta.2" dependencies = [ "arboard", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index b4c2fa6..de2ed5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosend" -version = "2.5.3-beta.1.1" +version = "2.5.3-beta.2" edition = "2021" description = "Fast, peer-to-peer file transfer designed for ZeroTier, Tailscale, Radmin VPN and LAN" authors = ["RarDog"] diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 92d2bc6..ec255ae 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -37,6 +37,19 @@ pub enum Tab { Settings, } +impl Tab { + pub fn index(&self) -> usize { + match self { + Tab::Nearby => 0, + Tab::Send => 1, + Tab::Receive => 2, + Tab::Shared => 3, + Tab::Transfers => 4, + Tab::Settings => 5, + } + } +} + #[derive(PartialEq, Eq, Clone, Copy)] pub enum SharedSubTab { NetworkShares, @@ -270,6 +283,7 @@ pub struct ZeroSendApp { last_poll: Instant, last_ping_poll: Instant, last_window_save: Instant, + last_tab_change: Instant, } fn setup_custom_fonts(ctx: &egui::Context) { @@ -397,7 +411,7 @@ impl ZeroSendApp { selected_peer_ids: HashSet::new(), queued_files: initial_files, transfers: Vec::new(), - logs: vec!["⚡ ZeroSend v2.5.3-beta.1.1 ready".to_string()], + logs: vec!["⚡ ZeroSend v2.5.3-beta.2 ready".to_string()], text_input: String::new(), analyzed_preview: analyze_text(""), recipient_pin: String::new(), @@ -476,6 +490,7 @@ impl ZeroSendApp { last_poll: Instant::now(), last_ping_poll: Instant::now(), last_window_save: Instant::now(), + last_tab_change: Instant::now(), } } @@ -499,7 +514,7 @@ impl ZeroSendApp { // 2. Poll incoming file transfer requests while let Ok(req) = self.incoming_request_rx.try_recv() { self.pending_request = Some(req.clone()); - self.current_tab = Tab::Transfers; + self.switch_tab(Tab::Transfers); self.logs.push(format!("📥 Incoming transfer request from {}", req.sender_name)); ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false)); @@ -534,7 +549,7 @@ impl ZeroSendApp { } } else { self.transfers.insert(0, t); - self.current_tab = Tab::Transfers; + self.switch_tab(Tab::Transfers); ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false)); ctx.send_viewport_cmd(egui::ViewportCommand::Focus); @@ -737,6 +752,13 @@ impl ZeroSendApp { }); } + pub fn switch_tab(&mut self, new_tab: Tab) { + if self.current_tab != new_tab { + self.current_tab = new_tab; + self.last_tab_change = Instant::now(); + } + } + fn get_peer_display_name(&self, peer: &PeerInfo) -> (String, bool) { if let Some(alias) = self.config.peer_aliases.get(&peer.peer_id) { if !alias.trim().is_empty() { @@ -790,7 +812,7 @@ impl ZeroSendApp { } } if added_any { - self.current_tab = Tab::Send; + self.switch_tab(Tab::Send); self.send_mode = SendMode::Files; self.logs.push("📄 Pasted copied file(s) from clipboard to queue".to_string()); return true; @@ -822,7 +844,7 @@ impl ZeroSendApp { }; self.queued_files.clear(); - self.current_tab = Tab::Transfers; + self.switch_tab(Tab::Transfers); for peer in targets { let peer_url = peer.transfer_url(); @@ -921,7 +943,13 @@ impl ZeroSendApp { (Tab::Settings, "⚙ Настройки и инфо", 0, false), ]; - for (tab, title, count, is_active_pulse) in tabs { + let anim_target = self.current_tab.index() as f32; + let animated_tab_idx = ui.ctx().animate_value_with_time(egui::Id::new("sidebar_active_tab"), anim_target, 0.20); + if (animated_tab_idx - anim_target).abs() > 0.001 { + ui.ctx().request_repaint(); + } + + for (idx, (tab, title, count, is_active_pulse)) in tabs.into_iter().enumerate() { let is_selected = self.current_tab == tab; let mut btn_text = title.to_string(); if count > 0 && tab != Tab::Receive && tab != Tab::Settings { @@ -932,6 +960,9 @@ impl ZeroSendApp { } } + // Smooth proximity factor (1.0 = active, 0.0 = inactive) + let proximity = (1.0 - (animated_tab_idx - (idx as f32)).abs()).clamp(0.0, 1.0); + let text_color = if is_selected { if theme.is_dark { Color32::from_rgb(56, 189, 248) @@ -944,14 +975,23 @@ impl ZeroSendApp { theme.text_secondary }; - let bg_color = if is_selected { - theme.bg_card_active + let bg_color = if proximity > 0.01 { + let r = theme.bg_card_active.r(); + let g = theme.bg_card_active.g(); + let b = theme.bg_card_active.b(); + let a = (theme.bg_card_active.a() as f32 * proximity) as u8; + Color32::from_rgba_premultiplied(r, g, b, a) } else { Color32::TRANSPARENT }; - 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) }) + let stroke = if proximity > 0.25 { + let stroke_color = if theme.is_dark { + Color32::from_rgba_premultiplied(56, 189, 248, (255.0 * proximity) as u8) + } else { + Color32::from_rgba_premultiplied(2, 132, 199, (255.0 * proximity) as u8) + }; + Stroke::new(1.0_f32, stroke_color) } else { Stroke::NONE }; @@ -963,7 +1003,7 @@ impl ZeroSendApp { .rounding(Rounding::same(6.0)); if ui.add(btn).clicked() { - self.current_tab = tab; + self.switch_tab(tab); } ui.add_space(3.0); } @@ -3996,11 +4036,12 @@ impl ZeroSendApp { .stroke(theme.stroke_border) .inner_margin(Margin::same(14.0)) .show(ui, |ui| { - // v2.5.3-beta.1.1 + // v2.5.3-beta.2 ui.horizontal(|ui| { - ui.label(RichText::new("v2.5.3-beta.1.1").strong().color(Color32::from_rgb(34, 197, 94))); + ui.label(RichText::new("v2.5.3-beta.2").strong().color(Color32::from_rgb(34, 197, 94))); ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY)); }); + ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню."); ui.label(" • ⚡ TCP Socket Tuning: 4MB буферы сокетов, TCP_NODELAY и пул соединений через socket2 для максимальной скорости на VPN и LAN."); ui.label(" • 🌊 Zero-Drop Auto-Recovery: Авто-восстановление передач при сбоях Wi-Fi/VPN с экспоненциальным backoff до 8 попыток без обрыва."); ui.label(" • 📡 mDNS & Multicast Discovery: Мультикаст группы (239.255.255.250 и 224.0.0.251) для 100% обнаружения пиров на любых роутерах."); @@ -4392,7 +4433,7 @@ impl ZeroSendApp { server_clone.accept_transfer(&session_id).await; }); self.pending_request = None; - self.current_tab = Tab::Transfers; + self.switch_tab(Tab::Transfers); } let reject_btn = Button::new( @@ -4759,6 +4800,18 @@ impl eframe::App for ZeroSendApp { } } + // Fluid Tab Transition Animation (Smooth Slide-in & Fade-in) + let elapsed = self.last_tab_change.elapsed().as_secs_f32(); + let duration = 0.20; // 200 ms + let progress = (elapsed / duration).min(1.0); + if progress < 1.0 { + ctx.request_repaint(); + } + // Cubic ease-out curve + let ease = 1.0 - (1.0 - progress).powi(3); + let slide_offset = (1.0 - ease) * 12.0; + ui.add_space(slide_offset); + match self.current_tab { Tab::Nearby => self.render_nearby_tab(ui, theme), Tab::Send => self.render_send_tab(ui, theme),