Release v2.5.5: beautiful network shares UI, breadcrumbs navigation & instant transfers tab redirection on shared downloads
This commit is contained in:
Generated
+1
-1
@@ -6453,7 +6453,7 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerosend"
|
name = "zerosend"
|
||||||
version = "2.5.4"
|
version = "2.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zerosend"
|
name = "zerosend"
|
||||||
version = "2.5.4"
|
version = "2.5.5"
|
||||||
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"]
|
||||||
|
|||||||
+144
-12
@@ -926,37 +926,89 @@ impl TransferClient {
|
|||||||
.map_err(|e| format!("Failed to read file text: {}", e))
|
.map_err(|e| format!("Failed to read file text: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a file or folder (zip) from a remote shared folder
|
/// Download a file or folder (zip) from a remote shared folder with live progress tracking
|
||||||
pub async fn download_shared_file(
|
pub async fn download_shared_file_with_progress<F>(
|
||||||
&self,
|
&self,
|
||||||
peer_url: &str,
|
peer_url: &str,
|
||||||
folder_id: &str,
|
folder_id: &str,
|
||||||
subpath: &str,
|
subpath: &str,
|
||||||
dest_path: &std::path::Path,
|
dest_path: &std::path::Path,
|
||||||
pin: Option<&str>,
|
pin: Option<&str>,
|
||||||
) -> Result<u64, String> {
|
peer_name: &str,
|
||||||
|
mut on_progress: F,
|
||||||
|
) -> Result<u64, String>
|
||||||
|
where
|
||||||
|
F: FnMut(TransferProgress) + Send + 'static,
|
||||||
|
{
|
||||||
let clean = subpath.trim_start_matches('/').trim_start_matches('\\');
|
let clean = subpath.trim_start_matches('/').trim_start_matches('\\');
|
||||||
let mut url = format!("{}/api/shared/download/{}/{}", peer_url, folder_id, clean);
|
let mut url = format!("{}/api/shared/download/{}/{}", peer_url, folder_id, clean);
|
||||||
if let Some(p) = pin.filter(|p| !p.trim().is_empty()) {
|
if let Some(p) = pin.filter(|p| !p.trim().is_empty()) {
|
||||||
url = format!("{}?pin={}", url, p);
|
url = format!("{}?pin={}", url, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fname = dest_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "downloaded".to_string());
|
||||||
|
let session_id = format!("shared-dl-{}", uuid::Uuid::new_v4());
|
||||||
|
|
||||||
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 0,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: 0,
|
||||||
|
total_bytes: 0,
|
||||||
|
speed_bps: 0,
|
||||||
|
state: TransferState::InProgress,
|
||||||
|
checksum_verified: false,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Network error: {}", e))?;
|
.map_err(|e| {
|
||||||
|
let err_msg = format!("Network error: {}", e);
|
||||||
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 0,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: 0,
|
||||||
|
total_bytes: 0,
|
||||||
|
speed_bps: 0,
|
||||||
|
state: TransferState::Failed(err_msg.clone()),
|
||||||
|
checksum_verified: false,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
err_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(format!("Download failed: HTTP {}", resp.status()));
|
let status = resp.status();
|
||||||
|
let err_msg = format!("Download failed: HTTP {}", status);
|
||||||
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 0,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: 0,
|
||||||
|
total_bytes: 0,
|
||||||
|
speed_bps: 0,
|
||||||
|
state: TransferState::Failed(err_msg.clone()),
|
||||||
|
checksum_verified: false,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
return Err(err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = resp
|
let total_size = resp.content_length().unwrap_or(0);
|
||||||
.bytes()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to read stream: {}", e))?;
|
|
||||||
let len = bytes.len() as u64;
|
|
||||||
|
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
let _ = tokio::fs::create_dir_all(parent).await;
|
let _ = tokio::fs::create_dir_all(parent).await;
|
||||||
@@ -966,14 +1018,94 @@ impl TransferClient {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Could not create file: {}", e))?;
|
.map_err(|e| format!("Could not create file: {}", e))?;
|
||||||
|
|
||||||
tokio::io::AsyncWriteExt::write_all(&mut file, &bytes)
|
use futures_util::StreamExt;
|
||||||
|
let mut stream = resp.bytes_stream();
|
||||||
|
let mut bytes_received: u64 = 0;
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
let mut last_emit = std::time::Instant::now();
|
||||||
|
|
||||||
|
while let Some(chunk_res) = stream.next().await {
|
||||||
|
match chunk_res {
|
||||||
|
Ok(chunk) => {
|
||||||
|
let len = chunk.len() as u64;
|
||||||
|
bytes_received += len;
|
||||||
|
tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Could not write file: {}", e))?;
|
.map_err(|e| format!("Could not write file: {}", e))?;
|
||||||
|
|
||||||
|
if last_emit.elapsed() > std::time::Duration::from_millis(250) {
|
||||||
|
last_emit = std::time::Instant::now();
|
||||||
|
let elapsed = start_time.elapsed().as_secs_f64().max(0.001);
|
||||||
|
let speed = (bytes_received as f64 / elapsed) as u64;
|
||||||
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 0,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: bytes_received,
|
||||||
|
total_bytes: if total_size > 0 { total_size } else { bytes_received },
|
||||||
|
speed_bps: speed,
|
||||||
|
state: TransferState::InProgress,
|
||||||
|
checksum_verified: false,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = format!("Stream error: {}", e);
|
||||||
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 0,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: bytes_received,
|
||||||
|
total_bytes: total_size,
|
||||||
|
speed_bps: 0,
|
||||||
|
state: TransferState::Failed(err_msg.clone()),
|
||||||
|
checksum_verified: false,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
return Err(err_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokio::io::AsyncWriteExt::flush(&mut file)
|
tokio::io::AsyncWriteExt::flush(&mut file)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Could not flush file: {}", e))?;
|
.map_err(|e| format!("Could not flush file: {}", e))?;
|
||||||
|
|
||||||
Ok(len)
|
on_progress(TransferProgress {
|
||||||
|
session_id: session_id.clone(),
|
||||||
|
peer_name: peer_name.to_string(),
|
||||||
|
is_incoming: true,
|
||||||
|
current_file_index: 1,
|
||||||
|
total_files: 1,
|
||||||
|
current_file_name: fname.clone(),
|
||||||
|
bytes_transferred: bytes_received,
|
||||||
|
total_bytes: if total_size > 0 { total_size } else { bytes_received },
|
||||||
|
speed_bps: 0,
|
||||||
|
state: TransferState::Completed,
|
||||||
|
checksum_verified: true,
|
||||||
|
compressed_bytes: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(bytes_received)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download a file or folder (zip) from a remote shared folder
|
||||||
|
pub async fn download_shared_file(
|
||||||
|
&self,
|
||||||
|
peer_url: &str,
|
||||||
|
folder_id: &str,
|
||||||
|
subpath: &str,
|
||||||
|
dest_path: &std::path::Path,
|
||||||
|
pin: Option<&str>,
|
||||||
|
) -> Result<u64, String> {
|
||||||
|
self.download_shared_file_with_progress(peer_url, folder_id, subpath, dest_path, pin, "Shared", |_| {}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a file directly to a friend's shared folder (if Read & Write allowed)
|
/// Upload a file directly to a friend's shared folder (if Read & Write allowed)
|
||||||
|
|||||||
+213
-93
@@ -414,7 +414,7 @@ impl ZeroSendApp {
|
|||||||
selected_peer_ids: HashSet::new(),
|
selected_peer_ids: HashSet::new(),
|
||||||
queued_files: initial_files,
|
queued_files: initial_files,
|
||||||
transfers: initial_transfers,
|
transfers: initial_transfers,
|
||||||
logs: vec!["⚡ ZeroSend v2.5.4 ready".to_string()],
|
logs: vec!["⚡ ZeroSend v2.5.5 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(),
|
||||||
@@ -2731,46 +2731,63 @@ impl ZeroSendApp {
|
|||||||
if online_peers_with_shares.is_empty() {
|
if online_peers_with_shares.is_empty() {
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.fill(theme.bg_card)
|
.fill(theme.bg_card)
|
||||||
.rounding(Rounding::same(10.0))
|
.rounding(Rounding::same(12.0))
|
||||||
.stroke(theme.stroke_border)
|
.stroke(theme.stroke_border)
|
||||||
.inner_margin(Margin::same(40.0))
|
.inner_margin(Margin::same(48.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new("🌐").size(40.0));
|
ui.label(RichText::new("🌐").size(48.0));
|
||||||
|
ui.add_space(8.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new("No shared folders discovered on your network.")
|
RichText::new("В сети пока нет обнаруженных общих папок")
|
||||||
.size(16.0)
|
.size(17.0)
|
||||||
.strong()
|
.strong()
|
||||||
.color(theme.text_primary),
|
.color(theme.text_primary),
|
||||||
);
|
);
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(
|
RichText::new(
|
||||||
"When your friends on ZeroTier / LAN add shared folders, they will automatically appear here with streaming and fast download support.",
|
"Когда ваши друзья в ZeroTier, Tailscale, Radmin VPN или LAN откроют доступ к папкам, они мгновенно появятся здесь с поддержкой стриминга и быстрой загрузки.",
|
||||||
)
|
)
|
||||||
.size(12.0)
|
.size(12.5)
|
||||||
.color(theme.text_muted),
|
.color(theme.text_secondary),
|
||||||
);
|
);
|
||||||
|
ui.add_space(14.0);
|
||||||
|
let refresh_btn = Button::new(RichText::new("🔄 Обновить список (Sync / Refresh)").strong().color(Color32::WHITE))
|
||||||
|
.fill(Color32::from_rgb(37, 99, 235))
|
||||||
|
.min_size(Vec2::new(200.0, 32.0));
|
||||||
|
if ui.add(refresh_btn).clicked() {
|
||||||
|
self.sync_all_network_shares();
|
||||||
|
self.refresh_current_share_tree();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_shared_folders_count: usize = online_peers_with_shares.iter().map(|p| p.shared_folders.len()).sum();
|
||||||
|
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
// Left Column: Peer & Folder Selector (250px)
|
// ── Left Column: Modern Friends & Shares Sidebar (270px) ──
|
||||||
ui.allocate_ui(Vec2::new(250.0, ui.available_height()), |ui| {
|
ui.allocate_ui(Vec2::new(270.0, ui.available_height()), |ui| {
|
||||||
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(10.0))
|
.inner_margin(Margin::symmetric(12.0, 10.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.label(
|
ui.horizontal(|ui| {
|
||||||
RichText::new("Friends & Shares")
|
ui.label(RichText::new("🌐 Общие папки").strong().size(13.5).color(theme.text_primary));
|
||||||
.strong()
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
.size(13.0)
|
Frame::none()
|
||||||
.color(theme.text_primary),
|
.fill(theme.bg_card_secondary)
|
||||||
);
|
.rounding(Rounding::same(10.0))
|
||||||
|
.inner_margin(Margin::symmetric(6.0, 2.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label(RichText::new(format!("{}", total_shared_folders_count)).size(10.5).strong().color(Color32::from_rgb(56, 189, 248)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
@@ -2778,47 +2795,74 @@ impl ZeroSendApp {
|
|||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
for peer in &online_peers_with_shares {
|
for peer in &online_peers_with_shares {
|
||||||
let (peer_display, _) = self.get_peer_display_name(peer);
|
let (peer_display, _) = self.get_peer_display_name(peer);
|
||||||
|
|
||||||
|
Frame::none()
|
||||||
|
.fill(theme.bg_card_secondary)
|
||||||
|
.rounding(Rounding::same(8.0))
|
||||||
|
.stroke(theme.stroke_border)
|
||||||
|
.inner_margin(Margin::same(8.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(11.0));
|
ui.label(RichText::new("●").color(Color32::from_rgb(34, 197, 94)).size(10.0));
|
||||||
ui.label(RichText::new(&peer_display).strong().color(theme.text_primary));
|
ui.label(RichText::new(&peer_display).strong().size(12.0).color(theme.text_primary));
|
||||||
|
if let Some(ping) = peer.ping_ms {
|
||||||
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
ui.label(RichText::new(format!("⚡ {}ms", ping)).size(9.5).color(Color32::from_rgb(34, 197, 94)));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
for folder in &peer.shared_folders {
|
for folder in &peer.shared_folders {
|
||||||
let is_selected = self.selected_peer_id_for_share.as_deref() == Some(&peer.peer_id)
|
let is_selected = self.selected_peer_id_for_share.as_deref() == Some(&peer.peer_id)
|
||||||
&& self.selected_folder_id_for_share.as_deref() == Some(&folder.id);
|
&& self.selected_folder_id_for_share.as_deref() == Some(&folder.id);
|
||||||
|
|
||||||
let badge = if folder.read_only { "RO" } else { "RW" };
|
let badge_color = if folder.read_only { Color32::from_rgb(59, 130, 246) } else { Color32::from_rgb(34, 197, 94) };
|
||||||
let btn_text = format!("📁 {} [{}]", folder.name, badge);
|
let badge_text = if folder.read_only { "RO" } else { "RW" };
|
||||||
|
|
||||||
let btn = Button::new(RichText::new(btn_text).size(12.0).color(if is_selected {
|
let btn = Button::new(
|
||||||
theme.text_accent
|
RichText::new(format!("📁 {}", folder.name))
|
||||||
} else {
|
.size(11.5)
|
||||||
theme.text_secondary
|
.strong()
|
||||||
}))
|
.color(if is_selected { theme.text_accent } else { theme.text_primary }),
|
||||||
.fill(if is_selected {
|
)
|
||||||
theme.bg_card_active
|
.fill(if is_selected { theme.bg_card_active } else { Color32::TRANSPARENT })
|
||||||
} else {
|
.stroke(if is_selected { Stroke::new(1.0_f32, theme.text_accent) } else { Stroke::NONE })
|
||||||
Color32::TRANSPARENT
|
|
||||||
})
|
|
||||||
.min_size(Vec2::new(ui.available_width(), 26.0))
|
.min_size(Vec2::new(ui.available_width(), 26.0))
|
||||||
.rounding(Rounding::same(4.0));
|
.rounding(Rounding::same(6.0));
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
if ui.add(btn).clicked() {
|
if ui.add(btn).clicked() {
|
||||||
self.selected_peer_id_for_share = Some(peer.peer_id.clone());
|
self.selected_peer_id_for_share = Some(peer.peer_id.clone());
|
||||||
self.selected_folder_id_for_share = Some(folder.id.clone());
|
self.selected_folder_id_for_share = Some(folder.id.clone());
|
||||||
self.current_share_subpath = String::new();
|
self.current_share_subpath = String::new();
|
||||||
self.refresh_current_share_tree();
|
self.refresh_current_share_tree();
|
||||||
}
|
}
|
||||||
|
// Pill badge
|
||||||
|
Frame::none()
|
||||||
|
.fill(theme.bg_card)
|
||||||
|
.rounding(Rounding::same(4.0))
|
||||||
|
.stroke(Stroke::new(1.0_f32, badge_color))
|
||||||
|
.inner_margin(Margin::symmetric(4.0, 1.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label(RichText::new(badge_text).size(9.0).color(badge_color));
|
||||||
|
});
|
||||||
|
if folder.requires_pin {
|
||||||
|
ui.label(RichText::new("🔒").size(10.0));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(2.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Right Column: Active Share Explorer
|
// ── Right Column: Active Share Modern Explorer ──
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
if let (Some(peer_id), Some(folder_id)) = (
|
if let (Some(peer_id), Some(folder_id)) = (
|
||||||
self.selected_peer_id_for_share.clone(),
|
self.selected_peer_id_for_share.clone(),
|
||||||
@@ -2828,39 +2872,63 @@ impl ZeroSendApp {
|
|||||||
let peer = match peer_opt {
|
let peer = match peer_opt {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
ui.label("Selected peer is offline.");
|
Frame::none()
|
||||||
|
.fill(theme.bg_card)
|
||||||
|
.rounding(Rounding::same(8.0))
|
||||||
|
.stroke(theme.stroke_border)
|
||||||
|
.inner_margin(Margin::same(20.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label(RichText::new("⚠️ Выбранный узел временно не в сети.").color(Color32::from_rgb(239, 68, 68)));
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (peer_name, _) = self.get_peer_display_name(&peer);
|
let (peer_name, _) = self.get_peer_display_name(&peer);
|
||||||
|
|
||||||
// Top Explorer Header & Breadcrumbs
|
// ── Top Explorer Header & Interactive Breadcrumbs Bar ──
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.fill(theme.bg_card)
|
.fill(theme.bg_card)
|
||||||
.rounding(Rounding::same(8.0))
|
.rounding(Rounding::same(8.0))
|
||||||
.stroke(theme.stroke_border)
|
.stroke(theme.stroke_border)
|
||||||
.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| {
|
||||||
if ui.button("🏠 Root").clicked() {
|
if ui.button(RichText::new("🏠 Корень").strong().color(theme.text_accent)).clicked() {
|
||||||
self.current_share_subpath = String::new();
|
self.current_share_subpath = String::new();
|
||||||
self.refresh_current_share_tree();
|
self.refresh_current_share_tree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut navigate_subpath = None;
|
||||||
if !self.current_share_subpath.is_empty() {
|
if !self.current_share_subpath.is_empty() {
|
||||||
ui.label("/");
|
let parts: Vec<String> = self.current_share_subpath.split('/').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect();
|
||||||
ui.label(
|
let mut accumulated = String::new();
|
||||||
RichText::new(&self.current_share_subpath)
|
for (idx, part) in parts.iter().enumerate() {
|
||||||
.monospace()
|
ui.label(RichText::new("/").color(theme.text_muted));
|
||||||
.strong()
|
if !accumulated.is_empty() {
|
||||||
.color(theme.text_primary),
|
accumulated.push('/');
|
||||||
);
|
}
|
||||||
|
accumulated.push_str(part);
|
||||||
|
let is_last = idx == parts.len() - 1;
|
||||||
|
if is_last {
|
||||||
|
ui.label(RichText::new(part).strong().color(theme.text_primary));
|
||||||
|
} else {
|
||||||
|
let acc_clone = accumulated.clone();
|
||||||
|
if ui.link(RichText::new(part).color(theme.text_accent)).clicked() {
|
||||||
|
navigate_subpath = Some(acc_clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sub) = navigate_subpath {
|
||||||
|
self.current_share_subpath = sub;
|
||||||
|
self.refresh_current_share_tree();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
// Grep search button
|
// Grep search button
|
||||||
if ui.button(RichText::new("🔎 Поиск текста/кода").color(Color32::from_rgb(56, 189, 248))).clicked() {
|
if ui.button(RichText::new("🔎 Grep по коду").color(Color32::from_rgb(56, 189, 248))).clicked() {
|
||||||
self.show_grep_modal = true;
|
self.show_grep_modal = true;
|
||||||
self.grep_query.clear();
|
self.grep_query.clear();
|
||||||
self.grep_results.clear();
|
self.grep_results.clear();
|
||||||
@@ -2873,7 +2941,7 @@ impl ZeroSendApp {
|
|||||||
let peer_name_clone = peer_name.clone();
|
let peer_name_clone = peer_name.clone();
|
||||||
let f_name = peer.shared_folders.iter().find(|f| f.id == folder_id_clone).map(|f| f.name.clone()).unwrap_or_else(|| "Shared".to_string());
|
let f_name = peer.shared_folders.iter().find(|f| f.id == folder_id_clone).map(|f| f.name.clone()).unwrap_or_else(|| "Shared".to_string());
|
||||||
|
|
||||||
if ui.button(RichText::new("🔄 Авто-Синхронизация").color(Color32::from_rgb(168, 85, 247))).clicked() {
|
if ui.button(RichText::new("🔄 Синхронизация").color(Color32::from_rgb(168, 85, 247))).clicked() {
|
||||||
self.new_sync_peer_id = peer_id_clone;
|
self.new_sync_peer_id = peer_id_clone;
|
||||||
self.new_sync_folder_id = folder_id_clone;
|
self.new_sync_folder_id = folder_id_clone;
|
||||||
self.new_sync_name = format!("Синхронизация {} ({})", f_name, peer_name_clone);
|
self.new_sync_name = format!("Синхронизация {} ({})", f_name, peer_name_clone);
|
||||||
@@ -2894,7 +2962,7 @@ impl ZeroSendApp {
|
|||||||
if let Some(tree) = &self.current_share_tree {
|
if let Some(tree) = &self.current_share_tree {
|
||||||
if !tree.read_only {
|
if !tree.read_only {
|
||||||
let upload_btn = Button::new(
|
let upload_btn = Button::new(
|
||||||
RichText::new("⬆️ Upload File")
|
RichText::new("⬆️ Загрузить файл")
|
||||||
.strong()
|
.strong()
|
||||||
.color(Color32::WHITE),
|
.color(Color32::WHITE),
|
||||||
)
|
)
|
||||||
@@ -2917,17 +2985,16 @@ impl ZeroSendApp {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for f in files {
|
for f in files {
|
||||||
let _ = log_tx.send(format!("⬆️ Uploading {:?} to share...", f.file_name()));
|
let _ = log_tx.send(format!("⬆️ Загрузка {:?} в общую папку...", f.file_name()));
|
||||||
match client.upload_to_shared_folder(&peer_url, &f_id, &sub, &f, pin.as_deref()).await {
|
match client.upload_to_shared_folder(&peer_url, &f_id, &sub, &f, pin.as_deref()).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = log_tx.send(format!("✅ Uploaded {:?}", f.file_name()));
|
let _ = log_tx.send(format!("✅ Загружено {:?}", f.file_name()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = log_tx.send(format!("❌ Upload failed: {}", e));
|
let _ = log_tx.send(format!("❌ Ошибка загрузки: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Refresh tree after upload
|
|
||||||
if let Ok(tree) = client.fetch_shared_tree(&peer_url, &f_id, Some(&sub), pin.as_deref()).await {
|
if let Ok(tree) = client.fetch_shared_tree(&peer_url, &f_id, Some(&sub), pin.as_deref()).await {
|
||||||
let _ = tx.send(Some(tree));
|
let _ = tx.send(Some(tree));
|
||||||
}
|
}
|
||||||
@@ -2940,9 +3007,9 @@ impl ZeroSendApp {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Multi-select batch action toolbar
|
// ── Multi-select Batch Action Toolbar ──
|
||||||
if !self.selected_share_items.is_empty() {
|
if !self.selected_share_items.is_empty() {
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.fill(if theme.is_dark { Color32::from_rgb(23, 37, 84) } else { Color32::from_rgb(219, 234, 254) })
|
.fill(if theme.is_dark { Color32::from_rgb(23, 37, 84) } else { Color32::from_rgb(219, 234, 254) })
|
||||||
@@ -2951,15 +3018,15 @@ impl ZeroSendApp {
|
|||||||
.inner_margin(Margin::symmetric(12.0, 8.0))
|
.inner_margin(Margin::symmetric(12.0, 8.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new(format!("Selected {} item(s)", self.selected_share_items.len())).strong().color(theme.text_primary));
|
ui.label(RichText::new(format!("Выбрано файлов: {}", self.selected_share_items.len())).strong().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 ui.button("Clear Selection").clicked() {
|
if ui.button("✖ Снять выбор").clicked() {
|
||||||
self.selected_share_items.clear();
|
self.selected_share_items.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
let dl_btn = Button::new(
|
let dl_btn = Button::new(
|
||||||
RichText::new(format!("⬇️ Download Selected ({})", self.selected_share_items.len()))
|
RichText::new(format!("⬇️ Скачать выбранное ({})", self.selected_share_items.len()))
|
||||||
.strong()
|
.strong()
|
||||||
.color(Color32::WHITE),
|
.color(Color32::WHITE),
|
||||||
)
|
)
|
||||||
@@ -2978,7 +3045,12 @@ impl ZeroSendApp {
|
|||||||
Some(self.share_pin_input.trim().to_string())
|
Some(self.share_pin_input.trim().to_string())
|
||||||
};
|
};
|
||||||
let log_tx = self.log_tx.clone();
|
let log_tx = self.log_tx.clone();
|
||||||
|
let transfer_tx = self.transfer_tx.clone();
|
||||||
let sound = self.sound_notifications;
|
let sound = self.sound_notifications;
|
||||||
|
let peer_display_name = peer_name.clone();
|
||||||
|
|
||||||
|
// Switch tab to Transfers!
|
||||||
|
self.switch_tab(Tab::Transfers);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for item_rel in items {
|
for item_rel in items {
|
||||||
@@ -2987,8 +3059,19 @@ impl ZeroSendApp {
|
|||||||
.map(|n| n.to_string_lossy().into_owned())
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| "downloaded".to_string());
|
.unwrap_or_else(|| "downloaded".to_string());
|
||||||
let dest = dl_dir.join(&fname);
|
let dest = dl_dir.join(&fname);
|
||||||
let _ = log_tx.send(format!("⬇️ Downloading {}...", fname));
|
let _ = log_tx.send(format!("⬇️ Загрузка {} от {}...", fname, peer_display_name));
|
||||||
let _ = client.download_shared_file(&peer_url, &f_id, &item_rel, &dest, pin.as_deref()).await;
|
let t_tx = transfer_tx.clone();
|
||||||
|
let _ = client.download_shared_file_with_progress(
|
||||||
|
&peer_url,
|
||||||
|
&f_id,
|
||||||
|
&item_rel,
|
||||||
|
&dest,
|
||||||
|
pin.as_deref(),
|
||||||
|
&peer_display_name,
|
||||||
|
move |prog| {
|
||||||
|
let _ = t_tx.send(prog);
|
||||||
|
},
|
||||||
|
).await;
|
||||||
}
|
}
|
||||||
if sound {
|
if sound {
|
||||||
crate::ui::sound::play_transfer_sound();
|
crate::ui::sound::play_transfer_sound();
|
||||||
@@ -3002,12 +3085,12 @@ impl ZeroSendApp {
|
|||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explorer File Table
|
// ── Explorer File Table & Cards ──
|
||||||
if self.share_tree_loading {
|
if self.share_tree_loading {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(30.0);
|
ui.add_space(30.0);
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
ui.label("Loading shared contents...");
|
ui.label("Загрузка содержимого папки...");
|
||||||
});
|
});
|
||||||
} else if let Some(tree) = self.current_share_tree.clone() {
|
} else if let Some(tree) = self.current_share_tree.clone() {
|
||||||
let is_writable_share = !tree.read_only;
|
let is_writable_share = !tree.read_only;
|
||||||
@@ -3021,8 +3104,8 @@ impl ZeroSendApp {
|
|||||||
.inner_margin(Margin::symmetric(10.0, 6.0))
|
.inner_margin(Margin::symmetric(10.0, 6.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("📁 ..");
|
ui.label("📁 ⮤ ..");
|
||||||
if ui.button("Parent Folder").clicked() {
|
if ui.button(RichText::new("На уровень выше").strong().color(theme.text_accent)).clicked() {
|
||||||
let p = std::path::Path::new(&self.current_share_subpath);
|
let p = std::path::Path::new(&self.current_share_subpath);
|
||||||
let parent_str = p.parent().map(|p| p.to_string_lossy().replace('\\', "/")).unwrap_or_default();
|
let parent_str = p.parent().map(|p| p.to_string_lossy().replace('\\', "/")).unwrap_or_default();
|
||||||
self.current_share_subpath = parent_str;
|
self.current_share_subpath = parent_str;
|
||||||
@@ -3045,7 +3128,7 @@ impl ZeroSendApp {
|
|||||||
.fill(theme.bg_card)
|
.fill(theme.bg_card)
|
||||||
.rounding(Rounding::same(6.0))
|
.rounding(Rounding::same(6.0))
|
||||||
.stroke(theme.stroke_border)
|
.stroke(theme.stroke_border)
|
||||||
.inner_margin(Margin::symmetric(10.0, 6.0))
|
.inner_margin(Margin::symmetric(10.0, 7.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// Checkbox for multi-select
|
// Checkbox for multi-select
|
||||||
@@ -3071,7 +3154,7 @@ impl ZeroSendApp {
|
|||||||
ui.label(RichText::new(&entry.name).strong().color(theme.text_primary));
|
ui.label(RichText::new(&entry.name).strong().color(theme.text_primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Badge
|
// Category Badge
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.fill(theme.bg_card_secondary)
|
.fill(theme.bg_card_secondary)
|
||||||
.rounding(Rounding::same(3.0))
|
.rounding(Rounding::same(3.0))
|
||||||
@@ -3090,10 +3173,14 @@ impl ZeroSendApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
// Action: Download
|
// ── Action: Download ──
|
||||||
let is_dir = entry.is_dir;
|
let is_dir = entry.is_dir;
|
||||||
let down_label = if is_dir { "⬇️ Zip" } else { "⬇️ Download" };
|
let down_label = if is_dir { "⬇️ Скачать папку (Zip)" } else { "⬇️ Скачать" };
|
||||||
if ui.button(down_label).clicked() {
|
let dl_btn = Button::new(RichText::new(down_label).strong().color(Color32::WHITE))
|
||||||
|
.fill(Color32::from_rgb(37, 99, 235))
|
||||||
|
.rounding(Rounding::same(4.0));
|
||||||
|
|
||||||
|
if ui.add(dl_btn).clicked() {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let peer_url = peer.transfer_url();
|
let peer_url = peer.transfer_url();
|
||||||
let f_id = folder_id.clone();
|
let f_id = folder_id.clone();
|
||||||
@@ -3106,25 +3193,43 @@ impl ZeroSendApp {
|
|||||||
Some(self.share_pin_input.trim().to_string())
|
Some(self.share_pin_input.trim().to_string())
|
||||||
};
|
};
|
||||||
let log_tx = self.log_tx.clone();
|
let log_tx = self.log_tx.clone();
|
||||||
|
let transfer_tx = self.transfer_tx.clone();
|
||||||
let sound = self.sound_notifications;
|
let sound = self.sound_notifications;
|
||||||
|
let peer_display_name = peer_name.clone();
|
||||||
|
|
||||||
|
// Switch tab to Transfers immediately!
|
||||||
|
self.switch_tab(Tab::Transfers);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = log_tx.send(format!("⬇️ Downloading {}...", out_name));
|
let _ = log_tx.send(format!("⬇️ Загрузка {} от {}...", out_name, peer_display_name));
|
||||||
match client.download_shared_file(&peer_url, &f_id, &sub, &dest, pin.as_deref()).await {
|
let t_tx = transfer_tx.clone();
|
||||||
|
let res = client.download_shared_file_with_progress(
|
||||||
|
&peer_url,
|
||||||
|
&f_id,
|
||||||
|
&sub,
|
||||||
|
&dest,
|
||||||
|
pin.as_deref(),
|
||||||
|
&peer_display_name,
|
||||||
|
move |prog| {
|
||||||
|
let _ = t_tx.send(prog);
|
||||||
|
},
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
let _ = log_tx.send(format!("✅ Downloaded {} ({})", out_name, human_bytes(bytes as f64)));
|
let _ = log_tx.send(format!("✅ Загружено {} ({})", out_name, human_bytes(bytes as f64)));
|
||||||
if sound {
|
if sound {
|
||||||
crate::ui::sound::play_transfer_sound();
|
crate::ui::sound::play_transfer_sound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = log_tx.send(format!("❌ Download failed: {}", e));
|
let _ = log_tx.send(format!("❌ Ошибка загрузки {}: {}", out_name, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action: Audio In-App Player
|
// ── Action: Audio In-App Player ──
|
||||||
if badge_text == "AUDIO" {
|
if badge_text == "AUDIO" {
|
||||||
let stream_url = format!(
|
let stream_url = format!(
|
||||||
"{}/api/shared/stream/{}/{}",
|
"{}/api/shared/stream/{}/{}",
|
||||||
@@ -3133,7 +3238,7 @@ impl ZeroSendApp {
|
|||||||
entry.relative_path.trim_start_matches('/')
|
entry.relative_path.trim_start_matches('/')
|
||||||
);
|
);
|
||||||
|
|
||||||
if ui.button(RichText::new("🎵 Play in App").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() {
|
if ui.button(RichText::new("🎵 Играть").color(Color32::from_rgb(236, 72, 153)).strong()).clicked() {
|
||||||
self.active_audio = Some(AudioTrackInfo {
|
self.active_audio = Some(AudioTrackInfo {
|
||||||
name: entry.name.clone(),
|
name: entry.name.clone(),
|
||||||
url: stream_url.clone(),
|
url: stream_url.clone(),
|
||||||
@@ -3144,7 +3249,7 @@ impl ZeroSendApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action: Video Watch Party & Stream
|
// ── Action: Video Watch Party & Stream ──
|
||||||
if badge_text == "VIDEO" {
|
if badge_text == "VIDEO" {
|
||||||
let stream_url = format!(
|
let stream_url = format!(
|
||||||
"{}/api/shared/stream/{}/{}",
|
"{}/api/shared/stream/{}/{}",
|
||||||
@@ -3165,18 +3270,18 @@ impl ZeroSendApp {
|
|||||||
timestamp_ms: chrono::Local::now().timestamp_millis() as u64,
|
timestamp_ms: chrono::Local::now().timestamp_millis() as u64,
|
||||||
});
|
});
|
||||||
self.is_in_watch_party = true;
|
self.is_in_watch_party = true;
|
||||||
self.logs.push(format!("🎬 Watch Party created for '{}'", entry.name));
|
self.logs.push(format!("🎬 Watch Party создана для '{}'", entry.name));
|
||||||
let _ = std::process::Command::new("explorer").arg(&stream_url).spawn();
|
let _ = std::process::Command::new("explorer").arg(&stream_url).spawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button(RichText::new("▶️ Stream").color(Color32::from_rgb(56, 189, 248))).clicked() {
|
if ui.button(RichText::new("▶️ Стрим").color(Color32::from_rgb(56, 189, 248))).clicked() {
|
||||||
let _ = std::process::Command::new("explorer").arg(&stream_url).spawn();
|
let _ = std::process::Command::new("explorer").arg(&stream_url).spawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action: Vim View / Edit (for code & text)
|
// ── Action: Vim View / Edit (for code & text) ──
|
||||||
if badge_text == "CODE" || badge_text == "DOC" || entry.name.ends_with(".txt") || entry.name.ends_with(".log") || entry.name.ends_with(".md") {
|
if badge_text == "CODE" || badge_text == "DOC" || entry.name.ends_with(".txt") || entry.name.ends_with(".log") || entry.name.ends_with(".md") {
|
||||||
let edit_label = if is_writable_share { "✏️ Vim Edit" } else { "👁 Vim View" };
|
let edit_label = if is_writable_share { "✏️ Vim Редактор" } else { "👁 Vim Просмотр" };
|
||||||
let btn_color = if is_writable_share { Color32::from_rgb(34, 197, 94) } else { Color32::from_rgb(20, 184, 166) };
|
let btn_color = if is_writable_share { Color32::from_rgb(34, 197, 94) } else { Color32::from_rgb(20, 184, 166) };
|
||||||
|
|
||||||
if ui.button(RichText::new(edit_label).color(btn_color).strong()).clicked() {
|
if ui.button(RichText::new(edit_label).color(btn_color).strong()).clicked() {
|
||||||
@@ -3212,15 +3317,17 @@ impl ZeroSendApp {
|
|||||||
.fill(theme.bg_card)
|
.fill(theme.bg_card)
|
||||||
.rounding(Rounding::same(8.0))
|
.rounding(Rounding::same(8.0))
|
||||||
.stroke(theme.stroke_border)
|
.stroke(theme.stroke_border)
|
||||||
.inner_margin(Margin::same(20.0))
|
.inner_margin(Margin::same(24.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new("🔒 Folder Locked or Empty").strong().size(14.0).color(theme.text_primary));
|
ui.label(RichText::new("🔒 Папка защищена PIN-кодом или пуста").strong().size(14.5).color(theme.text_primary));
|
||||||
ui.add_space(6.0);
|
ui.add_space(8.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Enter PIN if required:");
|
ui.label("Введите PIN-код:");
|
||||||
ui.add(egui::TextEdit::singleline(&mut self.share_pin_input).password(true).desired_width(120.0));
|
ui.add(egui::TextEdit::singleline(&mut self.share_pin_input).password(true).desired_width(120.0));
|
||||||
if ui.button("Unlock / Refresh").clicked() {
|
let unlock_btn = Button::new(RichText::new("🔓 Разблокировать / Обновить").strong().color(Color32::WHITE))
|
||||||
|
.fill(Color32::from_rgb(37, 99, 235));
|
||||||
|
if ui.add(unlock_btn).clicked() {
|
||||||
self.refresh_current_share_tree();
|
self.refresh_current_share_tree();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3230,12 +3337,16 @@ impl ZeroSendApp {
|
|||||||
} else {
|
} else {
|
||||||
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(40.0))
|
.inner_margin(Margin::same(48.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new("👈 Select a shared folder on the left to browse files, watch streams, and view code in Vim.").color(theme.text_muted));
|
ui.label(RichText::new("📂").size(40.0));
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label(RichText::new("Выберите общую папку друга на панели слева").strong().size(15.0).color(theme.text_primary));
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.label(RichText::new("Вы сможете просматривать структуру файлов, стримить музыку и видео, открывать код во встроенном редакторе Vim и скачивать нужные файлы.").size(12.0).color(theme.text_muted));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4106,17 +4217,26 @@ 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.5
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
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(" • 🚀 Мгновенный переход в «Передачи и файлы»: При скачивании любых файлов или архивов из общих папок друзей приложение сразу переключается во вкладку передач с отображением живой скорости (MB/s) и прогресса.");
|
||||||
|
ui.label(" • 🎨 Премиальный редизайн Сетевых общих папок: Удобная боковая панель с пингом и статусами друзей, интерактивные хлебные крошки (breadcrumbs), красивые карточки файлов по категориям и мульти-загрузка.");
|
||||||
|
ui.label(" • 🔄 Все ключевые улучшения v2.5.4: авто-обнаружение папок в реальном времени, отслеживание скачиваний из общих папок, прозрачный стриминг папок и сохранение истории передач на диск.");
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// v2.5.4
|
// v2.5.4
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("v2.5.4").strong().color(Color32::from_rgb(34, 197, 94)));
|
ui.label(RichText::new("v2.5.4").strong().color(Color32::from_rgb(56, 189, 248)));
|
||||||
ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY));
|
|
||||||
});
|
});
|
||||||
ui.label(" • 🔄 Мгновенное авто-обнаружение общих папок: Динамическое обновление списка папок друзей при добавлении/удалении в реальном времени и по кнопке Sync/Refresh.");
|
ui.label(" • 🔄 Мгновенное авто-обнаружение общих папок: Динамическое обновление списка папок друзей при добавлении/удалении в реальном времени и по кнопке Sync/Refresh.");
|
||||||
ui.label(" • 📥 Отслеживание скачиваний из общих папок: Когда друзья качают файлы или архивы из вашей общей папки, передача в реальном времени отображается в списке передач со скоростью.");
|
ui.label(" • 📥 Отслеживание скачиваний из общих папок: Когда друзья качают файлы или архивы из вашей общей папки, передача в реальном времени отображается в списке передач со скоростью.");
|
||||||
ui.label(" • 📁 Прозрачный стриминг папок: Получатель видит настоящее имя папки, примерный размер, прогресс и скорость в MB/s с момента старта стрима.");
|
ui.label(" • 📁 Прозрачный стриминг папок: Получатель видит настоящее имя папки, примерный размер, прогресс и скорость в MB/s с момента старта стрима.");
|
||||||
ui.label(" • 💾 Постоянное сохранение истории передач: История завершенных и прошлых передач теперь надежно сохраняется на диск и не стирается при перезапуске приложения.");
|
ui.label(" • 💾 Постоянное сохранение истории передач: История завершенных и прошлых передач теперь надежно сохраняется на диск и не стирается при перезапуске приложения.");
|
||||||
ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню.");
|
|
||||||
ui.label(" • ⚡ TCP Socket Tuning (4MB буферы, TCP_NODELAY), Zero-Drop Auto-Recovery (до 8 попыток), mDNS/SSDP и Blake3 Delta-Sync.");
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|||||||
Reference in New Issue
Block a user