From 8745fcbb6adc5729b3571486083c27fdd07e67ab Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 20 Aug 2024 10:53:55 +0800 Subject: [PATCH] opt desktop file manager status list (#9117) * Show delete file/dir log * Show full path rather than base file name * Show files count * Opt status card layout * Change selected color to accent Signed-off-by: 21pages --- .../lib/desktop/pages/file_manager_page.dart | 93 ++++--- flutter/lib/models/file_model.dart | 252 +++++++++++++++--- flutter/lib/models/model.dart | 9 +- 3 files changed, 272 insertions(+), 82 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index c9e565fd7..682ffa831 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -173,10 +173,25 @@ class _FileManagerPageState extends State /// transfer status list /// watch transfer status Widget statusList() { + Widget getIcon(JobProgress job) { + final color = Theme.of(context).tabBarTheme.labelColor; + switch (job.type) { + case JobType.deleteDir: + case JobType.deleteFile: + return Icon(Icons.delete_outline, color: color); + default: + return Transform.rotate( + angle: job.isRemoteToLocal ? pi : 0, + child: Icon(Icons.arrow_forward_ios, color: color), + ); + } + } + statusListView(List jobs) => ListView.builder( controller: ScrollController(), itemBuilder: (BuildContext context, int index) { final item = jobs[index]; + final status = item.getStatus(); return Padding( padding: const EdgeInsets.only(bottom: 5), child: generateCard( @@ -186,15 +201,8 @@ class _FileManagerPageState extends State Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Transform.rotate( - angle: item.isRemoteToLocal ? pi : 0, - child: SvgPicture.asset("assets/arrow.svg", - colorFilter: svgColor( - Theme.of(context).tabBarTheme.labelColor)), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, - ), + getIcon(item) + .marginSymmetric(horizontal: 10, vertical: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -204,44 +212,24 @@ class _FileManagerPageState extends State waitDuration: Duration(milliseconds: 500), message: item.jobName, child: Text( - item.fileName, + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, - ).paddingSymmetric(vertical: 10), - ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, ), ), - Offstage( - offstage: item.state != JobState.inProgress, - child: Text( - '${translate("Speed")} ${readableFileSize(item.speed)}/s', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: status, + child: Text(status, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + )).marginOnly(top: 6), ), Offstage( - offstage: item.state == JobState.inProgress, - child: Text( - translate( - item.display(), - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), - ), - Offstage( - offstage: item.state != JobState.inProgress, + offstage: item.type != JobType.transfer || + item.state != JobState.inProgress, child: LinearPercentIndicator( - padding: EdgeInsets.only(right: 15), animateFromLastPercent: true, center: Text( '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', @@ -251,7 +239,7 @@ class _FileManagerPageState extends State progressColor: MyTheme.accent, backgroundColor: Theme.of(context).hoverColor, lineHeight: kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ).paddingSymmetric(vertical: 8), ), ], ), @@ -276,7 +264,6 @@ class _FileManagerPageState extends State ), MenuButton( tooltip: translate("Delete"), - padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", colorFilter: svgColor(Colors.white), @@ -289,11 +276,11 @@ class _FileManagerPageState extends State hoverColor: MyTheme.accent80, ), ], - ), + ).marginAll(12), ], ), ], - ).paddingSymmetric(vertical: 10), + ), ), ); }, @@ -1007,7 +994,7 @@ class _FileManagerViewState extends State { child: Obx(() => Container( decoration: BoxDecoration( color: selectedItems.items.contains(entry) - ? Theme.of(context).hoverColor + ? MyTheme.button : Theme.of(context).cardColor, borderRadius: BorderRadius.all( Radius.circular(5.0), @@ -1050,6 +1037,11 @@ class _FileManagerViewState extends State { ), Expanded( child: Text(entry.name.nonBreaking, + style: TextStyle( + color: selectedItems.items + .contains(entry) + ? Colors.white + : null), overflow: TextOverflow.ellipsis)) ]), @@ -1111,7 +1103,10 @@ class _FileManagerViewState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, - color: MyTheme.darkGray, + color: selectedItems.items + .contains(entry) + ? Colors.white70 + : MyTheme.darkGray, ), )), ), @@ -1131,7 +1126,11 @@ class _FileManagerViewState extends State { sizeStr, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), + fontSize: 10, + color: + selectedItems.items.contains(entry) + ? Colors.white70 + : MyTheme.darkGray), ), ), ), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 0838c8b06..68af29380 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -451,7 +451,7 @@ class FileController { final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; for (var from in items.items) { - final jobID = jobController.add(from, isRemoteToLocal); + final jobID = jobController.addTransferJob(from, isRemoteToLocal); bind.sessionSendFiles( sessionId: sessionId, actId: jobID, @@ -494,13 +494,21 @@ class FileController { fd.format(isWindows); dialogManager?.dismissAll(); if (fd.entries.isEmpty) { + var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0); final confirm = await showRemoveDialog( translate( "Are you sure you want to delete this empty directory?"), item.name, false); if (confirm == true) { - sendRemoveEmptyDir(item.path, 0); + sendRemoveEmptyDir( + item.path, + 0, + deleteJobId, + ); + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", state: JobState.done); } return; } @@ -508,6 +516,13 @@ class FileController { } else { entries = []; } + int deleteJobId; + if (item.isDirectory) { + deleteJobId = + jobController.addDeleteDirJob(item, !isLocal, entries.length); + } else { + deleteJobId = jobController.addDeleteFileJob(item, !isLocal); + } for (var i = 0; i < entries.length; i++) { final dirShow = item.isDirectory @@ -522,24 +537,32 @@ class FileController { ); try { if (confirm == true) { - sendRemoveFile(entries[i].path, i); + sendRemoveFile(entries[i].path, i, deleteJobId); final res = await jobController.jobResultListener.start(); // handle remove res; if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } + } else { + jobController.updateJobStatus(deleteJobId, + file_num: i, error: "cancel"); } if (_removeCheckboxRemember) { if (confirm == true) { for (var j = i + 1; j < entries.length; j++) { - sendRemoveFile(entries[j].path, j); + sendRemoveFile(entries[j].path, j, deleteJobId); final res = await jobController.jobResultListener.start(); if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } } + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", + file_num: entries.length, + state: JobState.done); } break; } @@ -618,22 +641,19 @@ class FileController { }, useAnimation: false); } - void sendRemoveFile(String path, int fileNum) { + void sendRemoveFile(String path, int fileNum, int actId) { bind.sessionRemoveFile( sessionId: sessionId, - actId: JobController.jobID.next(), + actId: actId, path: path, isRemote: !isLocal, fileNum: fileNum); } - void sendRemoveEmptyDir(String path, int fileNum) { + void sendRemoveEmptyDir(String path, int fileNum, int actId) { history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( - sessionId: sessionId, - actId: JobController.jobID.next(), - path: path, - isRemote: !isLocal); + sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } Future createDir(String path) async { @@ -729,14 +749,11 @@ class JobController { return jobTable.indexWhere((element) => element.id == id); } - // JobProgress? getJob(int id) { - // return jobTable.firstWhere((element) => element.id == id); - // } - // return jobID - int add(Entry from, bool isRemoteToLocal) { + int addTransferJob(Entry from, bool isRemoteToLocal) { final jobID = JobController.jobID.next(); jobTable.add(JobProgress() + ..type = JobType.transfer ..fileName = path.basename(from.path) ..jobName = from.path ..totalSize = from.size @@ -746,6 +763,33 @@ class JobController { return jobID; } + int addDeleteFileJob(Entry file, bool isRemote) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteFile + ..fileName = path.basename(file.path) + ..jobName = file.path + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + + int addDeleteDirJob(Entry file, bool isRemote, int fileCount) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteDir + ..fileName = path.basename(file.path) + ..jobName = file.path + ..fileCount = fileCount + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + void tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); @@ -756,6 +800,7 @@ class JobController { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); + job.recvJobRes = true; debugPrint("update job $id with $evt"); jobTable.refresh(); } @@ -764,20 +809,48 @@ class JobController { } } - void jobDone(Map evt) async { + Future jobDone(Map evt) async { if (jobResultListener.isListening) { jobResultListener.complete(evt); - return; + // return; } - - int id = int.parse(evt['id']); + int id = -1; + int? fileNum = 0; + double? speed = 0; + try { + id = int.parse(evt['id']); + } catch (_) {} final jobIndex = getJob(id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.finishedSize = job.totalSize; + if (jobIndex == -1) return true; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (job.type == JobType.deleteFile) { job.state = JobState.done; - job.fileNum = int.parse(evt['file_num']); - jobTable.refresh(); + } else if (job.type == JobType.deleteDir) { + try { + fileNum = int.tryParse(evt['file_num']); + } catch (_) {} + if (fileNum != null) { + if (fileNum < job.fileNum) return true; // file_num can be 0 at last + job.fileNum = fileNum; + if (fileNum >= job.fileCount - 1) { + job.state = JobState.done; + } + } + } else { + try { + fileNum = int.tryParse(evt['file_num']); + speed = double.tryParse(evt['speed']); + } catch (_) {} + if (fileNum != null) job.fileNum = fileNum; + if (speed != null) job.speed = speed; + job.state = JobState.done; + } + jobTable.refresh(); + if (job.type == JobType.deleteDir) { + return job.state == JobState.done; + } else { + return true; } } @@ -788,16 +861,52 @@ class JobController { final job = jobTable[jobIndex]; job.state = JobState.error; job.err = err; - job.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - job.state = JobState.done; - job.finishedSize = job.totalSize; + job.recvJobRes = true; + if (job.type == JobType.transfer) { + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } + } else if (job.type == JobType.deleteDir) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + } else if (job.type == JobType.deleteFile) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } } jobTable.refresh(); } debugPrint("jobError $evt"); } + void updateJobStatus(int id, + {int? file_num, String? error, JobState? state}) { + final jobIndex = getJob(id); + if (jobIndex < 0) return; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (file_num != null) { + job.fileNum = file_num; + } + if (error != null) { + job.err = error; + job.state = JobState.error; + } + if (state != null) { + job.state = state; + } + if (job.type == JobType.deleteFile && error == null) { + job.state = JobState.done; + } + jobTable.refresh(); + } + Future cancelJob(int id) async { await bind.sessionCancelJob(sessionId: sessionId, actId: id); } @@ -814,6 +923,7 @@ class JobController { 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 @@ -1088,8 +1198,12 @@ extension JobStateDisplay on JobState { } } +enum JobType { none, transfer, deleteFile, deleteDir } + class JobProgress { + JobType type = JobType.none; JobState state = JobState.none; + var recvJobRes = false; var id = 0; var fileNum = 0; var speed = 0.0; @@ -1109,7 +1223,9 @@ class JobProgress { int lastTransferredSize = 0; clear() { + type = JobType.none; state = JobState.none; + recvJobRes = false; id = 0; fileNum = 0; speed = 0; @@ -1123,11 +1239,81 @@ class JobProgress { } String display() { - if (state == JobState.done && err == "skipped") { - return translate("Skipped"); + if (type == JobType.transfer) { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + } else if (type == JobType.deleteFile) { + if (err == "cancel") { + return translate("Cancel"); + } } + return state.display(); } + + String getStatus() { + int handledFileCount = recvJobRes ? fileNum + 1 : fileNum; + if (handledFileCount >= fileCount) { + handledFileCount = fileCount; + } + if (state == JobState.done) { + handledFileCount = fileCount; + finishedSize = totalSize; + } + final filesStr = "$handledFileCount/$fileCount files"; + final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : ""; + final sizePercentStr = totalSize > 0 && finishedSize > 0 + ? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}" + : ""; + if (type == JobType.deleteFile) { + return display(); + } else if (type == JobType.deleteDir) { + var res = ''; + if (state == JobState.done || state == JobState.error) { + res = display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += " "; + } + res += filesStr; + } + + if (sizeStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + return res; + } else if (type == JobType.transfer) { + var res = ""; + if (state != JobState.inProgress && state != JobState.none) { + res += display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += filesStr; + } + if (sizeStr.isNotEmpty && state != JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + if (sizePercentStr.isNotEmpty && state == JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizePercentStr; + } + return res; + } + return ''; + } } class _PathStat { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 050a92a5f..d5377ea9a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier { } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - parent.target?.fileModel.jobController.jobDone(evt); - parent.target?.fileModel.refreshAll(); + bool? refresh = + await parent.target?.fileModel.jobController.jobDone(evt); + if (refresh == true) { + // many job done for delete directory + // todo: refresh may not work when confirm delete local directory + parent.target?.fileModel.refreshAll(); + } } else if (name == 'job_error') { parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') {