add: send/receive file/folder
This commit is contained in:
parent
1db7fee6fb
commit
79217ca1d9
@ -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) {
|
||||
|
@ -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'))),
|
||||
)
|
||||
],
|
||||
));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user