desktop file transfer ctrl + click multi selection
This commit is contained in:
parent
76581d46f2
commit
7e7214bd07
@ -31,6 +31,8 @@ const int kMobileMaxDisplayHeight = 1280;
|
|||||||
const int kDesktopMaxDisplayWidth = 1920;
|
const int kDesktopMaxDisplayWidth = 1920;
|
||||||
const int kDesktopMaxDisplayHeight = 1080;
|
const int kDesktopMaxDisplayHeight = 1080;
|
||||||
|
|
||||||
|
const int kDesktopDoubleClickTimeMilli = 200;
|
||||||
|
|
||||||
const Size kConnectionManagerWindowSize = Size(300, 400);
|
const Size kConnectionManagerWindowSize = Size(300, 400);
|
||||||
// Tabbar transition duration, now we remove the duration
|
// Tabbar transition duration, now we remove the duration
|
||||||
const Duration kTabTransitionDuration = Duration.zero;
|
const Duration kTabTransitionDuration = Duration.zero;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
@ -44,15 +46,17 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
|
|
||||||
final _locationStatusLocal = LocationStatus.bread.obs;
|
final _locationStatusLocal = LocationStatus.bread.obs;
|
||||||
final _locationStatusRemote = LocationStatus.bread.obs;
|
final _locationStatusRemote = LocationStatus.bread.obs;
|
||||||
final FocusNode _locationNodeLocal =
|
final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal");
|
||||||
FocusNode(debugLabel: "locationNodeLocal");
|
final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote");
|
||||||
final FocusNode _locationNodeRemote =
|
|
||||||
FocusNode(debugLabel: "locationNodeRemote");
|
|
||||||
final _searchTextLocal = "".obs;
|
final _searchTextLocal = "".obs;
|
||||||
final _searchTextRemote = "".obs;
|
final _searchTextRemote = "".obs;
|
||||||
final _breadCrumbScrollerLocal = ScrollController();
|
final _breadCrumbScrollerLocal = ScrollController();
|
||||||
final _breadCrumbScrollerRemote = ScrollController();
|
final _breadCrumbScrollerRemote = ScrollController();
|
||||||
|
|
||||||
|
/// [_lastClickTime], [_lastClickEntry] help to handle double click
|
||||||
|
int _lastClickTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
Entry? _lastClickEntry;
|
||||||
|
|
||||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||||
|
|
||||||
ScrollController getBreadCrumbScrollController(bool isLocal) {
|
ScrollController getBreadCrumbScrollController(bool isLocal) {
|
||||||
@ -171,22 +175,6 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget body({bool isLocal = false}) {
|
Widget body({bool isLocal = false}) {
|
||||||
final fd = model.getCurrentDir(isLocal);
|
|
||||||
final entries = fd.entries;
|
|
||||||
final sortIndex = (SortBy style) {
|
|
||||||
switch (style) {
|
|
||||||
case SortBy.Name:
|
|
||||||
return 0;
|
|
||||||
case SortBy.Type:
|
|
||||||
return 0;
|
|
||||||
case SortBy.Modified:
|
|
||||||
return 1;
|
|
||||||
case SortBy.Size:
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}(model.getSortStyle(isLocal));
|
|
||||||
final sortAscending =
|
|
||||||
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
||||||
margin: const EdgeInsets.all(16.0),
|
margin: const EdgeInsets.all(16.0),
|
||||||
@ -208,126 +196,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
child: ObxValue<RxString>(
|
child: _buildDataTable(context, isLocal),
|
||||||
(searchText) {
|
|
||||||
final filteredEntries = searchText.isNotEmpty
|
|
||||||
? entries.where((element) {
|
|
||||||
return element.name.contains(searchText.value);
|
|
||||||
}).toList(growable: false)
|
|
||||||
: entries;
|
|
||||||
return DataTable(
|
|
||||||
key: ValueKey(isLocal ? 0 : 1),
|
|
||||||
showCheckboxColumn: true,
|
|
||||||
dataRowHeight: 25,
|
|
||||||
headingRowHeight: 30,
|
|
||||||
horizontalMargin: 8,
|
|
||||||
columnSpacing: 8,
|
|
||||||
showBottomBorder: true,
|
|
||||||
sortColumnIndex: sortIndex,
|
|
||||||
sortAscending: sortAscending,
|
|
||||||
columns: [
|
|
||||||
DataColumn(
|
|
||||||
label: Text(
|
|
||||||
translate("Name"),
|
|
||||||
).marginSymmetric(horizontal: 4),
|
|
||||||
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: filteredEntries.map((entry) {
|
|
||||||
final sizeStr = entry.isFile
|
|
||||||
? readableFileSize(entry.size.toDouble())
|
|
||||||
: "";
|
|
||||||
return DataRow(
|
|
||||||
key: ValueKey(entry.name),
|
|
||||||
onSelectChanged: (s) {
|
|
||||||
if (s != null) {
|
|
||||||
if (s) {
|
|
||||||
getSelectedItem(isLocal)
|
|
||||||
.add(isLocal, entry);
|
|
||||||
} else {
|
|
||||||
getSelectedItem(isLocal).remove(entry);
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected:
|
|
||||||
getSelectedItem(isLocal).contains(entry),
|
|
||||||
cells: [
|
|
||||||
DataCell(
|
|
||||||
Container(
|
|
||||||
width: 180,
|
|
||||||
child: Tooltip(
|
|
||||||
message: entry.name,
|
|
||||||
child: Row(children: [
|
|
||||||
Icon(
|
|
||||||
entry.isFile
|
|
||||||
? Icons.feed_outlined
|
|
||||||
: Icons.folder,
|
|
||||||
size: 20,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.iconTheme
|
|
||||||
.color
|
|
||||||
?.withOpacity(0.7),
|
|
||||||
).marginSymmetric(horizontal: 2),
|
|
||||||
Expanded(
|
|
||||||
child: Text(entry.name,
|
|
||||||
overflow:
|
|
||||||
TextOverflow.ellipsis))
|
|
||||||
]),
|
|
||||||
)), onTap: () {
|
|
||||||
if (entry.isDirectory) {
|
|
||||||
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(FittedBox(
|
|
||||||
child: Text(
|
|
||||||
"${entry.lastModified().toString().replaceAll(".000", "")} ",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12, color: MyTheme.darkGray),
|
|
||||||
))),
|
|
||||||
DataCell(Text(
|
|
||||||
sizeStr,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10, color: MyTheme.darkGray),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
}).toList(growable: false),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isLocal ? _searchTextLocal : _searchTextRemote,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -337,6 +206,176 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDataTable(BuildContext context, bool isLocal) {
|
||||||
|
final fd = model.getCurrentDir(isLocal);
|
||||||
|
final entries = fd.entries;
|
||||||
|
final sortIndex = (SortBy style) {
|
||||||
|
switch (style) {
|
||||||
|
case SortBy.Name:
|
||||||
|
return 0;
|
||||||
|
case SortBy.Type:
|
||||||
|
return 0;
|
||||||
|
case SortBy.Modified:
|
||||||
|
return 1;
|
||||||
|
case SortBy.Size:
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}(model.getSortStyle(isLocal));
|
||||||
|
final sortAscending =
|
||||||
|
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
||||||
|
|
||||||
|
return ObxValue<RxString>(
|
||||||
|
(searchText) {
|
||||||
|
final filteredEntries = searchText.isNotEmpty
|
||||||
|
? entries.where((element) {
|
||||||
|
return element.name.contains(searchText.value);
|
||||||
|
}).toList(growable: false)
|
||||||
|
: entries;
|
||||||
|
return DataTable(
|
||||||
|
key: ValueKey(isLocal ? 0 : 1),
|
||||||
|
showCheckboxColumn: false,
|
||||||
|
dataRowHeight: 25,
|
||||||
|
headingRowHeight: 30,
|
||||||
|
horizontalMargin: 8,
|
||||||
|
columnSpacing: 8,
|
||||||
|
showBottomBorder: true,
|
||||||
|
sortColumnIndex: sortIndex,
|
||||||
|
sortAscending: sortAscending,
|
||||||
|
columns: [
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
translate("Name"),
|
||||||
|
).marginSymmetric(horizontal: 4),
|
||||||
|
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: filteredEntries.map((entry) {
|
||||||
|
final sizeStr =
|
||||||
|
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
||||||
|
final lastModifiedStr =
|
||||||
|
"${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||||
|
return DataRow(
|
||||||
|
key: ValueKey(entry.name),
|
||||||
|
onSelectChanged: (s) {
|
||||||
|
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||||
|
.contains(LogicalKeyboardKey.controlLeft);
|
||||||
|
final items = getSelectedItem(isLocal);
|
||||||
|
if (isCtrlDown) {
|
||||||
|
if (s != null) {
|
||||||
|
if (s) {
|
||||||
|
items.add(isLocal, entry);
|
||||||
|
} else {
|
||||||
|
items.remove(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.clear();
|
||||||
|
items.add(isLocal, entry);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
selected: getSelectedItem(isLocal).contains(entry),
|
||||||
|
cells: [
|
||||||
|
DataCell(
|
||||||
|
Container(
|
||||||
|
width: 200,
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: entry.name,
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(
|
||||||
|
entry.isFile ? Icons.feed_outlined : Icons.folder,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.iconTheme
|
||||||
|
.color
|
||||||
|
?.withOpacity(0.7),
|
||||||
|
).marginSymmetric(horizontal: 2),
|
||||||
|
Expanded(
|
||||||
|
child: Text(entry.name,
|
||||||
|
overflow: TextOverflow.ellipsis))
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
onTap: () {
|
||||||
|
final items = getSelectedItem(isLocal);
|
||||||
|
|
||||||
|
// handle double click
|
||||||
|
if (_checkDoubleClick(entry)) {
|
||||||
|
openDirectory(entry.path, isLocal: isLocal);
|
||||||
|
items.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||||
|
.contains(LogicalKeyboardKey.controlLeft);
|
||||||
|
if (isCtrlDown) {
|
||||||
|
if (items.contains(entry)) {
|
||||||
|
items.remove(entry);
|
||||||
|
} else {
|
||||||
|
items.add(isLocal, entry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.clear();
|
||||||
|
items.add(isLocal, entry);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataCell(FittedBox(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: lastModifiedStr,
|
||||||
|
child: Text(
|
||||||
|
lastModifiedStr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: MyTheme.darkGray),
|
||||||
|
)))),
|
||||||
|
DataCell(Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: sizeStr,
|
||||||
|
child: Text(
|
||||||
|
sizeStr,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 10, color: MyTheme.darkGray),
|
||||||
|
))),
|
||||||
|
]);
|
||||||
|
}).toList(growable: false),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isLocal ? _searchTextLocal : _searchTextRemote,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkDoubleClick(Entry entry) {
|
||||||
|
final current = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final elapsed = current - _lastClickTime;
|
||||||
|
_lastClickTime = current;
|
||||||
|
if (_lastClickEntry == entry) {
|
||||||
|
if (elapsed < kDesktopDoubleClickTimeMilli) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_lastClickEntry = entry;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// transfer status list
|
/// transfer status list
|
||||||
/// watch transfer status
|
/// watch transfer status
|
||||||
Widget statusList() {
|
Widget statusList() {
|
||||||
@ -369,6 +408,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Tooltip(
|
Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
message: item.jobName,
|
message: item.jobName,
|
||||||
child: Text(
|
child: Text(
|
||||||
item.jobName,
|
item.jobName,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user