Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b751d82748 | ||
|
|
c27ac0f41c | ||
|
|
f65b15452f |
Generated
+1
-1
@@ -6453,7 +6453,7 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerosend"
|
name = "zerosend"
|
||||||
version = "2.5.3-beta.1"
|
version = "2.5.3-beta.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zerosend"
|
name = "zerosend"
|
||||||
version = "2.5.3-beta.1"
|
version = "2.5.3-beta.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast, peer-to-peer file transfer designed for ZeroTier, Tailscale, Radmin VPN and LAN"
|
description = "Fast, peer-to-peer file transfer designed for ZeroTier, Tailscale, Radmin VPN and LAN"
|
||||||
authors = ["RarDog"]
|
authors = ["RarDog"]
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ use tokio::sync::RwLock;
|
|||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
pub const DEFAULT_DISCOVERY_PORT: u16 = 53317;
|
pub const DEFAULT_DISCOVERY_PORT: u16 = 53317;
|
||||||
|
pub const MULTICAST_IPV4_DISCOVERY: Ipv4Addr = Ipv4Addr::new(239, 255, 255, 250);
|
||||||
|
pub const MDNS_IPV4_DISCOVERY: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const DEFAULT_TRANSFER_PORT: u16 = 53318;
|
pub const DEFAULT_TRANSFER_PORT: u16 = 53318;
|
||||||
pub const BEACON_INTERVAL_SECS: u64 = 3;
|
pub const BEACON_INTERVAL_SECS: u64 = 3;
|
||||||
@@ -143,8 +145,10 @@ impl DiscoveryService {
|
|||||||
let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
|
let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
|
||||||
self.shutdown_tx = Some(shutdown_tx.clone());
|
self.shutdown_tx = Some(shutdown_tx.clone());
|
||||||
|
|
||||||
// 1. Create UDP broadcast sending socket
|
// 1. Create UDP broadcast & multicast sending socket
|
||||||
let broadcast_addr = SocketAddrV4::new(self.selected_iface.broadcast, DEFAULT_DISCOVERY_PORT);
|
let broadcast_addr = SocketAddrV4::new(self.selected_iface.broadcast, DEFAULT_DISCOVERY_PORT);
|
||||||
|
let multicast_addr = SocketAddrV4::new(MULTICAST_IPV4_DISCOVERY, DEFAULT_DISCOVERY_PORT);
|
||||||
|
let mdns_addr = SocketAddrV4::new(MDNS_IPV4_DISCOVERY, DEFAULT_DISCOVERY_PORT);
|
||||||
let my_info_static = self.my_info.clone();
|
let my_info_static = self.my_info.clone();
|
||||||
let shared_folders = self.shared_folders.clone();
|
let shared_folders = self.shared_folders.clone();
|
||||||
let mut shutdown_rx1 = shutdown_tx.subscribe();
|
let mut shutdown_rx1 = shutdown_tx.subscribe();
|
||||||
@@ -163,8 +167,8 @@ impl DiscoveryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Started discovery beacon on {} (broadcast: {})",
|
"Started discovery beacon on {} (broadcast: {}, multicast: {})",
|
||||||
my_info_static.ip, broadcast_addr
|
my_info_static.ip, broadcast_addr, multicast_addr
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(BEACON_INTERVAL_SECS));
|
let mut interval = tokio::time::interval(Duration::from_secs(BEACON_INTERVAL_SECS));
|
||||||
@@ -175,6 +179,8 @@ impl DiscoveryService {
|
|||||||
current_beacon.shared_folders = shared_folders.read().await.clone();
|
current_beacon.shared_folders = shared_folders.read().await.clone();
|
||||||
if let Ok(data) = serde_json::to_vec(¤t_beacon) {
|
if let Ok(data) = serde_json::to_vec(¤t_beacon) {
|
||||||
let _ = socket.send_to(&data, broadcast_addr).await;
|
let _ = socket.send_to(&data, broadcast_addr).await;
|
||||||
|
let _ = socket.send_to(&data, multicast_addr).await;
|
||||||
|
let _ = socket.send_to(&data, mdns_addr).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = shutdown_rx1.recv() => {
|
_ = shutdown_rx1.recv() => {
|
||||||
@@ -185,15 +191,16 @@ impl DiscoveryService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Create UDP listener socket
|
// 2. Create UDP listener socket with Multicast support
|
||||||
let listener_socket = match create_udp_listener(DEFAULT_DISCOVERY_PORT) {
|
let iface_ip = self.selected_iface.ip;
|
||||||
|
let listener_socket = match create_udp_listener(DEFAULT_DISCOVERY_PORT, iface_ip) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(
|
warn!(
|
||||||
"Could not bind listener on port {}: {}. Will try random port.",
|
"Could not bind listener on port {}: {}. Will try random port.",
|
||||||
DEFAULT_DISCOVERY_PORT, e
|
DEFAULT_DISCOVERY_PORT, e
|
||||||
);
|
);
|
||||||
create_udp_listener(0)?
|
create_udp_listener(0, iface_ip)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,13 +292,13 @@ impl DiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to create a reusable broadcast socket on Windows / Linux / macOS
|
/// Helper to create a reusable broadcast & multicast socket on Windows / Linux / macOS
|
||||||
fn create_udp_listener(port: u16) -> std::io::Result<UdpSocket> {
|
fn create_udp_listener(port: u16, iface_ip: Ipv4Addr) -> std::io::Result<UdpSocket> {
|
||||||
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
|
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
|
||||||
socket.set_reuse_address(true)?;
|
socket.set_reuse_address(true)?;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
socket.set_reuse_port(true)?;
|
let _ = socket.set_reuse_port(true);
|
||||||
|
|
||||||
socket.set_broadcast(true)?;
|
socket.set_broadcast(true)?;
|
||||||
socket.set_nonblocking(true)?;
|
socket.set_nonblocking(true)?;
|
||||||
@@ -299,6 +306,10 @@ fn create_udp_listener(port: u16) -> std::io::Result<UdpSocket> {
|
|||||||
let bind_addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
|
let bind_addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||||
socket.bind(&bind_addr.into())?;
|
socket.bind(&bind_addr.into())?;
|
||||||
|
|
||||||
|
// Join local SSDP/ZeroSend multicast group and mDNS group
|
||||||
|
let _ = socket.join_multicast_v4(&MULTICAST_IPV4_DISCOVERY, &iface_ip);
|
||||||
|
let _ = socket.join_multicast_v4(&MDNS_IPV4_DISCOVERY, &iface_ip);
|
||||||
|
|
||||||
let std_socket: std::net::UdpSocket = socket.into();
|
let std_socket: std::net::UdpSocket = socket.into();
|
||||||
UdpSocket::from_std(std_socket)
|
UdpSocket::from_std(std_socket)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
|||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Compress a whole folder into a ZIP archive with fast Deflate compression
|
/// Compress a whole folder into a ZIP archive with fast Deflate compression
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn compress_folder_to_zip(
|
pub fn compress_folder_to_zip(
|
||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
zip_dest: &Path,
|
zip_dest: &Path,
|
||||||
|
|||||||
+14
-2
@@ -70,6 +70,9 @@ impl TransferClient {
|
|||||||
.timeout(Duration::from_secs(7200))
|
.timeout(Duration::from_secs(7200))
|
||||||
.connect_timeout(Duration::from_secs(6))
|
.connect_timeout(Duration::from_secs(6))
|
||||||
.tcp_nodelay(true)
|
.tcp_nodelay(true)
|
||||||
|
.tcp_keepalive(Some(Duration::from_secs(15)))
|
||||||
|
.pool_idle_timeout(Duration::from_secs(120))
|
||||||
|
.pool_max_idle_per_host(64)
|
||||||
.build()
|
.build()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -93,12 +96,14 @@ impl TransferClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn is_paused(&self, session_id: &str) -> bool {
|
pub async fn is_paused(&self, session_id: &str) -> bool {
|
||||||
let flags = self.pause_flags.lock().await;
|
let flags = self.pause_flags.lock().await;
|
||||||
flags.get(session_id).map(|f| f.load(Ordering::Relaxed)).unwrap_or(false)
|
flags.get(session_id).map(|f| f.load(Ordering::Relaxed)).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Race-ping multiple candidate URLs and return the fastest responding one
|
/// Race-ping multiple candidate URLs and return the fastest responding one
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn pick_fastest_url(&self, candidates: &[String]) -> Option<String> {
|
pub async fn pick_fastest_url(&self, candidates: &[String]) -> Option<String> {
|
||||||
if candidates.is_empty() { return None; }
|
if candidates.is_empty() { return None; }
|
||||||
if candidates.len() == 1 { return Some(candidates[0].clone()); }
|
if candidates.len() == 1 { return Some(candidates[0].clone()); }
|
||||||
@@ -679,9 +684,14 @@ impl TransferClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !is_ok {
|
if !is_ok {
|
||||||
if chunk_task.retries < 3 {
|
if chunk_task.retries < 8 {
|
||||||
chunk_task.retries += 1;
|
chunk_task.retries += 1;
|
||||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
let backoff = (250 * (1 << chunk_task.retries.min(5))).min(4000);
|
||||||
|
info!(
|
||||||
|
"Zero-Drop Auto-Recovery: retrying chunk {} (attempt {}/8) after {}ms",
|
||||||
|
chunk_task.rel_path, chunk_task.retries, backoff
|
||||||
|
);
|
||||||
|
tokio::time::sleep(Duration::from_millis(backoff)).await;
|
||||||
tasks_c.lock().await.push_back(chunk_task);
|
tasks_c.lock().await.push_back(chunk_task);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
@@ -1032,6 +1042,7 @@ impl TransferClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch active watch-party state from host peer
|
/// Fetch active watch-party state from host peer
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn get_watch_party(
|
pub async fn get_watch_party(
|
||||||
&self,
|
&self,
|
||||||
peer_url: &str,
|
peer_url: &str,
|
||||||
@@ -1054,6 +1065,7 @@ impl TransferClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send watch-party sync event to host peer
|
/// Send watch-party sync event to host peer
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn send_watch_party_event(
|
pub async fn send_watch_party_event(
|
||||||
&self,
|
&self,
|
||||||
peer_url: &str,
|
peer_url: &str,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use tracing::info;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SyncStatusUpdate {
|
pub struct SyncStatusUpdate {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub pair_id: String,
|
pub pair_id: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub is_error: bool,
|
pub is_error: bool,
|
||||||
@@ -78,6 +79,7 @@ impl FolderSyncEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn pairs_handle(&self) -> Arc<RwLock<Vec<FolderSyncPairConfig>>> {
|
pub fn pairs_handle(&self) -> Arc<RwLock<Vec<FolderSyncPairConfig>>> {
|
||||||
self.pairs.clone()
|
self.pairs.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-2
@@ -222,8 +222,42 @@ impl TransferServer {
|
|||||||
.with_state(self.state.clone());
|
.with_state(self.state.clone());
|
||||||
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let socket = socket2::Socket::new(
|
||||||
info!("ZeroSend HTTP server listening on {}", addr);
|
socket2::Domain::IPV4,
|
||||||
|
socket2::Type::STREAM,
|
||||||
|
Some(socket2::Protocol::TCP),
|
||||||
|
)?;
|
||||||
|
socket.set_reuse_address(true)?;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _ = socket.set_reuse_port(true);
|
||||||
|
let _ = socket.set_recv_buffer_size(4 * 1024 * 1024); // 4MB TCP Receive Window
|
||||||
|
let _ = socket.set_send_buffer_size(4 * 1024 * 1024); // 4MB TCP Send Window
|
||||||
|
let _ = socket.set_nodelay(true);
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
socket.bind(&addr.into())?;
|
||||||
|
socket.listen(1024)?;
|
||||||
|
let std_listener: std::net::TcpListener = socket.into();
|
||||||
|
let listener = tokio::net::TcpListener::from_std(std_listener)?;
|
||||||
|
info!("ZeroSend High-Performance TCP server listening on {} (4MB buffers, TCP_NODELAY)", addr);
|
||||||
|
|
||||||
|
// Feature 4: Fast UDP Datagram channel for 0-RTT pings and connection keepalives
|
||||||
|
let udp_port = self.port;
|
||||||
|
let mut udp_shutdown = shutdown_tx.subscribe();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Ok(udp) = tokio::net::UdpSocket::bind(("0.0.0.0", udp_port)).await {
|
||||||
|
let mut buf = [0u8; 128];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Ok((n, src)) = udp.recv_from(&mut buf) => {
|
||||||
|
if n >= 4 && &buf[..4] == b"PING" {
|
||||||
|
let _ = udp.send_to(b"PONG", src).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = udp_shutdown.recv() => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut shutdown_rx = shutdown_tx.subscribe();
|
let mut shutdown_rx = shutdown_tx.subscribe();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -660,6 +694,9 @@ async fn handle_finish_transfer(
|
|||||||
let part_path = state.download_dir.join(format!("{}.zerosend_part", safe_rel_path.to_string_lossy()));
|
let part_path = state.download_dir.join(format!("{}.zerosend_part", safe_rel_path.to_string_lossy()));
|
||||||
|
|
||||||
if part_path.exists() {
|
if part_path.exists() {
|
||||||
|
if target_path.exists() {
|
||||||
|
let _ = tokio::fs::remove_file(&target_path).await;
|
||||||
|
}
|
||||||
let _ = tokio::fs::rename(&part_path, &target_path).await;
|
let _ = tokio::fs::rename(&part_path, &target_path).await;
|
||||||
if let Some(expected_hash) = &f.sha256 {
|
if let Some(expected_hash) = &f.sha256 {
|
||||||
if let Ok(actual_hash) = super::client::compute_file_sha256(&target_path) {
|
if let Ok(actual_hash) = super::client::compute_file_sha256(&target_path) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use tracing::{info, warn};
|
|||||||
|
|
||||||
pub struct WebDavState {
|
pub struct WebDavState {
|
||||||
pub shared_folders: Arc<RwLock<Vec<SharedFolderConfig>>>,
|
pub shared_folders: Arc<RwLock<Vec<SharedFolderConfig>>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +390,7 @@ mod urlencoding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to mount Windows Network Drive via `net use`
|
/// Helper function to mount Windows Network Drive via `net use`
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn mount_webdav_drive(drive_letter: char, port: u16) -> Result<String, String> {
|
pub fn mount_webdav_drive(drive_letter: char, port: u16) -> Result<String, String> {
|
||||||
let drive = format!("{}:", drive_letter);
|
let drive = format!("{}:", drive_letter);
|
||||||
let url = format!("http://127.0.0.1:{}/webdav", port);
|
let url = format!("http://127.0.0.1:{}/webdav", port);
|
||||||
@@ -412,6 +414,7 @@ pub fn mount_webdav_drive(drive_letter: char, port: u16) -> Result<String, Strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to unmount Windows Network Drive
|
/// Helper function to unmount Windows Network Drive
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn unmount_webdav_drive(drive_letter: char) -> Result<(), String> {
|
pub fn unmount_webdav_drive(drive_letter: char) -> Result<(), String> {
|
||||||
let drive = format!("{}:", drive_letter);
|
let drive = format!("{}:", drive_letter);
|
||||||
let output = std::process::Command::new("net")
|
let output = std::process::Command::new("net")
|
||||||
|
|||||||
+79
-19
@@ -37,6 +37,19 @@ pub enum Tab {
|
|||||||
Settings,
|
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)]
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum SharedSubTab {
|
pub enum SharedSubTab {
|
||||||
NetworkShares,
|
NetworkShares,
|
||||||
@@ -49,6 +62,7 @@ pub struct AudioTrackInfo {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub peer_name: String,
|
pub peer_name: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub is_playing: bool,
|
pub is_playing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +226,7 @@ pub struct ZeroSendApp {
|
|||||||
|
|
||||||
// Media & Audio & Watch Party
|
// Media & Audio & Watch Party
|
||||||
pub active_audio: Option<AudioTrackInfo>,
|
pub active_audio: Option<AudioTrackInfo>,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub audio_volume: f32,
|
pub audio_volume: f32,
|
||||||
pub watch_party_session: Option<crate::transfer::WatchPartySyncEvent>,
|
pub watch_party_session: Option<crate::transfer::WatchPartySyncEvent>,
|
||||||
pub is_in_watch_party: bool,
|
pub is_in_watch_party: bool,
|
||||||
@@ -232,8 +247,11 @@ pub struct ZeroSendApp {
|
|||||||
pub new_sync_folder_id: String,
|
pub new_sync_folder_id: String,
|
||||||
|
|
||||||
// WebDAV Windows Drive Mount State
|
// WebDAV Windows Drive Mount State
|
||||||
|
#[allow(dead_code)]
|
||||||
pub webdav_mounted: bool,
|
pub webdav_mounted: bool,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub webdav_drive_letter: char,
|
pub webdav_drive_letter: char,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub webdav_status_msg: Option<String>,
|
pub webdav_status_msg: Option<String>,
|
||||||
|
|
||||||
// Gitea Updater State
|
// Gitea Updater State
|
||||||
@@ -255,6 +273,7 @@ pub struct ZeroSendApp {
|
|||||||
share_text_rx: mpsc::UnboundedReceiver<(String, String, String, String, Option<String>, Option<String>, Option<String>, bool)>,
|
share_text_rx: mpsc::UnboundedReceiver<(String, String, String, String, Option<String>, Option<String>, Option<String>, bool)>,
|
||||||
share_text_tx: mpsc::UnboundedSender<(String, String, String, String, Option<String>, Option<String>, Option<String>, bool)>,
|
share_text_tx: mpsc::UnboundedSender<(String, String, String, String, Option<String>, Option<String>, Option<String>, bool)>,
|
||||||
sync_status_rx: mpsc::UnboundedReceiver<crate::transfer::SyncStatusUpdate>,
|
sync_status_rx: mpsc::UnboundedReceiver<crate::transfer::SyncStatusUpdate>,
|
||||||
|
#[allow(dead_code)]
|
||||||
sync_status_tx: mpsc::UnboundedSender<crate::transfer::SyncStatusUpdate>,
|
sync_status_tx: mpsc::UnboundedSender<crate::transfer::SyncStatusUpdate>,
|
||||||
update_rx: mpsc::UnboundedReceiver<Result<UpdateInfo, String>>,
|
update_rx: mpsc::UnboundedReceiver<Result<UpdateInfo, String>>,
|
||||||
update_tx: mpsc::UnboundedSender<Result<UpdateInfo, String>>,
|
update_tx: mpsc::UnboundedSender<Result<UpdateInfo, String>>,
|
||||||
@@ -264,6 +283,7 @@ pub struct ZeroSendApp {
|
|||||||
last_poll: Instant,
|
last_poll: Instant,
|
||||||
last_ping_poll: Instant,
|
last_ping_poll: Instant,
|
||||||
last_window_save: Instant,
|
last_window_save: Instant,
|
||||||
|
last_tab_change: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_custom_fonts(ctx: &egui::Context) {
|
fn setup_custom_fonts(ctx: &egui::Context) {
|
||||||
@@ -391,7 +411,7 @@ impl ZeroSendApp {
|
|||||||
selected_peer_ids: HashSet::new(),
|
selected_peer_ids: HashSet::new(),
|
||||||
queued_files: initial_files,
|
queued_files: initial_files,
|
||||||
transfers: Vec::new(),
|
transfers: Vec::new(),
|
||||||
logs: vec!["⚡ ZeroSend v2.5.3.1-beta ready".to_string()],
|
logs: vec!["⚡ ZeroSend v2.5.3-beta.2 ready".to_string()],
|
||||||
text_input: String::new(),
|
text_input: String::new(),
|
||||||
analyzed_preview: analyze_text(""),
|
analyzed_preview: analyze_text(""),
|
||||||
recipient_pin: String::new(),
|
recipient_pin: String::new(),
|
||||||
@@ -470,6 +490,7 @@ impl ZeroSendApp {
|
|||||||
last_poll: Instant::now(),
|
last_poll: Instant::now(),
|
||||||
last_ping_poll: Instant::now(),
|
last_ping_poll: Instant::now(),
|
||||||
last_window_save: Instant::now(),
|
last_window_save: Instant::now(),
|
||||||
|
last_tab_change: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +514,7 @@ impl ZeroSendApp {
|
|||||||
// 2. Poll incoming file transfer requests
|
// 2. Poll incoming file transfer requests
|
||||||
while let Ok(req) = self.incoming_request_rx.try_recv() {
|
while let Ok(req) = self.incoming_request_rx.try_recv() {
|
||||||
self.pending_request = Some(req.clone());
|
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));
|
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::Visible(true));
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
||||||
@@ -528,7 +549,7 @@ impl ZeroSendApp {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.transfers.insert(0, t);
|
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::Visible(true));
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||||
@@ -731,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) {
|
fn get_peer_display_name(&self, peer: &PeerInfo) -> (String, bool) {
|
||||||
if let Some(alias) = self.config.peer_aliases.get(&peer.peer_id) {
|
if let Some(alias) = self.config.peer_aliases.get(&peer.peer_id) {
|
||||||
if !alias.trim().is_empty() {
|
if !alias.trim().is_empty() {
|
||||||
@@ -784,7 +812,7 @@ impl ZeroSendApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if added_any {
|
if added_any {
|
||||||
self.current_tab = Tab::Send;
|
self.switch_tab(Tab::Send);
|
||||||
self.send_mode = SendMode::Files;
|
self.send_mode = SendMode::Files;
|
||||||
self.logs.push("📄 Pasted copied file(s) from clipboard to queue".to_string());
|
self.logs.push("📄 Pasted copied file(s) from clipboard to queue".to_string());
|
||||||
return true;
|
return true;
|
||||||
@@ -816,7 +844,7 @@ impl ZeroSendApp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.queued_files.clear();
|
self.queued_files.clear();
|
||||||
self.current_tab = Tab::Transfers;
|
self.switch_tab(Tab::Transfers);
|
||||||
|
|
||||||
for peer in targets {
|
for peer in targets {
|
||||||
let peer_url = peer.transfer_url();
|
let peer_url = peer.transfer_url();
|
||||||
@@ -915,7 +943,13 @@ impl ZeroSendApp {
|
|||||||
(Tab::Settings, "⚙ Настройки и инфо", 0, false),
|
(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 is_selected = self.current_tab == tab;
|
||||||
let mut btn_text = title.to_string();
|
let mut btn_text = title.to_string();
|
||||||
if count > 0 && tab != Tab::Receive && tab != Tab::Settings {
|
if count > 0 && tab != Tab::Receive && tab != Tab::Settings {
|
||||||
@@ -926,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 {
|
let text_color = if is_selected {
|
||||||
if theme.is_dark {
|
if theme.is_dark {
|
||||||
Color32::from_rgb(56, 189, 248)
|
Color32::from_rgb(56, 189, 248)
|
||||||
@@ -938,14 +975,23 @@ impl ZeroSendApp {
|
|||||||
theme.text_secondary
|
theme.text_secondary
|
||||||
};
|
};
|
||||||
|
|
||||||
let bg_color = if is_selected {
|
let bg_color = if proximity > 0.01 {
|
||||||
theme.bg_card_active
|
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 {
|
} else {
|
||||||
Color32::TRANSPARENT
|
Color32::TRANSPARENT
|
||||||
};
|
};
|
||||||
|
|
||||||
let stroke = if is_selected {
|
let stroke = if proximity > 0.25 {
|
||||||
Stroke::new(1.0_f32, if theme.is_dark { Color32::from_rgb(56, 189, 248) } else { Color32::from_rgb(2, 132, 199) })
|
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 {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
};
|
};
|
||||||
@@ -957,7 +1003,7 @@ impl ZeroSendApp {
|
|||||||
.rounding(Rounding::same(6.0));
|
.rounding(Rounding::same(6.0));
|
||||||
|
|
||||||
if ui.add(btn).clicked() {
|
if ui.add(btn).clicked() {
|
||||||
self.current_tab = tab;
|
self.switch_tab(tab);
|
||||||
}
|
}
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
}
|
}
|
||||||
@@ -3990,18 +4036,20 @@ impl ZeroSendApp {
|
|||||||
.stroke(theme.stroke_border)
|
.stroke(theme.stroke_border)
|
||||||
.inner_margin(Margin::same(14.0))
|
.inner_margin(Margin::same(14.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
// v2.5.3.1-beta
|
// v2.5.3-beta.2
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("v2.5.3.1-beta").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(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY));
|
||||||
});
|
});
|
||||||
ui.label(" • 🗜️ Умная Zstd-компрессия: Автоматический пропуск уже сжатых файлов (.jpg, .mp4, .zip, .rar...) и сжатие кода/текста/документов.");
|
ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню.");
|
||||||
ui.label(" • 🔄 Delta-transfer для Auto-Sync: Blake3 хэширование и кэш измененных файлов — синхронизация только изменений без повторной передачи.");
|
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% обнаружения пиров на любых роутерах.");
|
||||||
|
ui.label(" • 🗜️ Умная Zstd-компрессия: Авто-пропуск сжатия для медиа (.jpg/.mp4/.zip...) и потоковое Zstd Level 1 сжатие кода/текста.");
|
||||||
|
ui.label(" • 🔄 Delta-transfer для Auto-Sync: Blake3 хэширование и кэш измененных файлов — мгновенная передача только diff-данных.");
|
||||||
ui.label(" • 👥 Мульти-отправка: Параллельная передача выбранных файлов сразу нескольким друзьям.");
|
ui.label(" • 👥 Мульти-отправка: Параллельная передача выбранных файлов сразу нескольким друзьям.");
|
||||||
ui.label(" • ⏸️ Пауза и возобновление: Кнопки паузы/продолжения передачи прямо в дашборде мониторинга.");
|
ui.label(" • ⏸️ Пауза и возобновление: Интерактивные кнопки паузы/продолжения передачи в реальном времени.");
|
||||||
ui.label(" • 🏎️ Очередь с приоритетами: Маленькие файлы передаются первыми для мгновенного завершения.");
|
ui.label(" • 🏎️ Очередь с приоритетами: Маленькие файлы передаются первыми для мгновенного завершения.");
|
||||||
ui.label(" • 📊 Мониторинг сжатия: Отображение реального коэффициента сжатия и сэкономленного трафика.");
|
|
||||||
ui.label(" • 🏷️ Умный парсер версий: Поддержка 4-значных номеров (2.5.3.1) и автоматическое определение Beta-релизов.");
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -4385,7 +4433,7 @@ impl ZeroSendApp {
|
|||||||
server_clone.accept_transfer(&session_id).await;
|
server_clone.accept_transfer(&session_id).await;
|
||||||
});
|
});
|
||||||
self.pending_request = None;
|
self.pending_request = None;
|
||||||
self.current_tab = Tab::Transfers;
|
self.switch_tab(Tab::Transfers);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reject_btn = Button::new(
|
let reject_btn = Button::new(
|
||||||
@@ -4752,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 {
|
match self.current_tab {
|
||||||
Tab::Nearby => self.render_nearby_tab(ui, theme),
|
Tab::Nearby => self.render_nearby_tab(ui, theme),
|
||||||
Tab::Send => self.render_send_tab(ui, theme),
|
Tab::Send => self.render_send_tab(ui, theme),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ impl Default for VimViewerModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VimViewerModal {
|
impl VimViewerModal {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn open(
|
pub fn open(
|
||||||
&mut self,
|
&mut self,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
|
|||||||
+13
-5
@@ -79,11 +79,19 @@ pub fn parse_version(v: &str) -> (u32, u32, u32, u32, bool, u32) {
|
|||||||
pl.contains("beta") || pl.contains("alpha") || pl.contains("rc")
|
pl.contains("beta") || pl.contains("alpha") || pl.contains("rc")
|
||||||
}).unwrap_or(false);
|
}).unwrap_or(false);
|
||||||
|
|
||||||
// Extract pre-release number, e.g. "beta.1" -> 1, "beta" -> 0
|
// Extract pre-release number, e.g. "beta.1.1" -> 1001, "beta.1" -> 1000, "beta" -> 0
|
||||||
let pre_num = pre_part
|
let pre_num = if let Some(p) = pre_part {
|
||||||
.and_then(|p| p.split('.').last())
|
let parts: Vec<&str> = p.split('.').collect();
|
||||||
.and_then(|s| s.parse::<u32>().ok())
|
let p1 = parts.get(1).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
|
||||||
.unwrap_or(0);
|
let p2 = parts.get(2).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
|
||||||
|
if p1 == 0 && p2 == 0 {
|
||||||
|
parts.last().and_then(|s| s.parse::<u32>().ok()).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
p1 * 1000 + p2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
(major, minor, patch, subpatch, is_beta, pre_num)
|
(major, minor, patch, subpatch, is_beta, pre_num)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user