diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a987f54df..e1315d233 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -202,7 +202,7 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toString() + " B"; + return size.toStringAsFixed(2) + " B"; } else if (size < M) { return (size / K).toStringAsFixed(2) + " KB"; } else if (size < G) { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e9f4ed29c..5de1c206c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -393,13 +393,52 @@ class _FileManagerPageState extends State decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( - itemExtent: 100, itemBuilder: (BuildContext context, int index) { - final item = model.jobTable[index + 1]; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Text('${item.id}'), - Icon(Icons.delete) + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: Icon(Icons.send)), + SizedBox(width: 16.0,), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: item.jobName, + child: Text('${item.jobName}', + maxLines: 1, + style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis,)), + Wrap( + children: [ + Text('${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text('${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage(offstage: item.state != JobState.inProgress, child: Text('${readableFileSize(item.speed) + "/s"} ')), + Text('${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton(icon: Icon(Icons.delete), onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + },), + ], + ) + ], + ), + SizedBox(height: 8.0,), + Divider(height: 2.0, ) ], ); }, @@ -430,12 +469,15 @@ class _FileManagerPageState extends State Offstage( offstage: isLocal, child: TextButton.icon( - onPressed: (){}, icon: Transform.rotate( + onPressed: (){ + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: true); + }, icon: Transform.rotate( angle: isLocal ? 0 : pi, child: Icon( Icons.send ), - ), label: Text(isLocal ? translate('Send') : translate('Receive'))), + ), label: Text(translate('Receive'))), ), Expanded( child: Container( @@ -495,12 +537,15 @@ class _FileManagerPageState extends State Offstage( offstage: !isLocal, child: TextButton.icon( - onPressed: (){}, icon: Transform.rotate( + onPressed: (){ + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + }, icon: Transform.rotate( angle: isLocal ? 0 : pi, child: Icon( Icons.send ), - ), label: Text(isLocal ? translate('Send') : translate('Receive'))), + ), label: Text(translate('Send'))), ) ], )); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index af5e5db4b..996c5112c 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -56,6 +56,10 @@ class FileModel extends ChangeNotifier { return isLocal ? _localOption.home : _remoteOption.home; } + int getJob(int id) { + return jobTable.indexWhere((element) => element.id == id); + } + String get currentShortPath { if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); @@ -136,11 +140,14 @@ class FileModel extends ChangeNotifier { _jobProgress.finishedSize = int.parse(evt['finished_size']); } else { // Desktop uses jobTable - final job = _jobTable[id]; - if (job != null) { + // id = index + 1 + final jobIndex = getJob(id); + if (jobIndex >= 0 && _jobTable.length > jobIndex){ + final job = _jobTable[jobIndex]; job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); + debugPrint("update job ${id} with ${evt}"); } } notifyListeners(); @@ -150,14 +157,28 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - if (_remoteOption.home.isEmpty && evt['is_local'] == "false") { + debugPrint("recv file dir:${evt}"); + if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { final fd = FileDirectory.fromJson(jsonDecode(evt['value'])); fd.format(_remoteOption.isWindows, sort: _sortStyle); - _remoteOption.home = fd.path; - debugPrint("init remote home:${fd.path}"); - _currentRemoteDir = fd; + if (fd.id > 0){ + final jobIndex = getJob(fd.id); + if (jobIndex != -1){ + final job = jobTable[jobIndex]; + var totalSize = 0; + var fileCount = fd.entries.length; + fd.entries.forEach((element) {totalSize += element.size;}); + job.totalSize = totalSize; + job.fileCount = fileCount; + debugPrint("update receive details:${fd.path}"); + } + } else if (_remoteOption.home.isEmpty) { + _remoteOption.home = fd.path; + debugPrint("init remote home:${fd.path}"); + _currentRemoteDir = fd; + } notifyListeners(); return; } finally {} @@ -166,33 +187,57 @@ class FileModel extends ChangeNotifier { } jobDone(Map evt) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; + if (!isDesktop) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; + _jobProgress.state = JobState.done; + } else { + int id = int.parse(evt['id']); + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.finishedSize = job.totalSize; + job.state = JobState.done; + job.fileNum = int.parse(evt['file_num']); + } } - _selectMode = false; - _jobProgress.state = JobState.done; refresh(); } jobError(Map evt) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; + if (!isDesktop) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; + _jobProgress.clear(); + _jobProgress.state = JobState.error; + } else { + int jobIndex = getJob(int.parse(evt['id'])); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.state = JobState.error; + } } - debugPrint("jobError $evt"); - _selectMode = false; - _jobProgress.clear(); - _jobProgress.state = JobState.error; notifyListeners(); } overrideFileConfirm(Map evt) async { final resp = await showFileConfirmDialog( translate("Overwrite"), "${evt['read_path']}", true); + final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { - cancelJob(int.tryParse(evt['id']) ?? 0); + final jobIndex = getJob(id); + if (jobIndex != -1){ + cancelJob(id); + final job = jobTable[jobIndex]; + job.state = JobState.done; + } } else { var need_override = false; if (resp == null) { @@ -203,9 +248,9 @@ class FileModel extends ChangeNotifier { need_override = true; } _ffi.target?.bind.sessionSetConfirmOverrideFile(id: _ffi.target?.id ?? "", - actId: evt['id'], fileNum: evt['file_num'], + actId: id, fileNum: int.parse(evt['file_num']), needOverride: need_override, remember: fileConfirmCheckboxRemember, - isUpload: evt['is_upload']); + isUpload: evt['is_upload'] == "true"); } } @@ -319,7 +364,6 @@ class FileModel extends ChangeNotifier { sendFiles(SelectedItems items, {bool isRemote = false}) { if (isDesktop) { // desktop sendFiles - _jobProgress.state = JobState.inProgress; final toPath = isRemote ? currentRemoteDir.path : currentLocalDir.path; final isWindows = @@ -328,10 +372,14 @@ class FileModel extends ChangeNotifier { isRemote ? _localOption.showHidden : _remoteOption.showHidden ; items.items.forEach((from) async { final jobId = ++_jobId; - _jobTable[jobId] = JobProgress() + _jobTable.add(JobProgress() + ..jobName = from.path + ..totalSize = from.size ..state = JobState.inProgress - ..id = jobId; - await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) + ..id = jobId + ..isRemote = isRemote + ); + _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); }); } else { @@ -543,20 +591,20 @@ class FileModel extends ChangeNotifier { } sendRemoveFile(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); + _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); + _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } createDir(String path) async { _jobId++; - _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); + _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } cancelJob(int id) async { - _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.getId()}', actId: id); + _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.id}', actId: id); jobReset(); } @@ -577,6 +625,21 @@ class FileModel extends ChangeNotifier { initFileFetcher() { _fileFetcher.id = _ffi.target?.id; } + + void updateFolderFiles(Map evt) { + // ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}" + Map info = json.decode(evt['info']); + int id = info['id']; + int num_entries = info['num_entries']; + double total_size = info['total_size']; + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.fileCount = num_entries; + job.totalSize = total_size.toInt(); + } + debugPrint("update folder files: ${info}"); + } } class JobResultListener { @@ -784,12 +847,33 @@ class Entry { enum JobState { none, inProgress, done, error } +extension JobStateDisplay on JobState { + String display() { + switch (this) { + case JobState.none: + return translate("Waiting"); + case JobState.inProgress: + return translate("Transfer File"); + case JobState.done: + return translate("Finished"); + case JobState.error: + return translate("Error"); + default: + return ""; + } + } +} + class JobProgress { JobState state = JobState.none; var id = 0; var fileNum = 0; var speed = 0.0; var finishedSize = 0; + var totalSize = 0; + var fileCount = 0; + var isRemote = false; + var jobName = ""; clear() { state = JobState.none; @@ -797,6 +881,8 @@ class JobProgress { fileNum = 0; speed = 0; finishedSize = 0; + jobName = ""; + fileCount = 0; } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e5e521035..a76fe8e04 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -168,6 +168,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { @@ -217,6 +219,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { diff --git a/src/common.rs b/src/common.rs index 92ccb901e..c344b93a1 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,9 @@ +use std::sync::{Arc, Mutex}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; +use serde_json::json; + use hbb_common::{ allow_err, anyhow::bail, @@ -14,7 +18,6 @@ use hbb_common::{ }; // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use std::sync::{Arc, Mutex}; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; @@ -633,3 +636,30 @@ pub fn make_fd_to_json(fd: FileDirectory) -> String { fd_json.insert("entries".into(), json!(entries)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} diff --git a/src/flutter.rs b/src/flutter.rs index 4877cce58..8514e7515 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -27,7 +27,7 @@ use hbb_common::{ }; use crate::common::make_fd_to_json; -use crate::{client::*, flutter_ffi::EventToUI}; +use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); @@ -991,6 +991,9 @@ impl Connection { to, job.files().len() ); + let m = make_fd_flutter(id, job.files(), true); + self.session + .push_event("update_folder_files", vec![("info", &m)]); let files = job.files().clone(); self.read_jobs.push(job); self.timer = time::interval(MILLI1);