Release v2.5.3-beta.2: fluid tab transition animations with cubic ease-out and animated sidebar indicator
This commit is contained in:
+67
-14
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user