opt: dual selected items & send/receive action icon

This commit is contained in:
Kingtous 2022-07-09 19:14:40 +08:00
parent 0598ee304c
commit 1db7fee6fb
2 changed files with 128 additions and 45 deletions

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
@ -23,7 +24,8 @@ class FileManagerPage extends StatefulWidget {
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final _selectedItems = SelectedItems(); final _localSelectedItems = SelectedItems();
final _remoteSelectedItems = SelectedItems();
final _breadCrumbLocalScroller = ScrollController(); final _breadCrumbLocalScroller = ScrollController();
final _breadCrumbRemoteScroller = ScrollController(); final _breadCrumbRemoteScroller = ScrollController();
@ -32,6 +34,10 @@ class _FileManagerPageState extends State<FileManagerPage>
FileModel get model => _ffi.fileModel; FileModel get model => _ffi.fileModel;
SelectedItems getSelectedItem(bool isLocal) {
return isLocal ? _localSelectedItems : _remoteSelectedItems;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -83,13 +89,6 @@ class _FileManagerPageState extends State<FileManagerPage>
})); }));
} }
bool needShowCheckBox() {
if (!model.selectMode) {
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
}
Widget menu({bool isLocal = false}) { Widget menu({bool isLocal = false}) {
return PopupMenuButton<String>( return PopupMenuButton<String>(
icon: Icon(Icons.more_vert), icon: Icon(Icons.more_vert),
@ -145,7 +144,7 @@ class _FileManagerPageState extends State<FileManagerPage>
if (v == "refresh") { if (v == "refresh") {
model.refresh(); model.refresh();
} else if (v == "select") { } else if (v == "select") {
_selectedItems.clear(); _localSelectedItems.clear();
model.toggleSelectMode(); model.toggleSelectMode();
} else if (v == "folder") { } else if (v == "folder") {
final name = TextEditingController(); final name = TextEditingController();
@ -223,8 +222,16 @@ class _FileManagerPageState extends State<FileManagerPage>
return DataRow( return DataRow(
key: ValueKey(entry.name), key: ValueKey(entry.name),
onSelectChanged: (s) { 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: [ cells: [
// TODO: icon // TODO: icon
DataCell(Icon( DataCell(Icon(
@ -240,6 +247,13 @@ class _FileManagerPageState extends State<FileManagerPage>
model.openDirectory(entry.path, isLocal: isLocal); model.openDirectory(entry.path, isLocal: isLocal);
} else { } else {
// Perform file-related tasks. // Perform file-related tasks.
final _selectedItems = getSelectedItem(isLocal);
if (_selectedItems.contains(entry)) {
_selectedItems.remove(entry);
} else {
_selectedItems.add(isLocal, entry);
}
setState((){});
} }
}), }),
DataCell(Text( DataCell(Text(
@ -377,6 +391,21 @@ class _FileManagerPageState extends State<FileManagerPage>
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), 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(
itemExtent: 100, itemBuilder: (BuildContext context, int index) {
final item = model.jobTable[index + 1];
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('${item.id}'),
Icon(Icons.delete)
],
);
},
itemCount: model.jobTable.length,
),
),
), ),
preferredSize: Size(200, double.infinity)); preferredSize: Size(200, double.infinity));
} }
@ -398,29 +427,46 @@ class _FileManagerPageState extends State<FileManagerPage>
Widget headTools(bool isLocal) => Container( Widget headTools(bool isLocal) => Container(
child: Row( child: Row(
children: [ children: [
Offstage(
offstage: isLocal,
child: TextButton.icon(
onPressed: (){}, icon: Transform.rotate(
angle: isLocal ? 0 : pi,
child: Icon(
Icons.send
),
), label: Text(isLocal ? translate('Send') : translate('Receive'))),
),
Expanded( Expanded(
child: BreadCrumb( child: Container(
items: getPathBreadCrumbItems(() => model.goHome(), (list) { decoration: BoxDecoration(
var path = ""; border: Border.all(color: Colors.black12)
if (model.currentHome.startsWith(list[0])) { ),
// absolute path child: BreadCrumb(
for (var item in list) { items: getPathBreadCrumbItems(() => model.goHome(isLocal: isLocal), (list) {
path = PathUtil.join(path, item, model.currentIsWindows); var path = "";
final currentHome = model.getCurrentHome(isLocal);
final currentIsWindows = model.getCurrentIsWindows(isLocal);
if (currentHome.startsWith(list[0])) {
// absolute path
for (var item in list) {
path = PathUtil.join(path, item, currentIsWindows);
}
} else {
path += currentHome;
for (var item in list) {
path = PathUtil.join(path, item, currentIsWindows);
}
} }
} else { model.openDirectory(path, isLocal: isLocal);
path += model.currentHome;
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
}
}
model.openDirectory(path, isLocal: isLocal);
}, isLocal), }, isLocal),
divider: Icon(Icons.chevron_right), divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow( overflow: ScrollableOverflow(
controller: isLocal controller: isLocal
? _breadCrumbLocalScroller ? _breadCrumbLocalScroller
: _breadCrumbRemoteScroller), : _breadCrumbRemoteScroller),
)), ),
)),
Row( Row(
children: [ children: [
IconButton( IconButton(
@ -443,8 +489,18 @@ class _FileManagerPageState extends State<FileManagerPage>
onSelected: (sort) { onSelected: (sort) {
model.changeSortStyle(sort, isLocal: isLocal); model.changeSortStyle(sort, isLocal: isLocal);
}), }),
menu(isLocal: isLocal) menu(isLocal: isLocal),
], ],
),
Offstage(
offstage: !isLocal,
child: TextButton.icon(
onPressed: (){}, icon: Transform.rotate(
angle: isLocal ? 0 : pi,
child: Icon(
Icons.send
),
), label: Text(isLocal ? translate('Send') : translate('Receive'))),
) )
], ],
)); ));
@ -476,14 +532,14 @@ class _FileManagerPageState extends State<FileManagerPage>
Widget? bottomSheet() { Widget? bottomSheet() {
final state = model.jobState; final state = model.jobState;
final isOtherPage = _selectedItems.isOtherPage(model.isLocal); final isOtherPage = _localSelectedItems.isOtherPage(model.isLocal);
final selectedItemsLen = "${_selectedItems.length} ${translate("items")}"; final selectedItemsLen = "${_localSelectedItems.length} ${translate("items")}";
final local = _selectedItems.isLocal == null final local = _localSelectedItems.isLocal == null
? "" ? ""
: " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; : " [${_localSelectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
if (model.selectMode) { if (model.selectMode) {
if (_selectedItems.length == 0 || !isOtherPage) { if (_localSelectedItems.length == 0 || !isOtherPage) {
return BottomSheetBody( return BottomSheetBody(
leading: Icon(Icons.check), leading: Icon(Icons.check),
title: translate("Selected"), title: translate("Selected"),
@ -497,8 +553,8 @@ class _FileManagerPageState extends State<FileManagerPage>
IconButton( IconButton(
icon: Icon(Icons.delete_forever), icon: Icon(Icons.delete_forever),
onPressed: () { onPressed: () {
if (_selectedItems.length > 0) { if (_localSelectedItems.length > 0) {
model.removeAction(_selectedItems); model.removeAction(_localSelectedItems);
} }
}, },
) )
@ -518,7 +574,7 @@ class _FileManagerPageState extends State<FileManagerPage>
icon: Icon(Icons.paste), icon: Icon(Icons.paste),
onPressed: () { onPressed: () {
model.toggleSelectMode(); model.toggleSelectMode();
model.sendFiles(_selectedItems); model.sendFiles(_localSelectedItems);
}, },
) )
]); ]);

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as Path; import 'package:path/path.dart' as Path;
import 'model.dart'; import 'model.dart';
@ -22,6 +23,11 @@ class FileModel extends ChangeNotifier {
var _jobProgress = JobProgress(); // from rust update 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 isLocal => _isLocal;
bool get selectMode => _selectMode; bool get selectMode => _selectMode;
@ -46,6 +52,10 @@ class FileModel extends ChangeNotifier {
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
String getCurrentHome(bool isLocal) {
return isLocal ? _localOption.home : _remoteOption.home;
}
String get currentShortPath { String get currentShortPath {
if (currentDir.path.startsWith(currentHome)) { if (currentDir.path.startsWith(currentHome)) {
var path = currentDir.path.replaceFirst(currentHome, ""); var path = currentDir.path.replaceFirst(currentHome, "");
@ -81,6 +91,10 @@ class FileModel extends ChangeNotifier {
bool get currentIsWindows => bool get currentIsWindows =>
_isLocal ? _localOption.isWindows : _remoteOption.isWindows; _isLocal ? _localOption.isWindows : _remoteOption.isWindows;
bool getCurrentIsWindows(bool isLocal) {
return isLocal ? _localOption.isWindows : _remoteOption.isWindows;
}
final _fileFetcher = FileFetcher(); final _fileFetcher = FileFetcher();
final _jobResultListener = JobResultListener<Map<String, dynamic>>(); final _jobResultListener = JobResultListener<Map<String, dynamic>>();
@ -115,10 +129,20 @@ class FileModel extends ChangeNotifier {
tryUpdateJobProgress(Map<String, dynamic> evt) { tryUpdateJobProgress(Map<String, dynamic> evt) {
try { try {
int id = int.parse(evt['id']); int id = int.parse(evt['id']);
_jobProgress.id = id; if (!isDesktop) {
_jobProgress.fileNum = int.parse(evt['file_num']); _jobProgress.id = id;
_jobProgress.speed = double.parse(evt['speed']); _jobProgress.fileNum = int.parse(evt['file_num']);
_jobProgress.finishedSize = int.parse(evt['finished_size']); _jobProgress.speed = double.parse(evt['speed']);
_jobProgress.finishedSize = int.parse(evt['finished_size']);
} else {
// Desktop uses jobTable
final job = _jobTable[id];
if (job != null) {
job.fileNum = int.parse(evt['file_num']);
job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']);
}
}
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
@ -270,12 +294,12 @@ class FileModel extends ChangeNotifier {
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint("Failed to openDirectory :$e"); debugPrint("Failed to openDirectory ${path} :$e");
} }
} }
goHome() { goHome({bool? isLocal}) {
openDirectory(currentHome); openDirectory(currentHome, isLocal: isLocal);
} }
goToParentDirectory({bool? isLocal}) { goToParentDirectory({bool? isLocal}) {
@ -303,7 +327,10 @@ class FileModel extends ChangeNotifier {
final showHidden = final showHidden =
isRemote ? _localOption.showHidden : _remoteOption.showHidden ; isRemote ? _localOption.showHidden : _remoteOption.showHidden ;
items.items.forEach((from) async { items.items.forEach((from) async {
_jobId++; final jobId = ++_jobId;
_jobTable[jobId] = JobProgress()
..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) await _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); ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote);
}); });