add: send/receive file/folder

This commit is contained in:
Kingtous 2022-07-11 10:30:45 +08:00
parent 1db7fee6fb
commit 79217ca1d9
6 changed files with 210 additions and 42 deletions

View File

@ -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) {

View File

@ -393,13 +393,52 @@ class _FileManagerPageState extends State<FileManagerPage>
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<FileManagerPage>
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<FileManagerPage>
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'))),
)
],
));

View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> evt) {
// ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}"
Map<String,dynamic> 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<T> {
@ -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;
}
}

View File

@ -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') {

View File

@ -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<FileEntry>, 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())
}

View File

@ -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<RwLock<Option<Session>>> = 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);