Merge pull request #956 from Kingtous/flutter_desktop
feat: file transfer implementation for flutter
This commit is contained in:
commit
eb14a7a977
@ -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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
@ -23,15 +23,18 @@ class FileManagerPage extends StatefulWidget {
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final _selectedItems = SelectedItems();
|
||||
final _breadCrumbLocalScroller = ScrollController();
|
||||
final _breadCrumbRemoteScroller = ScrollController();
|
||||
final _localSelectedItems = SelectedItems();
|
||||
final _remoteSelectedItems = SelectedItems();
|
||||
|
||||
/// FFI with name file_transfer_id
|
||||
FFI get _ffi => ffi('ft_${widget.id}');
|
||||
|
||||
FileModel get model => _ffi.fileModel;
|
||||
|
||||
SelectedItems getSelectedItem(bool isLocal) {
|
||||
return isLocal ? _localSelectedItems : _remoteSelectedItems;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -83,53 +86,16 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}));
|
||||
}
|
||||
|
||||
bool needShowCheckBox() {
|
||||
if (!model.selectMode) {
|
||||
return false;
|
||||
}
|
||||
return !_selectedItems.isOtherPage(model.isLocal);
|
||||
}
|
||||
|
||||
Widget menu({bool isLocal = false}) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.refresh, color: Colors.black),
|
||||
SizedBox(width: 5),
|
||||
Text(translate("Refresh File"))
|
||||
],
|
||||
),
|
||||
value: "refresh",
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check, color: Colors.black),
|
||||
SizedBox(width: 5),
|
||||
Text(translate("Multi Select"))
|
||||
],
|
||||
),
|
||||
value: "select",
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.folder_outlined, color: Colors.black),
|
||||
SizedBox(width: 5),
|
||||
Text(translate("Create Folder"))
|
||||
],
|
||||
),
|
||||
value: "folder",
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
model.currentShowHidden
|
||||
model.getCurrentShowHidden(isLocal)
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank,
|
||||
color: Colors.black),
|
||||
@ -142,46 +108,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
];
|
||||
},
|
||||
onSelected: (v) {
|
||||
if (v == "refresh") {
|
||||
model.refresh();
|
||||
} else if (v == "select") {
|
||||
_selectedItems.clear();
|
||||
model.toggleSelectMode();
|
||||
} else if (v == "folder") {
|
||||
final name = TextEditingController();
|
||||
DialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
translate("Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
model.createDir(PathUtil.join(
|
||||
model.currentDir.path,
|
||||
name.value.text,
|
||||
model.currentIsWindows));
|
||||
close();
|
||||
}
|
||||
},
|
||||
child: Text(translate("OK")))
|
||||
]));
|
||||
} else if (v == "hidden") {
|
||||
if (v == "hidden") {
|
||||
model.toggleShowHidden(local: isLocal);
|
||||
}
|
||||
});
|
||||
@ -190,9 +117,23 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Widget body({bool isLocal = false}) {
|
||||
final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir;
|
||||
final entries = fd.entries;
|
||||
final sortIndex = (SortBy style) {
|
||||
switch (style) {
|
||||
case SortBy.Name:
|
||||
return 1;
|
||||
case SortBy.Type:
|
||||
return 0;
|
||||
case SortBy.Modified:
|
||||
return 2;
|
||||
case SortBy.Size:
|
||||
return 3;
|
||||
}
|
||||
}(model.getSortStyle(isLocal));
|
||||
final sortAscending =
|
||||
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white70, border: Border.all(color: Colors.grey)),
|
||||
color: Colors.white54, border: Border.all(color: Colors.black26)),
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
@ -205,16 +146,36 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
showCheckboxColumn: true,
|
||||
dataRowHeight: 30,
|
||||
dataRowHeight: 25,
|
||||
headingRowHeight: 30,
|
||||
columnSpacing: 8,
|
||||
showBottomBorder: true,
|
||||
sortColumnIndex: sortIndex,
|
||||
sortAscending: sortAscending,
|
||||
columns: [
|
||||
DataColumn(label: Text(translate(" "))), // icon
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Name"),
|
||||
)),
|
||||
DataColumn(label: Text(translate("Modified"))),
|
||||
DataColumn(label: Text(translate("Size"))),
|
||||
),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.Name,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Modified"),
|
||||
),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.Modified,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(translate("Size")),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.Size,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
],
|
||||
rows: entries.map((entry) {
|
||||
final sizeStr = entry.isFile
|
||||
@ -223,23 +184,44 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return DataRow(
|
||||
key: ValueKey(entry.name),
|
||||
onSelectChanged: (s) {
|
||||
// TODO
|
||||
if (s != null) {
|
||||
if (s) {
|
||||
getSelectedItem(isLocal).add(isLocal, entry);
|
||||
} else {
|
||||
getSelectedItem(isLocal).remove(entry);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
selected: getSelectedItem(isLocal).contains(entry),
|
||||
cells: [
|
||||
// TODO: icon
|
||||
DataCell(Icon(
|
||||
entry.isFile ? Icons.feed_outlined : Icons.folder,
|
||||
size: 25)),
|
||||
DataCell(
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 100),
|
||||
child: Tooltip(
|
||||
message: entry.name,
|
||||
child: Text(entry.name,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
onTap: () {
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)), onTap: () {
|
||||
if (entry.isDirectory) {
|
||||
model.openDirectory(entry.path, isLocal: isLocal);
|
||||
if (isLocal) {
|
||||
_localSelectedItems.clear();
|
||||
} else {
|
||||
_remoteSelectedItems.clear();
|
||||
}
|
||||
} else {
|
||||
// Perform file-related tasks.
|
||||
final _selectedItems = getSelectedItem(isLocal);
|
||||
if (_selectedItems.contains(entry)) {
|
||||
_selectedItems.remove(entry);
|
||||
} else {
|
||||
_selectedItems.add(isLocal, entry);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}),
|
||||
DataCell(Text(
|
||||
@ -263,7 +245,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
)
|
||||
],
|
||||
)),
|
||||
Center(child: listTail(isLocal: isLocal)),
|
||||
// Center(child: listTail(isLocal: isLocal)),
|
||||
// Expanded(
|
||||
// child: ListView.builder(
|
||||
// itemCount: entries.length + 1,
|
||||
@ -374,9 +356,94 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Widget statusList() {
|
||||
return PreferredSize(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16.0,bottom: 16.0, right: 16.0),
|
||||
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white70, border: Border.all(color: Colors.grey)),
|
||||
child: Obx(
|
||||
() => ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = model.jobTable[index];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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"} ')),
|
||||
Offstage(
|
||||
offstage: item.totalSize <= 0,
|
||||
child: Text(
|
||||
'${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: item.state != JobState.paused,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
model.resumeJob(item.id);
|
||||
},
|
||||
icon: Icon(Icons.restart_alt_rounded)),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
model.jobTable.removeAt(index);
|
||||
model.cancelJob(item.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Divider(
|
||||
height: 2.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: model.jobTable.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
preferredSize: Size(200, double.infinity));
|
||||
}
|
||||
@ -385,67 +452,176 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
model.goToParentDirectory(isLocal: isLocal);
|
||||
}
|
||||
|
||||
breadCrumbScrollToEnd(bool isLocal) {
|
||||
final controller =
|
||||
isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller;
|
||||
Future.delayed(Duration(milliseconds: 200), () {
|
||||
controller.animateTo(controller.position.maxScrollExtent,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.fastLinearToSlowEaseIn);
|
||||
});
|
||||
}
|
||||
|
||||
Widget headTools(bool isLocal) => Container(
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BreadCrumb(
|
||||
items: getPathBreadCrumbItems(() => model.goHome(), (list) {
|
||||
var path = "";
|
||||
if (model.currentHome.startsWith(list[0])) {
|
||||
// absolute path
|
||||
for (var item in list) {
|
||||
path = PathUtil.join(path, item, model.currentIsWindows);
|
||||
}
|
||||
// symbols
|
||||
PreferredSize(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(color: Colors.blue),
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: FutureBuilder<String>(
|
||||
future: _ffi.bind.sessionGetPlatform(
|
||||
id: _ffi.id, isRemote: !isLocal),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||
return getPlatformImage('${snapshot.data}');
|
||||
} else {
|
||||
path += model.currentHome;
|
||||
for (var item in list) {
|
||||
path = PathUtil.join(path, item, model.currentIsWindows);
|
||||
return CircularProgressIndicator(color: Colors.white,);
|
||||
}
|
||||
}
|
||||
model.openDirectory(path, isLocal: isLocal);
|
||||
}, isLocal),
|
||||
divider: Icon(Icons.chevron_right),
|
||||
overflow: ScrollableOverflow(
|
||||
controller: isLocal
|
||||
? _breadCrumbLocalScroller
|
||||
: _breadCrumbRemoteScroller),
|
||||
)),
|
||||
})),
|
||||
Text(isLocal
|
||||
? translate("Local Computer")
|
||||
: translate("Remote Computer"))
|
||||
.marginOnly(left: 8.0)
|
||||
],
|
||||
),
|
||||
preferredSize: Size(double.infinity, 70)),
|
||||
// buttons
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.goHome(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.home_outlined)),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_upward),
|
||||
onPressed: () {
|
||||
goBack(isLocal: isLocal);
|
||||
},
|
||||
),
|
||||
PopupMenuButton<SortBy>(
|
||||
icon: Icon(Icons.sort),
|
||||
itemBuilder: (context) {
|
||||
return SortBy.values
|
||||
.map((e) => PopupMenuItem(
|
||||
child:
|
||||
Text(translate(e.toString().split(".").last)),
|
||||
value: e,
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
onSelected: (sort) {
|
||||
model.changeSortStyle(sort, isLocal: isLocal);
|
||||
}),
|
||||
menu(isLocal: isLocal)
|
||||
menu(isLocal: isLocal),
|
||||
],
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black12)),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
prefix: Padding(padding: EdgeInsets.only(left: 4.0)),
|
||||
suffix: DropdownButton<String>(
|
||||
isDense: true,
|
||||
underline: Offstage(),
|
||||
items: [
|
||||
// TODO: favourite
|
||||
DropdownMenuItem(child: Text('/'), value: '/',)
|
||||
], onChanged: (path) {
|
||||
if (path is String && path.isNotEmpty){
|
||||
model.openDirectory(path, isLocal: isLocal);
|
||||
}
|
||||
})
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: isLocal
|
||||
? model.currentLocalDir.path
|
||||
: model.currentRemoteDir.path),
|
||||
onSubmitted: (path) {
|
||||
model.openDirectory(path, isLocal: isLocal);
|
||||
},
|
||||
))),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.refresh(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.refresh))
|
||||
],
|
||||
),
|
||||
Row(
|
||||
textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
DialogManager.show((setState, close) =>
|
||||
CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
model.createDir(
|
||||
PathUtil.join(
|
||||
model
|
||||
.getCurrentDir(isLocal)
|
||||
.path,
|
||||
name.value.text,
|
||||
model.getCurrentIsWindows(
|
||||
isLocal)),
|
||||
isLocal: isLocal);
|
||||
close();
|
||||
}
|
||||
},
|
||||
child: Text(translate("OK")))
|
||||
]));
|
||||
},
|
||||
icon: Icon(Icons.create_new_folder_outlined)),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final items = isLocal
|
||||
? _localSelectedItems
|
||||
: _remoteSelectedItems;
|
||||
debugPrint("remove items: ${items.items}");
|
||||
await (model.removeAction(items));
|
||||
items.clear();
|
||||
},
|
||||
icon: Icon(Icons.delete_forever_outlined)),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
final items = getSelectedItem(isLocal);
|
||||
model.sendFiles(items, isRemote: !isLocal);
|
||||
items.clear();
|
||||
},
|
||||
icon: Transform.rotate(
|
||||
angle: isLocal ? 0 : pi,
|
||||
child: Icon(
|
||||
Icons.send,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
isLocal ? translate('Send') : translate('Receive'),
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
),
|
||||
)),
|
||||
],
|
||||
).marginOnly(top: 8.0)
|
||||
],
|
||||
));
|
||||
|
||||
@ -476,14 +652,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
|
||||
Widget? bottomSheet() {
|
||||
final state = model.jobState;
|
||||
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
|
||||
final selectedItemsLen = "${_selectedItems.length} ${translate("items")}";
|
||||
final local = _selectedItems.isLocal == null
|
||||
final isOtherPage = _localSelectedItems.isOtherPage(model.isLocal);
|
||||
final selectedItemsLen =
|
||||
"${_localSelectedItems.length} ${translate("items")}";
|
||||
final local = _localSelectedItems.isLocal == null
|
||||
? ""
|
||||
: " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
|
||||
: " [${_localSelectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
|
||||
|
||||
if (model.selectMode) {
|
||||
if (_selectedItems.length == 0 || !isOtherPage) {
|
||||
if (_localSelectedItems.length == 0 || !isOtherPage) {
|
||||
return BottomSheetBody(
|
||||
leading: Icon(Icons.check),
|
||||
title: translate("Selected"),
|
||||
@ -497,8 +674,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_forever),
|
||||
onPressed: () {
|
||||
if (_selectedItems.length > 0) {
|
||||
model.removeAction(_selectedItems);
|
||||
if (_localSelectedItems.length > 0) {
|
||||
model.removeAction(_localSelectedItems);
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -518,7 +695,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
icon: Icon(Icons.paste),
|
||||
onPressed: () {
|
||||
model.toggleSelectMode();
|
||||
model.sendFiles(_selectedItems);
|
||||
model.sendFiles(_localSelectedItems);
|
||||
},
|
||||
)
|
||||
]);
|
||||
@ -576,6 +753,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
Widget getPlatformImage(String platform) {
|
||||
platform = platform.toLowerCase();
|
||||
if (platform == 'mac os')
|
||||
platform = 'mac';
|
||||
else if (platform != 'linux' && platform != 'android') platform = 'win';
|
||||
return Image.asset('assets/$platform.png', width: 25, height: 25);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheetBody extends StatelessWidget {
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as Path;
|
||||
|
||||
import 'model.dart';
|
||||
@ -22,6 +23,11 @@ class FileModel extends ChangeNotifier {
|
||||
|
||||
var _jobProgress = JobProgress(); // from rust update
|
||||
|
||||
/// JobTable <jobId, JobProgress>
|
||||
final _jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||
|
||||
RxList<JobProgress> get jobTable => _jobTable;
|
||||
|
||||
bool get isLocal => _isLocal;
|
||||
|
||||
bool get selectMode => _selectMode;
|
||||
@ -34,6 +40,20 @@ class FileModel extends ChangeNotifier {
|
||||
|
||||
SortBy get sortStyle => _sortStyle;
|
||||
|
||||
SortBy _localSortStyle = SortBy.Name;
|
||||
|
||||
bool _localSortAscending = true;
|
||||
|
||||
bool _remoteSortAscending = true;
|
||||
|
||||
SortBy _remoteSortStyle = SortBy.Name;
|
||||
|
||||
bool get localSortAscending => _localSortAscending;
|
||||
|
||||
SortBy getSortStyle(bool isLocal){
|
||||
return isLocal ? _localSortStyle : _remoteSortStyle;
|
||||
}
|
||||
|
||||
FileDirectory _currentLocalDir = FileDirectory();
|
||||
|
||||
FileDirectory get currentLocalDir => _currentLocalDir;
|
||||
@ -44,8 +64,20 @@ class FileModel extends ChangeNotifier {
|
||||
|
||||
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
|
||||
|
||||
FileDirectory getCurrentDir(bool isLocal) {
|
||||
return isLocal ? currentLocalDir : currentRemoteDir;
|
||||
}
|
||||
|
||||
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
|
||||
|
||||
String getCurrentHome(bool isLocal) {
|
||||
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, "");
|
||||
@ -78,9 +110,17 @@ class FileModel extends ChangeNotifier {
|
||||
bool get currentShowHidden =>
|
||||
_isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||
|
||||
bool getCurrentShowHidden(bool isLocal) {
|
||||
return isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||
}
|
||||
|
||||
bool get currentIsWindows =>
|
||||
_isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||
|
||||
bool getCurrentIsWindows(bool isLocal) {
|
||||
return isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||
}
|
||||
|
||||
final _fileFetcher = FileFetcher();
|
||||
|
||||
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||
@ -115,10 +155,23 @@ class FileModel extends ChangeNotifier {
|
||||
tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||
try {
|
||||
int id = int.parse(evt['id']);
|
||||
if (!isDesktop) {
|
||||
_jobProgress.id = id;
|
||||
_jobProgress.fileNum = int.parse(evt['file_num']);
|
||||
_jobProgress.speed = double.parse(evt['speed']);
|
||||
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
||||
} else {
|
||||
// Desktop uses jobTable
|
||||
// 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();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
||||
@ -126,49 +179,89 @@ 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);
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
finally {}
|
||||
}
|
||||
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
jobDone(Map<String, dynamic> evt) {
|
||||
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']);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
jobError(Map<String, dynamic> evt) {
|
||||
if (!isDesktop) {
|
||||
if (_jobResultListener.isListening) {
|
||||
_jobResultListener.complete(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("jobError $evt");
|
||||
_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");
|
||||
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) {
|
||||
@ -179,9 +272,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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,6 +309,8 @@ class FileModel extends ChangeNotifier {
|
||||
if (_currentRemoteDir.path.isEmpty) {
|
||||
openDirectory(_remoteOption.home, isLocal: false);
|
||||
}
|
||||
// load last transfer jobs
|
||||
await _ffi.target?.bind.sessionLoadLastTransferJobs(id: '${_ffi.target?.id}');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
@ -238,10 +333,10 @@ class FileModel extends ChangeNotifier {
|
||||
_remoteOption.clear();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
refresh({bool? isLocal}) {
|
||||
if (isDesktop) {
|
||||
openDirectory(currentRemoteDir.path);
|
||||
openDirectory(currentLocalDir.path);
|
||||
isLocal = isLocal ?? _isLocal;
|
||||
isLocal ? openDirectory(currentLocalDir.path) : openDirectory(currentRemoteDir.path);
|
||||
} else {
|
||||
openDirectory(currentDir.path);
|
||||
}
|
||||
@ -254,7 +349,7 @@ class FileModel extends ChangeNotifier {
|
||||
final isWindows =
|
||||
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||
// process /C:\ -> C:\ on Windows
|
||||
if (currentIsWindows && path.length > 1 && path[0] == '/') {
|
||||
if (isLocal ? _localOption.isWindows : _remoteOption.isWindows && path.length > 1 && path[0] == '/') {
|
||||
path = path.substring(1);
|
||||
if (path[path.length - 1] != '\\') {
|
||||
path = path + "\\";
|
||||
@ -270,19 +365,22 @@ class FileModel extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to openDirectory :$e");
|
||||
debugPrint("Failed to openDirectory ${path} :$e");
|
||||
}
|
||||
}
|
||||
|
||||
goHome() {
|
||||
openDirectory(currentHome);
|
||||
goHome({bool? isLocal}) {
|
||||
isLocal = isLocal ?? _isLocal;
|
||||
openDirectory(getCurrentHome(isLocal), isLocal: isLocal);
|
||||
}
|
||||
|
||||
goToParentDirectory({bool? isLocal}) {
|
||||
final currDir = isLocal != null ? isLocal ? currentLocalDir : currentRemoteDir : currentDir;
|
||||
var parent = PathUtil.dirname(currDir.path, currentIsWindows);
|
||||
isLocal = isLocal ?? _isLocal;
|
||||
final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||
final currDir = isLocal ? currentLocalDir : currentRemoteDir;
|
||||
var parent = PathUtil.dirname(currDir.path, isWindows);
|
||||
// specially for C:\, D:\, goto '/'
|
||||
if (parent == currDir.path && currentIsWindows) {
|
||||
if (parent == currDir.path && isWindows) {
|
||||
openDirectory('/', isLocal: isLocal);
|
||||
return;
|
||||
}
|
||||
@ -293,17 +391,24 @@ 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;
|
||||
isRemote ? currentLocalDir.path : currentRemoteDir.path;
|
||||
final isWindows =
|
||||
isRemote ? _localOption.isWindows : _remoteOption.isWindows;
|
||||
isRemote ? _remoteOption.isWindows : _localOption.isWindows;
|
||||
final showHidden =
|
||||
isRemote ? _localOption.showHidden : _remoteOption.showHidden ;
|
||||
isRemote ? _remoteOption.showHidden : _localOption.showHidden;
|
||||
items.items.forEach((from) async {
|
||||
_jobId++;
|
||||
await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows)
|
||||
final jobId = ++_jobId;
|
||||
_jobTable.add(JobProgress()
|
||||
..jobName = from.path
|
||||
..totalSize = from.size
|
||||
..state = JobState.inProgress
|
||||
..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);
|
||||
print("path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}");
|
||||
});
|
||||
} else {
|
||||
if (items.isLocal == null) {
|
||||
@ -514,33 +619,38 @@ 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 {
|
||||
createDir(String path, {bool? isLocal}) async {
|
||||
isLocal = isLocal ?? this.isLocal;
|
||||
_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();
|
||||
}
|
||||
|
||||
changeSortStyle(SortBy sort, {bool? isLocal}) {
|
||||
changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) {
|
||||
_sortStyle = sort;
|
||||
if (isLocal == null) {
|
||||
// compatible for mobile logic
|
||||
_currentLocalDir.changeSortStyle(sort);
|
||||
_currentRemoteDir.changeSortStyle(sort);
|
||||
_currentLocalDir.changeSortStyle(sort, ascending: ascending);
|
||||
_currentRemoteDir.changeSortStyle(sort, ascending: ascending);
|
||||
_localSortStyle = sort; _localSortAscending = ascending;
|
||||
_remoteSortStyle = sort; _remoteSortAscending = ascending;
|
||||
} else if (isLocal) {
|
||||
_currentLocalDir.changeSortStyle(sort);
|
||||
_currentLocalDir.changeSortStyle(sort, ascending: ascending);
|
||||
_localSortStyle = sort; _localSortAscending = ascending;
|
||||
} else {
|
||||
_currentRemoteDir.changeSortStyle(sort);
|
||||
_currentRemoteDir.changeSortStyle(sort, ascending: ascending);
|
||||
_remoteSortStyle = sort; _remoteSortAscending = ascending;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@ -548,6 +658,67 @@ 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}");
|
||||
}
|
||||
|
||||
bool get remoteSortAscending => _remoteSortAscending;
|
||||
|
||||
void loadLastJob(Map<String, dynamic> evt) {
|
||||
debugPrint("load last job: ${evt}");
|
||||
Map<String,dynamic> jobDetail = json.decode(evt['value']);
|
||||
// int id = int.parse(jobDetail['id']);
|
||||
String remote = jobDetail['remote'];
|
||||
String to = jobDetail['to'];
|
||||
bool showHidden = jobDetail['show_hidden'];
|
||||
int fileNum = jobDetail['file_num'];
|
||||
bool isRemote = jobDetail['is_remote'];
|
||||
final currJobId = _jobId++;
|
||||
var jobProgress = JobProgress()
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
..isRemote = isRemote
|
||||
..fileNum = fileNum
|
||||
..remote = remote
|
||||
..to = to
|
||||
..showHidden = showHidden
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
_ffi.target?.bind.sessionAddJob(id: '${_ffi.target?.id}',
|
||||
isRemote: isRemote,
|
||||
includeHidden: showHidden,
|
||||
actId: currJobId,
|
||||
path: isRemote ? remote : to,
|
||||
to: isRemote ? to: remote,
|
||||
fileNum: fileNum,
|
||||
);
|
||||
}
|
||||
|
||||
resumeJob(int jobId) {
|
||||
final jobIndex = getJob(jobId);
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
_ffi.target?.bind.sessionResumeJob(id: '${_ffi.target?.id}',
|
||||
actId: job.id,
|
||||
isRemote: job.isRemote);
|
||||
job.state = JobState.inProgress;
|
||||
} else {
|
||||
debugPrint("jobId ${jobId} is not exists");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class JobResultListener<T> {
|
||||
@ -717,8 +888,8 @@ class FileDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
changeSortStyle(SortBy sort) {
|
||||
entries = _sortList(entries, sort);
|
||||
changeSortStyle(SortBy sort, {bool ascending = true}) {
|
||||
entries = _sortList(entries, sort, ascending);
|
||||
}
|
||||
|
||||
clear() {
|
||||
@ -753,7 +924,24 @@ class Entry {
|
||||
}
|
||||
}
|
||||
|
||||
enum JobState { none, inProgress, done, error }
|
||||
enum JobState { none, inProgress, done, error, paused }
|
||||
|
||||
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;
|
||||
@ -761,6 +949,13 @@ class JobProgress {
|
||||
var fileNum = 0;
|
||||
var speed = 0.0;
|
||||
var finishedSize = 0;
|
||||
var totalSize = 0;
|
||||
var fileCount = 0;
|
||||
var isRemote = false;
|
||||
var jobName = "";
|
||||
var remote = "";
|
||||
var to = "";
|
||||
var showHidden = false;
|
||||
|
||||
clear() {
|
||||
state = JobState.none;
|
||||
@ -768,6 +963,10 @@ class JobProgress {
|
||||
fileNum = 0;
|
||||
speed = 0;
|
||||
finishedSize = 0;
|
||||
jobName = "";
|
||||
fileCount = 0;
|
||||
remote = "";
|
||||
to = "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -814,7 +1013,7 @@ class DirectoryOption {
|
||||
}
|
||||
|
||||
// code from file_manager pkg after edit
|
||||
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||
List<Entry> _sortList(List<Entry> list, SortBy sortType, bool ascending) {
|
||||
if (sortType == SortBy.Name) {
|
||||
// making list of only folders.
|
||||
final dirs = list.where((element) => element.isDirectory).toList();
|
||||
@ -827,7 +1026,7 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||
files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
|
||||
// first folders will go to list (if available) then files will go to list.
|
||||
return [...dirs, ...files];
|
||||
return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||
} else if (sortType == SortBy.Modified) {
|
||||
// making the list of Path & DateTime
|
||||
List<_PathStat> _pathStat = [];
|
||||
@ -842,7 +1041,7 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||
list.sort((a, b) => _pathStat
|
||||
.indexWhere((element) => element.path == a.name)
|
||||
.compareTo(_pathStat.indexWhere((element) => element.path == b.name)));
|
||||
return list;
|
||||
return ascending ? list : list.reversed.toList();
|
||||
} else if (sortType == SortBy.Type) {
|
||||
// making list of only folders.
|
||||
final dirs = list.where((element) => element.isDirectory).toList();
|
||||
@ -859,7 +1058,7 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||
.split('.')
|
||||
.last
|
||||
.compareTo(b.name.toLowerCase().split('.').last));
|
||||
return [...dirs, ...files];
|
||||
return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||
} else if (sortType == SortBy.Size) {
|
||||
// create list of path and size
|
||||
Map<String, int> _sizeMap = {};
|
||||
@ -884,7 +1083,7 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||
.indexWhere((element) => element.key == a.name)
|
||||
.compareTo(
|
||||
_sizeMapList.indexWhere((element) => element.key == b.name)));
|
||||
return [...dirs, ...files];
|
||||
return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -168,6 +168,10 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.overrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
parent.target?.fileModel.loadLastJob(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 +221,10 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.overrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
parent.target?.fileModel.loadLastJob(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') {
|
||||
|
@ -102,6 +102,10 @@ void wire_session_peer_option(int64_t port_,
|
||||
struct wire_uint_8_list *name,
|
||||
struct wire_uint_8_list *value);
|
||||
|
||||
void wire_session_get_peer_option(int64_t port_,
|
||||
struct wire_uint_8_list *id,
|
||||
struct wire_uint_8_list *name);
|
||||
|
||||
void wire_session_input_os_password(int64_t port_,
|
||||
struct wire_uint_8_list *id,
|
||||
struct wire_uint_8_list *value);
|
||||
@ -139,7 +143,8 @@ void wire_session_read_dir_recursive(int64_t port_,
|
||||
struct wire_uint_8_list *id,
|
||||
int32_t act_id,
|
||||
struct wire_uint_8_list *path,
|
||||
bool is_remote);
|
||||
bool is_remote,
|
||||
bool show_hidden);
|
||||
|
||||
void wire_session_remove_all_empty_dirs(int64_t port_,
|
||||
struct wire_uint_8_list *id,
|
||||
@ -197,6 +202,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_send_chat);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_send_mouse);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_peer_option);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_get_peer_option);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_input_os_password);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_read_remote_dir);
|
||||
dummy_var ^= ((int64_t) (void*) wire_session_send_files);
|
||||
|
@ -93,7 +93,7 @@ pub trait FileManager: Interface {
|
||||
}
|
||||
|
||||
fn add_job(
|
||||
&mut self,
|
||||
&self,
|
||||
id: i32,
|
||||
path: String,
|
||||
to: String,
|
||||
@ -111,7 +111,7 @@ pub trait FileManager: Interface {
|
||||
)));
|
||||
}
|
||||
|
||||
fn resume_job(&mut self, id: i32, is_remote: bool) {
|
||||
fn resume_job(&self, id: i32, is_remote: bool) {
|
||||
self.send(Data::ResumeJob((id, is_remote)));
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
154
src/flutter.rs
154
src/flutter.rs
@ -5,6 +5,8 @@ use std::{
|
||||
|
||||
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
|
||||
|
||||
use hbb_common::config::{PeerConfig, TransferSerde};
|
||||
use hbb_common::fs::{get_job, TransferJobMeta};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
compress::decompress,
|
||||
@ -27,7 +29,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();
|
||||
@ -464,6 +466,45 @@ impl Session {
|
||||
log::debug!("{:?}", msg_out);
|
||||
self.send_msg(msg_out);
|
||||
}
|
||||
|
||||
pub fn load_config(&self) -> PeerConfig {
|
||||
load_config(&self.id)
|
||||
}
|
||||
|
||||
pub fn save_config(&self, config: &PeerConfig) {
|
||||
config.store(&self.id);
|
||||
}
|
||||
|
||||
pub fn get_platform(&self, is_remote: bool) -> String {
|
||||
if is_remote {
|
||||
self.lc.read().unwrap().info.platform.clone()
|
||||
} else {
|
||||
whoami::platform().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_last_jobs(&self) {
|
||||
let pc = self.load_config();
|
||||
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
|
||||
// no last jobs
|
||||
return;
|
||||
}
|
||||
let mut cnt = 1;
|
||||
for job_str in pc.transfer.read_jobs.iter() {
|
||||
if !job_str.is_empty() {
|
||||
self.push_event("load_last_job", vec![("value", job_str)]);
|
||||
cnt += 1;
|
||||
println!("restore read_job: {:?}", job_str);
|
||||
}
|
||||
}
|
||||
for job_str in pc.transfer.write_jobs.iter() {
|
||||
if !job_str.is_empty() {
|
||||
self.push_event("load_last_job", vec![("value", job_str)]);
|
||||
cnt += 1;
|
||||
println!("restore write_job: {:?}", job_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileManager for Session {}
|
||||
@ -941,6 +982,7 @@ impl Connection {
|
||||
async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool {
|
||||
match data {
|
||||
Data::Close => {
|
||||
self.sync_jobs_status_to_local().await;
|
||||
return false;
|
||||
}
|
||||
Data::Login((password, remember)) => {
|
||||
@ -952,8 +994,7 @@ impl Connection {
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => {
|
||||
// in mobile, can_enable_override_detection is always true
|
||||
let od = true;
|
||||
let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version);
|
||||
if is_remote {
|
||||
log::debug!("New job {}, write to {} from remote {}", id, to, path);
|
||||
self.write_jobs.push(fs::TransferJob::new_write(
|
||||
@ -964,7 +1005,7 @@ impl Connection {
|
||||
include_hidden,
|
||||
is_remote,
|
||||
Vec::new(),
|
||||
true,
|
||||
od,
|
||||
));
|
||||
allow_err!(
|
||||
peer.send(&fs::new_send(id, path, file_num, include_hidden))
|
||||
@ -978,7 +1019,7 @@ impl Connection {
|
||||
file_num,
|
||||
include_hidden,
|
||||
is_remote,
|
||||
true,
|
||||
od,
|
||||
) {
|
||||
Err(err) => {
|
||||
self.handle_job_status(id, -1, Some(err.to_string()));
|
||||
@ -991,6 +1032,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);
|
||||
@ -1140,6 +1184,87 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
}
|
||||
Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => {
|
||||
let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version);
|
||||
if is_remote {
|
||||
log::debug!(
|
||||
"new write waiting job {}, write to {} from remote {}",
|
||||
id,
|
||||
to,
|
||||
path
|
||||
);
|
||||
let mut job = fs::TransferJob::new_write(
|
||||
id,
|
||||
path.clone(),
|
||||
to,
|
||||
file_num,
|
||||
include_hidden,
|
||||
is_remote,
|
||||
Vec::new(),
|
||||
od,
|
||||
);
|
||||
job.is_last_job = true;
|
||||
self.write_jobs.push(job);
|
||||
} else {
|
||||
match fs::TransferJob::new_read(
|
||||
id,
|
||||
to.clone(),
|
||||
path.clone(),
|
||||
file_num,
|
||||
include_hidden,
|
||||
is_remote,
|
||||
od,
|
||||
) {
|
||||
Err(err) => {
|
||||
self.handle_job_status(id, -1, Some(err.to_string()));
|
||||
}
|
||||
Ok(mut job) => {
|
||||
log::debug!(
|
||||
"new read waiting job {}, read {} to remote {}, {} files",
|
||||
id,
|
||||
path,
|
||||
to,
|
||||
job.files().len()
|
||||
);
|
||||
let m = make_fd_flutter(job.id(), job.files(), true);
|
||||
self.session
|
||||
.push_event("update_folder_files", vec![("info", &m)]);
|
||||
job.is_last_job = true;
|
||||
self.read_jobs.push(job);
|
||||
self.timer = time::interval(MILLI1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Data::ResumeJob((id, is_remote)) => {
|
||||
if is_remote {
|
||||
if let Some(job) = get_job(id, &mut self.write_jobs) {
|
||||
job.is_last_job = false;
|
||||
allow_err!(
|
||||
peer.send(&fs::new_send(
|
||||
id,
|
||||
job.remote.clone(),
|
||||
job.file_num,
|
||||
job.show_hidden
|
||||
))
|
||||
.await
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if let Some(job) = get_job(id, &mut self.read_jobs) {
|
||||
job.is_last_job = false;
|
||||
allow_err!(
|
||||
peer.send(&fs::new_receive(
|
||||
id,
|
||||
job.path.to_string_lossy().to_string(),
|
||||
job.file_num,
|
||||
job.files.clone()
|
||||
))
|
||||
.await
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
@ -1229,6 +1354,24 @@ impl Connection {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
async fn sync_jobs_status_to_local(&mut self) -> bool {
|
||||
log::info!("sync transfer job status");
|
||||
let mut config: PeerConfig = self.session.load_config();
|
||||
let mut transfer_metas = TransferSerde::default();
|
||||
for job in self.read_jobs.iter() {
|
||||
let json_str = serde_json::to_string(&job.gen_meta()).unwrap();
|
||||
transfer_metas.read_jobs.push(json_str);
|
||||
}
|
||||
for job in self.write_jobs.iter() {
|
||||
let json_str = serde_json::to_string(&job.gen_meta()).unwrap();
|
||||
transfer_metas.write_jobs.push(json_str);
|
||||
}
|
||||
log::info!("meta: {:?}", transfer_metas);
|
||||
config.transfer = transfer_metas;
|
||||
self.session.save_config(&config);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Server Side
|
||||
@ -1470,7 +1613,6 @@ pub mod connection_manager {
|
||||
mut files,
|
||||
} => {
|
||||
// in mobile, can_enable_override_detection is always true
|
||||
let od = true;
|
||||
WRITE_JOBS.lock().unwrap().push(fs::TransferJob::new_write(
|
||||
id,
|
||||
"".to_string(),
|
||||
|
@ -339,6 +339,45 @@ pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool)
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn session_get_platform(id: String, is_remote: bool) -> String {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
return session.get_platform(is_remote);
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn session_load_last_transfer_jobs(id: String) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
return session.load_last_jobs();
|
||||
} else {
|
||||
// a tip for flutter dev
|
||||
eprintln!(
|
||||
"cannot load last transfer job from non-existed session. Please ensure session \
|
||||
is connected before calling load last transfer jobs."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_add_job(
|
||||
id: String,
|
||||
act_id: i32,
|
||||
path: String,
|
||||
to: String,
|
||||
file_num: i32,
|
||||
include_hidden: bool,
|
||||
is_remote: bool,
|
||||
) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.add_job(act_id, path, to, file_num, include_hidden, is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.resume_job(act_id, is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
/// FFI for **get** commands which are idempotent.
|
||||
/// Return result in c string.
|
||||
///
|
||||
|
Loading…
x
Reference in New Issue
Block a user