From 0c6df924d18180db30584286dd20e5c68104d7c3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 23 Jun 2026 11:23:42 +0800 Subject: [PATCH] refact: file transfer, do this for all conflicts(tasks) (#15385) Signed-off-by: fufesou --- flutter/lib/models/file_model.dart | 177 ++++++++++++++++++++++++++--- src/ui/file_transfer.tis | 91 +++++++++++---- 2 files changed, 236 insertions(+), 32 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 7d91b03b3..94f0fcb7b 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -142,12 +142,22 @@ class FileModel { } Future postOverrideFileConfirm(Map evt) async { + final id = int.tryParse(evt['id']?.toString() ?? ''); + if (id == null || !jobController.hasTransferConflictJob(id)) { + debugPrint("Ignore stale override confirm event: $evt"); + return; + } evtLoop.pushEvent( _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); } Future overrideFileConfirm(Map evt, {bool? overrideConfirm, bool skip = false}) async { + final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0; + if (id == 0 || !jobController.hasTransferConflictJob(id)) { + debugPrint("Ignore override confirm for inactive job: $evt"); + return; + } // If `skip == true`, it means to skip this file without showing dialog. // Because `resp` may be null after the user operation or the last remembered operation, // and we should distinguish them. @@ -156,15 +166,12 @@ class FileModel { ? await showFileConfirmDialog(translate("Overwrite"), "${evt['read_path']}", true, evt['is_identical'] == "true") : null); - final id = int.tryParse(evt['id']) ?? 0; + if (!jobController.hasTransferConflictJob(id)) { + debugPrint("Ignore override confirm result for inactive job: $evt"); + return; + } if (false == resp) { - final jobIndex = jobController.getJob(id); - if (jobIndex != -1) { - await jobController.cancelJob(id); - final job = jobController.jobTable[jobIndex]; - job.state = JobState.done; - jobController.jobTable.refresh(); - } + await jobController.cancelTransferConflictBatch(id); } else { var need_override = false; if (resp == null) { @@ -176,6 +183,7 @@ class FileModel { } // Update the loop config. if (fileConfirmCheckboxRemember) { + jobController.rememberTransferConflictBatch(id, resp); evtLoop.setSkip(!need_override); } await bind.sessionSetConfirmOverrideFile( @@ -285,6 +293,8 @@ class FileModel { final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; final jobID = jobController.addTransferJob(entry, false); + jobController.registerTransferConflictBatch([jobID], + batchId: int.tryParse(obj['batchId']?.toString() ?? '')); webSendLocalFiles( handleIndex: handleIndex, actId: jobID, @@ -570,8 +580,15 @@ class FileController { final toPath = otherSideData.directory.path; final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; + final transferJobs = <(Entry, int)>[]; + final transferJobIds = []; for (var from in items.items) { final jobID = jobController.addTransferJob(from, isRemoteToLocal); + transferJobs.add((from, jobID)); + transferJobIds.add(jobID); + } + jobController.registerTransferConflictBatch(transferJobIds); + for (final (from, jobID) in transferJobs) { bind.sessionSendFiles( sessionId: sessionId, actId: jobID, @@ -917,6 +934,10 @@ class JobController { static final JobID jobID = JobID(); final jobTable = List.empty(growable: true).obs; final jobResultListener = JobResultListener>(); + int _nextTransferConflictBatchId = 1; + final Map _transferConflictJobToBatch = {}; + int? _transferConflictRememberBatchId; + bool? _transferConflictRememberOverrideConfirm; final GetSessionID getSessionID; final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); @@ -929,6 +950,57 @@ class JobController { return jobTable.indexWhere((element) => element.id == id); } + void registerTransferConflictBatch(Iterable jobIds, {int? batchId}) { + final ids = jobIds.toList(growable: false); + if (ids.isEmpty) { + return; + } + batchId ??= _nextTransferConflictBatchId++; + if (batchId >= _nextTransferConflictBatchId) { + _nextTransferConflictBatchId = batchId + 1; + } + for (final jobId in ids) { + _transferConflictJobToBatch[jobId] = batchId; + } + } + + int? transferConflictBatchId(int jobId) { + return _transferConflictJobToBatch[jobId]; + } + + bool hasTransferConflictJob(int jobId) { + return transferConflictBatchId(jobId) != null; + } + + bool isTransferConflictRememberBatch(int? batchId) { + return batchId != null && batchId == _transferConflictRememberBatchId; + } + + bool? transferConflictRememberOverrideConfirm(int? batchId) { + if (!isTransferConflictRememberBatch(batchId)) { + return null; + } + return _transferConflictRememberOverrideConfirm; + } + + void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) { + _transferConflictRememberBatchId = _transferConflictJobToBatch[jobId]; + _transferConflictRememberOverrideConfirm = overrideConfirm; + } + + void unregisterTransferConflictJob(int jobId) { + final batchId = _transferConflictJobToBatch.remove(jobId); + if (batchId == null) { + return; + } + if (!_transferConflictJobToBatch.containsValue(batchId)) { + if (_transferConflictRememberBatchId == batchId) { + _transferConflictRememberBatchId = null; + _transferConflictRememberOverrideConfirm = null; + } + } + } + // return jobID int addTransferJob(Entry from, bool isRemoteToLocal) { final jobID = JobController.jobID.next(); @@ -1000,7 +1072,10 @@ class JobController { id = int.parse(evt['id']); } catch (_) {} final jobIndex = getJob(id); - if (jobIndex == -1) return true; + if (jobIndex == -1) { + unregisterTransferConflictJob(id); + return true; + } final job = jobTable[jobIndex]; job.recvJobRes = true; if (job.type == JobType.deleteFile) { @@ -1026,6 +1101,9 @@ class JobController { job.state = JobState.done; } jobTable.refresh(); + if (job.state == JobState.done || job.state == JobState.error) { + unregisterTransferConflictJob(id); + } if (job.type == JobType.deleteDir) { return job.state == JobState.done; } else { @@ -1035,9 +1113,15 @@ class JobController { void jobError(Map evt) { final err = evt['err'].toString(); - int jobIndex = getJob(int.parse(evt['id'])); + final id = int.tryParse(evt['id']?.toString() ?? ''); + if (id == null) { + debugPrint("Ignore job error with invalid id: $evt"); + return; + } + int jobIndex = getJob(id); if (jobIndex != -1) { final job = jobTable[jobIndex]; + if (job.state == JobState.done && job.err == "cancel") return; job.state = JobState.error; job.err = err; job.recvJobRes = true; @@ -1060,6 +1144,11 @@ class JobController { } } jobTable.refresh(); + if (job.state == JobState.done || job.state == JobState.error) { + unregisterTransferConflictJob(job.id); + } + } else { + unregisterTransferConflictJob(id); } if (err == _kOneWayFileTransferError) { if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { @@ -1096,9 +1185,42 @@ class JobController { } Future cancelJob(int id) async { + unregisterTransferConflictJob(id); await bind.sessionCancelJob(sessionId: sessionId, actId: id); } + Future cancelTransferConflictBatch(int jobId) async { + final batchId = _transferConflictJobToBatch[jobId]; + final batchJobIds = batchId == null ? [jobId] : []; + if (batchId != null) { + for (final entry in _transferConflictJobToBatch.entries) { + if (entry.value == batchId) { + batchJobIds.add(entry.key); + } + } + for (final id in batchJobIds) { + unregisterTransferConflictJob(id); + } + } + final jobIdsToCancel = batchJobIds.toSet(); + for (final job in jobTable) { + if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) { + continue; + } + job.state = JobState.done; + job.err = "cancel"; + job.recvJobRes = true; + } + jobTable.refresh(); + for (final id in batchJobIds) { + try { + await bind.sessionCancelJob(sessionId: sessionId, actId: id); + } catch (e) { + debugPrint("Failed to cancel transfer job $id in conflict batch: $e"); + } + } + } + Future loadLastJob(Map evt) async { debugPrint("load last job: $evt"); Map jobDetail = json.decode(evt['value']); @@ -1145,7 +1267,7 @@ class JobController { ..state = JobState.paused; jobTable.add(jobProgress); } - + registerTransferConflictBatch([currJobId]); await bind.sessionAddJob( sessionId: sessionId, isRemote: isRemote, @@ -1193,6 +1315,9 @@ class JobController { void clear() { jobTable.clear(); + _transferConflictJobToBatch.clear(); + _transferConflictRememberBatchId = null; + _transferConflictRememberOverrideConfirm = null; jobResultListener.clear(); } } @@ -1535,6 +1660,9 @@ class JobProgress { String display() { if (type == JobType.transfer) { + if (state == JobState.done && err == "cancel") { + return translate("Cancel"); + } if (state == JobState.done && err == "skipped") { return translate("Skipped"); } @@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent> { class FileDialogEventLoop extends BaseEventLoop> { + int? _batchId; bool? _overrideConfirm; bool _skip = false; @override Future onPreConsume( BaseEvent> evt) async { - var event = evt as _FileDialogEvent; + final event = evt as _FileDialogEvent; + final model = event.fileModel.target; + final jobId = int.tryParse(evt.data['id']?.toString() ?? ''); + final batchId = model == null || jobId == null + ? null + : model.jobController.transferConflictBatchId(jobId); + final keepRemembered = model != null && + model.jobController.isTransferConflictRememberBatch(batchId); + // The loop only preloads the remembered batch choice. The model updates it + // after the user answers the current overwrite dialog. + if (_batchId != batchId && !keepRemembered) { + _batchId = batchId; + _overrideConfirm = null; + _skip = false; + } else { + _batchId = batchId; + } + if (keepRemembered) { + _overrideConfirm = + model.jobController.transferConflictRememberOverrideConfirm(batchId); + _skip = _overrideConfirm == null; + } event.setOverrideConfirm(_overrideConfirm); event.setSkip(_skip); debugPrint( - "FileDialogEventLoop: consuming"); + "FileDialogEventLoop: consuming"); } @override Future onEventsClear() { + _batchId = null; _overrideConfirm = null; _skip = false; return super.onEventsClear(); diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 1090c018d..cc682e5db 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -72,6 +72,48 @@ function getExt(name) { class JobTable: Reactor.Component { this var jobs = []; this var job_map = {}; + this var next_conflict_batch_id = 1; + this var remembered_write_strategy = {}; + + function nextConflictBatchId() { + return this.next_conflict_batch_id++; + } + + function getRememberedWriteStrategy(conflict_batch_id) { + var is_override = this.remembered_write_strategy[conflict_batch_id]; + if (is_override == true || is_override == false) return is_override; + return null; + } + + function rememberWriteStrategy(conflict_batch_id, is_override) { + this.remembered_write_strategy[conflict_batch_id] = is_override; + } + + function cancelTransferJob(job) { + job.finished = true; + job.err = "cancel"; + this.updateJob(job); + } + + function cancelTransferConflictBatch(id) { + var job = this.job_map[id]; + if (!job) return; + var conflict_batch_id = job.conflict_batch_id; + if (conflict_batch_id == null) { + this.cancelTransferJob(job); + handler.cancel_job(job.id); + refreshDir(!job.is_remote); + return; + } + delete this.remembered_write_strategy[conflict_batch_id]; + for (var i = 0; i < this.jobs.length; ++i) { + var current_job = this.jobs[i]; + if (current_job.conflict_batch_id != conflict_batch_id || current_job.finished) continue; + this.cancelTransferJob(current_job); + handler.cancel_job(current_job.id); + } + refreshDir(!job.is_remote); + } function render() { var me = this; @@ -109,10 +151,12 @@ class JobTable: Reactor.Component { function clearAllJobs() { this.jobs = []; this.job_map = {}; + this.next_conflict_batch_id = 1; + this.remembered_write_strategy = {}; this.update(); } - function send(path, is_remote) { + function send(path, is_remote, conflict_batch_id = null) { var to; var show_hidden; if (is_remote) { @@ -123,13 +167,15 @@ class JobTable: Reactor.Component { show_hidden = file_transfer.local_folder_view.show_hidden; } if (!to) return; + if (conflict_batch_id == null) conflict_batch_id = this.nextConflictBatchId(); to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); var id = handler.get_next_job_id(); this.jobs.push({ type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, is_remote: is_remote, - is_last: false + is_last: false, + conflict_batch_id: conflict_batch_id }); this.job_map[id] = this.jobs[this.jobs.length - 1]; handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); @@ -141,7 +187,8 @@ class JobTable: Reactor.Component { var job = { type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, - is_remote: is_remote, is_last: true, file_num: file_num }; + is_remote: is_remote, is_last: true, file_num: file_num, + conflict_batch_id: this.nextConflictBatchId() }; this.jobs.push(job); this.job_map[id] = this.jobs[this.jobs.length - 1]; handler.update_next_job_id(id + 1); @@ -230,6 +277,7 @@ class JobTable: Reactor.Component { else return translate("Waiting"); } } + if (job.err == "cancel") return translate("Cancel"); if (!job.entries) return translate("Waiting"); var i = job.file_num + 1; var n = job.num_entries || job.entries.length; @@ -262,6 +310,8 @@ class JobTable: Reactor.Component { function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { var job = this.job_map[id]; + if (!job) return; + if (job.finished && job.err == "cancel") return; if (job.type == "del-file"){ job.finished = true; job.err = err; @@ -269,7 +319,6 @@ class JobTable: Reactor.Component { this.updateJob(job); return; } - if (!job) return; if (file_num < job.file_num) return; job.file_num = file_num; var n = job.num_entries || job.entries.length; @@ -601,8 +650,9 @@ class FolderView : Reactor.Component { event click $(.send) () { var rows = this.getCurrentRows(); if (!rows || rows.length == 0) return; + var conflict_batch_id = file_transfer.job_table.nextConflictBatchId(); for (var i = 0; i < rows.length; ++i) { - file_transfer.job_table.send(rows[i][0], this.is_remote); + file_transfer.job_table.send(rows[i][0], this.is_remote, conflict_batch_id); } } @@ -780,6 +830,13 @@ handler.confirmDeleteFiles = function(id, i, name) { handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { var jt = file_transfer.job_table; + var job = jt.job_map[id]; + if (!job || job.finished) return; + var remembered = jt.getRememberedWriteStrategy(job.conflict_batch_id); + if (remembered == true || remembered == false) { + handler.set_write_override(id, file_num, remembered, true, is_upload); + return; + } var identical_msg = is_identical ? translate("identical_file_tip"): ""; msgbox("custom-skip", "Confirm Write Strategy", "
\
" + translate('Overwrite') + " " + translate('files') + ".
\ @@ -788,22 +845,18 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical
" + identical_msg + "
\
" + translate('Do this for all conflicts') + "
\
", "", function(res=null) { + var current_job = jt.job_map[id]; + if (!current_job || current_job.finished) return; if (!res) { - jt.updateJobStatus(id, -1, "cancel"); - handler.cancel_job(id); - } else if (res.skip) { - if (res.remember){ - handler.set_write_override(id,file_num,false,true, is_upload); // - } else { - handler.set_write_override(id,file_num,false,false,is_upload); // - } - } else { - if (res.remember){ - handler.set_write_override(id,file_num,true,true,is_upload); // - } else { - handler.set_write_override(id,file_num,true,false,is_upload); // - } + jt.cancelTransferConflictBatch(id); + return; } + var is_override = !res.skip; + var remember = res.remember ? true : false; + if (remember) { + jt.rememberWriteStrategy(current_job.conflict_batch_id, is_override); + } + handler.set_write_override(id, file_num, is_override, remember, is_upload); }); }