From e3fcc6cce3390d04c2f853edb1664a4dbd1c58ed Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:15:05 +0800 Subject: [PATCH] fix: file transfer, auto start on reconnect (#13329) Signed-off-by: fufesou --- .../lib/mobile/pages/file_manager_page.dart | 1 + flutter/lib/models/file_model.dart | 68 ++++++++++++++----- src/flutter.rs | 5 +- src/ui/file_transfer.tis | 13 ++-- src/ui/remote.rs | 15 +++- src/ui_session_interface.rs | 47 +++++++++++-- 6 files changed, 119 insertions(+), 30 deletions(-) diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 828632beb..c63a9c606 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -92,6 +92,7 @@ class _FileManagerPageState extends State { gFFI.dialogManager.dismissAll(); WakelockPlus.disable(); }); + model.jobController.clear(); super.dispose(); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index db9b13e45..d2ae7cff2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1033,30 +1033,54 @@ class JobController { await bind.sessionCancelJob(sessionId: sessionId, actId: id); } - void loadLastJob(Map evt) { + Future loadLastJob(Map evt) async { debugPrint("load last job: $evt"); Map jobDetail = json.decode(evt['value']); - // int id = int.parse(jobDetail['id']); String remote = jobDetail['remote']; String to = jobDetail['to']; bool showHidden = jobDetail['show_hidden']; int fileNum = jobDetail['file_num']; bool isRemote = jobDetail['is_remote']; - final currJobId = JobController.jobID.next(); - String fileName = path.basename(isRemote ? remote : to); - var jobProgress = JobProgress() - ..type = JobType.transfer - ..fileName = fileName - ..jobName = isRemote ? remote : to - ..id = currJobId - ..isRemoteToLocal = isRemote - ..fileNum = fileNum - ..remote = remote - ..to = to - ..showHidden = showHidden - ..state = JobState.paused; - jobTable.add(jobProgress); - bind.sessionAddJob( + bool isAutoStart = jobDetail['auto_start'] == true; + int currJobId = -1; + if (isAutoStart) { + // Ensure jobDetail['id'] exists and is an int + if (jobDetail.containsKey('id') && + jobDetail['id'] != null && + jobDetail['id'] is int) { + currJobId = jobDetail['id']; + } + } + if (currJobId < 0) { + // If id is missing or invalid, disable auto-start and assign a new job id + isAutoStart = false; + currJobId = JobController.jobID.next(); + } + + if (!isAutoStart) { + if (!(isDesktop || isWebDesktop)) { + // Don't add to job table if not auto start on mobile. + // Because mobile does not support job list view now. + return; + } + + // Add to job table if not auto start on desktop. + String fileName = path.basename(isRemote ? remote : to); + final jobProgress = JobProgress() + ..type = JobType.transfer + ..fileName = fileName + ..jobName = isRemote ? remote : to + ..id = currJobId + ..isRemoteToLocal = isRemote + ..fileNum = fileNum + ..remote = remote + ..to = to + ..showHidden = showHidden + ..state = JobState.paused; + jobTable.add(jobProgress); + } + + await bind.sessionAddJob( sessionId: sessionId, isRemote: isRemote, includeHidden: showHidden, @@ -1065,6 +1089,11 @@ class JobController { to: isRemote ? to : remote, fileNum: fileNum, ); + + if (isAutoStart) { + await bind.sessionResumeJob( + sessionId: sessionId, actId: currJobId, isRemote: isRemote); + } } void resumeJob(int jobId) { @@ -1095,6 +1124,11 @@ class JobController { } debugPrint("update folder files: $info"); } + + void clear() { + jobTable.clear(); + jobResultListener.clear(); + } } class JobResultListener { diff --git a/src/flutter.rs b/src/flutter.rs index 57e09e620..31793ecb2 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -23,7 +23,7 @@ use std::{ os::raw::{c_char, c_int, c_void}, str::FromStr, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, RwLock, }, }; @@ -756,7 +756,7 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn clear_all_jobs(&self) {} - fn load_last_job(&self, _cnt: i32, job_json: &str) { + fn load_last_job(&self, _cnt: i32, job_json: &str, _auto_start: bool) { self.push_event("load_last_job", &[("value", job_json)], &[]); } @@ -1328,6 +1328,7 @@ pub fn session_add( server_keyboard_enabled: Arc::new(RwLock::new(true)), server_file_transfer_enabled: Arc::new(RwLock::new(true)), server_clipboard_enabled: Arc::new(RwLock::new(true)), + reconnect_count: Arc::new(AtomicUsize::new(0)), ..Default::default() }; diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 0b60cf748..1090c018d 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -137,7 +137,7 @@ class JobTable: Reactor.Component { self.timer(30ms, function() { self.update(); }); } - function addJob(id, path, to, file_num, show_hidden, is_remote) { + function addJob(id, path, to, file_num, show_hidden, is_remote, auto_start) { var job = { type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, @@ -146,6 +146,10 @@ class JobTable: Reactor.Component { this.job_map[id] = this.jobs[this.jobs.length - 1]; handler.update_next_job_id(id + 1); handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); + if (auto_start) { + this.continueJob(id); + this.update(); + } stdout.println(JSON.stringify(job)); } @@ -279,7 +283,8 @@ class JobTable: Reactor.Component { if (!err) { handler.remove_dir(job.id, job.path, job.is_remote); refreshDir(job.is_remote); - if (is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + // Use the job's is_remote; local variable `is_remote` is undefined in this scope. + if (job.is_remote) file_transfer.remote_folder_view.table.resetCurrent(); else file_transfer.local_folder_view.table.resetCurrent(); } } else if (!job.no_confirm) { @@ -697,9 +702,9 @@ handler.clearAllJobs = function() { file_transfer.job_table.clearAllJobs(); } -handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote, auto_start) { // load last job // stdout.println("restore job: " + is_remote); - file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote); + file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote,auto_start); } handler.updateTransferList = function () { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f67f37902..e04d8e81b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{Arc, Mutex, RwLock}, + sync::{atomic::AtomicUsize, Arc, Mutex, RwLock}, }; use sciter::{ @@ -199,7 +199,7 @@ impl InvokeUiSession for SciterHandler { self.call("clearAllJobs", &make_args!()); } - fn load_last_job(&self, cnt: i32, job_json: &str) { + fn load_last_job(&self, cnt: i32, job_json: &str, auto_start: bool) { let job: Result = serde_json::from_str(job_json); if let Ok(job) = job { let path; @@ -213,7 +213,15 @@ impl InvokeUiSession for SciterHandler { } self.call( "addJob", - &make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote), + &make_args!( + cnt, + path, + to, + job.file_num, + job.show_hidden, + job.is_remote, + auto_start + ), ); } } @@ -570,6 +578,7 @@ impl SciterSession { server_keyboard_enabled: Arc::new(RwLock::new(true)), server_file_transfer_enabled: Arc::new(RwLock::new(true)), server_clipboard_enabled: Arc::new(RwLock::new(true)), + reconnect_count: Arc::new(AtomicUsize::new(0)), ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e41d873cc..93c041348 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -29,7 +29,10 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, str::FromStr, - sync::{Arc, Mutex, RwLock}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, RwLock, + }, time::SystemTime, }; use uuid::Uuid; @@ -61,6 +64,9 @@ pub struct Session { pub last_change_display: Arc>, pub connection_round_state: Arc>, pub printer_names: Arc>>, + // Indicate whether the session is reconnected. + // Used to auto start file transfer after reconnection. + pub reconnect_count: Arc, } #[derive(Clone)] @@ -1272,6 +1278,7 @@ impl Session { self.lc.write().unwrap().force_relay = true; } self.lc.write().unwrap().peer_info = None; + self.reconnect_count.fetch_add(1, Ordering::SeqCst); let mut lock = self.thread.lock().unwrap(); // No need to join the previous thread, because it will exit automatically. // And the previous thread will not change important states. @@ -1372,6 +1379,24 @@ impl Session { self.send(Data::Close); } + fn try_auto_start_job_str(is_reconnected: bool, job_str: &str) -> Option { + if is_reconnected { + let job_str = job_str.trim(); + if let Some(stripped) = job_str.strip_suffix('}') { + format!(r#"{},"auto_start": true}}"#, stripped).into() + } else { + // unreachable in normal cases + log::warn!( + "The last character is not '}}': {}, auto start is ignored on flutter", + job_str + ); + Some(job_str.to_owned()) + } + } else { + None + } + } + pub fn load_last_jobs(&self) { self.clear_all_jobs(); let pc = self.load_config(); @@ -1379,18 +1404,32 @@ impl Session { // no last jobs return; } + let reconnect_count_thr = if cfg!(feature = "flutter") { 0 } else { 1 }; + let is_reconnected = self.reconnect_count.load(Ordering::SeqCst) > reconnect_count_thr; // TODO: can add a confirm dialog let mut cnt = 1; for job_str in pc.transfer.read_jobs.iter() { if !job_str.is_empty() { - self.load_last_job(cnt, job_str); + self.load_last_job( + cnt, + Self::try_auto_start_job_str(is_reconnected, job_str) + .as_deref() + .unwrap_or(job_str), + is_reconnected, + ); cnt += 1; log::info!("restore read_job: {:?}", job_str); } } for job_str in pc.transfer.write_jobs.iter() { if !job_str.is_empty() { - self.load_last_job(cnt, job_str); + self.load_last_job( + cnt, + Self::try_auto_start_job_str(is_reconnected, job_str) + .as_deref() + .unwrap_or(job_str), + is_reconnected, + ); cnt += 1; log::info!("restore write_job: {:?}", job_str); } @@ -1623,7 +1662,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clear_all_jobs(&self); fn new_message(&self, msg: String); fn update_transfer_list(&self); - fn load_last_job(&self, cnt: i32, job_json: &str); + fn load_last_job(&self, cnt: i32, job_json: &str, auto_start: bool); fn update_folder_files( &self, id: i32,