1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::path::PathBuf;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex};
6use tauri::{Emitter, State, WebviewWindow};
7use toole_core::sender::start_sender;
8use toole_core::{Peer, ToolError, UI};
9
10pub struct AppUI {
15 pub peers: Arc<Mutex<Vec<Peer>>>,
16 pub window: WebviewWindow,
17}
18
19impl UI for AppUI {
20 fn log(&self, msg: &str) {
21 let _ = self.window.emit("tool://log", msg);
22 }
23
24 fn peer_found(&self, peer: &Peer) {
25 let mut peers = self.peers.lock().unwrap_or_else(|e| e.into_inner());
26 if !peers.iter().any(|p| p.id == peer.id) {
27 peers.push(peer.clone());
28 let _ = self.window.emit("tool://peer_found", peer);
29 }
30 }
31
32 fn peer_lost(&self, id: &str) {
33 let mut peers = self.peers.lock().unwrap_or_else(|e| e.into_inner());
34 peers.retain(|p| p.id != id);
35 let _ = self.window.emit("tool://peer_lost", id);
36 }
37
38 fn show_progress_bar(&self, transfer_id: &str) {
39 let _ = self.window.emit("tool://transfer/start", transfer_id);
40 }
41
42 fn update_progress_bar(&self, transfer_id: &str, bytes_sent: u64, total_bytes: u64) {
43 let percent = if total_bytes > 0 {
44 (bytes_sent as f64 / total_bytes as f64 * 100.0).min(100.0) as u8
45 } else {
46 0
47 };
48 let payload = serde_json::json!({
49 "transfer_id": transfer_id,
50 "bytes_sent": bytes_sent,
51 "total_bytes": total_bytes,
52 "percent": percent
53 });
54 let _ = self.window.emit("tool://transfer/progress", payload);
55 }
56
57 fn file_progress_bar(
58 &self,
59 transfer_id: &str,
60 file_name: &str,
61 file_bytes_sent: u64,
62 file_total_bytes: u64,
63 ) {
64 let percent = if file_total_bytes > 0 {
65 (file_bytes_sent as f64 / file_total_bytes as f64 * 100.0).min(100.0) as u8
66 } else {
67 0
68 };
69 let payload = serde_json::json!({
70 "transfer_id": transfer_id,
71 "file_name": file_name,
72 "file_bytes_sent": file_bytes_sent,
73 "file_total_bytes": file_total_bytes,
74 "percent": percent
75 });
76 let _ = self.window.emit("tool://transfer/file_progress", payload);
77 }
78
79 fn transfert_incoming(
80 &self,
81 transfer_id: &str,
82 sender: &str,
83 total_bytes: u64,
84 files: Vec<String>,
85 ) {
86 let payload = serde_json::json!({
87 "transfer_id": transfer_id,
88 "sender": sender,
89 "total_bytes": total_bytes,
90 "files": files
91 });
92 let _ = self.window.emit("tool://transfer/incoming", payload);
93 }
94
95 fn transfert_refused(&self, transfer_id: &str) {
96 let _ = self.window.emit("tool://transfer/refused", transfer_id);
97 }
98
99 fn transfert_cancel(&self, transfer_id: &str) {
100 let _ = self.window.emit("tool://transfer/cancel", transfer_id);
101 }
102
103 fn transfert_completed(&self, transfer_id: &str) {
104 let _ = self.window.emit("tool://transfer/done", transfer_id);
105 }
106
107 fn transfert_received(&self, transfer_id: &str, peer: &str, bytes: u64, files: Vec<String>) {
108 let payload = serde_json::json!({
109 "transfer_id": transfer_id,
110 "peer": peer,
111 "bytes": bytes,
112 "files": files
113 });
114 let _ = self.window.emit("tool://transfer/received", payload);
115 }
116
117 fn transfert_error(&self, transfer_id: &str, error: &ToolError) {
118 let payload = serde_json::json!({
119 "transfer_id": transfer_id,
120 "error": error.to_string()
121 });
122 let _ = self.window.emit("tool://transfer/error", payload);
123 }
124}
125
126pub struct DiscoveryState {
131 pub stop_flag: Mutex<Arc<AtomicBool>>,
132 pub handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
136 pub peers: Arc<Mutex<Vec<Peer>>>,
137}
138
139type ActiveTransfers =
143 Arc<Mutex<HashMap<String, (Arc<AtomicBool>, Option<tokio::task::AbortHandle>)>>>;
144
145pub struct TransferState {
146 pub active: ActiveTransfers,
147 pub decisions: Arc<toole_core::transfer::DecisionBoard>,
150}
151
152pub struct TransferRegistryHandle {
155 pub active: ActiveTransfers,
156}
157
158impl toole_core::TransferRegistry for TransferRegistryHandle {
159 fn register(&self, transfer_id: &str, stop: Arc<AtomicBool>) {
160 self.active
161 .lock()
162 .unwrap_or_else(|e| e.into_inner())
163 .insert(transfer_id.to_string(), (stop, None));
164 }
165
166 fn unregister(&self, transfer_id: &str) {
167 self.active.lock().unwrap_or_else(|e| e.into_inner()).remove(transfer_id);
168 }
169}
170
171#[tauri::command]
176pub async fn start_discovery(
177 state: State<'_, DiscoveryState>,
178 window: WebviewWindow,
179) -> Result<(), String> {
180 state.stop_flag.lock().unwrap_or_else(|e| e.into_inner()).store(true, Ordering::Relaxed);
184 let old = state.handle.lock().unwrap_or_else(|e| e.into_inner()).take();
185 if let Some(old) = old {
186 let _ = old.await;
187 }
188
189 state.peers.lock().unwrap_or_else(|e| e.into_inner()).retain(|p| p.id.starts_with("manual-"));
192
193 let stop = Arc::new(AtomicBool::new(false));
194 *state.stop_flag.lock().unwrap_or_else(|e| e.into_inner()) = stop.clone();
195
196 let local_ip = toole_core::utils::local_ip();
197 let peers = state.peers.clone();
198 let ui: Arc<dyn UI> = Arc::new(AppUI {
199 peers,
200 window: window.clone(),
201 });
202
203 let handle = tokio::spawn(async move {
204 if let Err(e) = toole_core::discovery::start_discovery(local_ip, stop, ui).await {
205 eprintln!("Discovery error: {e}");
206 let _ = window.emit("tool://discovery/error", e.to_string());
207 }
208 });
209 *state.handle.lock().unwrap_or_else(|e| e.into_inner()) = Some(handle);
210
211 Ok(())
212}
213
214#[tauri::command]
215pub async fn stop_discovery(state: State<'_, DiscoveryState>) -> Result<(), String> {
216 state.stop_flag.lock().unwrap_or_else(|e| e.into_inner()).store(true, Ordering::Relaxed);
217 let h = state.handle.lock().unwrap_or_else(|e| e.into_inner()).take();
220 if let Some(h) = h {
221 let _ = h.await;
222 }
223 Ok(())
224}
225
226#[tauri::command]
230pub async fn add_peer(
231 ip: String,
232 state: State<'_, DiscoveryState>,
233 window: WebviewWindow,
234) -> Result<(), String> {
235 let peer = toole_core::utils::manual_peer(&ip)
236 .ok_or_else(|| format!("Adresse invalide : {ip} (IPv4 privé attendu)"))?;
237 let mut peers = state.peers.lock().unwrap_or_else(|e| e.into_inner());
238 if peers.iter().any(|p| p.id == peer.id) {
239 return Ok(()); }
241 peers.push(peer.clone());
242 let _ = window.emit("tool://peer_found", &peer);
243 Ok(())
244}
245
246#[tauri::command]
250pub async fn remove_peer(id: String, state: State<'_, DiscoveryState>) -> Result<(), String> {
251 let mut peers = state.peers.lock().unwrap_or_else(|e| e.into_inner());
252 if !id.starts_with("manual-") {
253 return Ok(()); }
255 peers.retain(|p| p.id != id);
256 Ok(())
257}
258
259#[tauri::command]
263pub fn check_firewall() -> Result<serde_json::Value, String> {
264 use toole_core::firewall::*;
265
266 #[cfg(target_os = "linux")]
267 {
268 let mut ufw_active = false;
269 let mut ufw_open = false;
270 if let Ok(out) = std::process::Command::new("ufw").arg("status").output() {
271 if let Ok(s) = String::from_utf8(out.stdout) {
272 let (a, o) = ufw_ports_open(&s);
273 ufw_active = a;
274 ufw_open = o;
275 }
276 }
277 let mut fw_active = false;
278 let mut fw_open = false;
279 if let Ok(out) = std::process::Command::new("firewall-cmd").arg("--list-ports").output() {
280 if let Ok(s) = String::from_utf8(out.stdout) {
281 fw_active = out.status.success();
282 fw_open = firewalld_ports_open(&s);
283 }
284 }
285 let status = linux_status(ufw_active, ufw_open, fw_active, fw_open);
286 return Ok(serde_json::to_value(status).map_err(|e| e.to_string())?);
287 }
288
289 #[cfg(target_os = "windows")]
290 {
291 let mut ports_open = false;
295 if let Ok(out) = std::process::Command::new("netsh")
296 .args(["advfirewall", "firewall", "show", "rule", "name=Toolé UDP"])
297 .output()
298 {
299 ports_open = out.status.success()
300 && String::from_utf8_lossy(&out.stdout).contains("Toolé UDP");
301 }
302 let status = FirewallStatus {
303 os: "windows".to_string(),
304 active: true,
305 ports_open,
306 commands: commands_for("windows"),
307 };
308 return Ok(serde_json::to_value(status).map_err(|e| e.to_string())?);
309 }
310
311 #[cfg(target_os = "macos")]
312 {
313 let status = FirewallStatus {
316 os: "macos".to_string(),
317 active: false,
318 ports_open: true,
319 commands: vec![],
320 };
321 return Ok(serde_json::to_value(status).map_err(|e| e.to_string())?);
322 }
323}
324
325#[tauri::command]
330pub async fn send_files(
331 paths: Vec<String>,
332 peer_addr: String,
333 peer_id: String,
334 state: State<'_, TransferState>,
335 window: WebviewWindow,
336) -> Result<String, String> {
337 let transfer_id = uuid::Uuid::new_v4().to_string();
338 let stop = Arc::new(AtomicBool::new(false));
339
340 let path_bufs: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
341 let addr: SocketAddr = peer_addr
342 .parse()
343 .map_err(|e| format!("Adresse invalide: {e}"))?;
344
345 let peers = Arc::new(Mutex::new(Vec::new()));
346 let ui: Arc<dyn UI> = Arc::new(AppUI { peers, window });
347 let transfer_id_clone = transfer_id.clone();
348 let stop_clone = stop.clone();
349 let active = state.active.clone();
350
351 let handle = tokio::spawn(async move {
352 let tid = transfer_id_clone.clone();
353 if let Err(e) = start_sender(ui, transfer_id_clone, path_bufs, addr, peer_id, stop_clone)
354 .await
355 {
356 eprintln!("Sender error: {e}");
357 }
358 active.lock().unwrap_or_else(|e| e.into_inner()).remove(&tid);
361 });
362
363 state
364 .active
365 .lock()
366 .unwrap_or_else(|e| e.into_inner())
367 .insert(transfer_id.clone(), (stop, Some(handle.abort_handle())));
368
369 Ok(transfer_id)
370}
371
372#[tauri::command]
373pub async fn cancel_transfer(
374 transfer_id: String,
375 state: State<'_, TransferState>,
376 window: WebviewWindow,
377) -> Result<(), String> {
378 let mut active = state.active.lock().unwrap_or_else(|e| e.into_inner());
379 if let Some((stop, handle)) = active.remove(&transfer_id) {
380 stop.store(true, Ordering::Relaxed);
381 if let Some(h) = handle {
386 if !h.is_finished() {
387 h.abort();
388 let _ = window.emit("tool://transfer/cancel", &transfer_id);
389 }
390 }
391 }
392 Ok(())
393}
394
395#[tauri::command]
396pub async fn respond_transfer(
397 transfer_id: String,
398 accepted: bool,
399 state: State<'_, TransferState>,
400) -> Result<(), String> {
401 if state.decisions.resolve(&transfer_id, accepted) {
402 Ok(())
403 } else {
404 Err(format!("transfert {transfer_id} inconnu ou deja traite"))
405 }
406}
407
408#[tauri::command]
413pub fn get_device_id() -> String {
414 toole_core::utils::device_id()
415}
416
417#[tauri::command]
418pub fn get_peers(state: State<'_, DiscoveryState>) -> Result<Vec<Peer>, String> {
419 let peers = state.peers.lock().unwrap_or_else(|e| e.into_inner());
420 Ok(peers.clone())
421}
422
423#[tauri::command]
424pub fn read_clipboard() -> Result<String, String> {
425 let mut cb = arboard::Clipboard::new().map_err(|e| format!("Clipboard error: {e}"))?;
426 cb.get_text()
427 .map_err(|e| format!("Clipboard read error: {e}"))
428}
429
430#[tauri::command]
431pub fn get_file_infos(paths: Vec<String>) -> Result<Vec<FileInfo>, String> {
432 paths
433 .iter()
434 .map(|p| {
435 std::fs::metadata(p)
436 .map(|m| FileInfo {
437 size: if m.is_file() { m.len() } else { 0 },
438 is_dir: m.is_dir(),
439 })
440 .map_err(|e| format!("Erreur {p}: {e}"))
441 })
442 .collect()
443}
444
445#[derive(serde::Serialize)]
446pub struct FileInfo {
447 pub size: u64,
448 pub is_dir: bool,
449}