Merge pull request #1719 from Heap-Hop/opt/file_transfer

opt file transfer
This commit is contained in:
RustDesk 2022-10-13 21:09:18 +08:00 committed by GitHub
commit 85b4758d93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 564 additions and 403 deletions

View File

@ -12,13 +12,6 @@ import '../../models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
class CustomPopupMenuTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 3.0;
}
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
Function(BuildContext);

View File

@ -232,11 +232,11 @@ class _GeneralState extends State<_General> {
controller: scrollController,
children: [
theme(),
abr(),
hwcodec(),
audio(context),
record(context),
_Card(title: 'Language', children: [language()]),
other()
],
).marginOnly(bottom: _kListViewBottomMargin));
}
@ -267,8 +267,10 @@ class _GeneralState extends State<_General> {
]);
}
Widget abr() {
return _Card(title: 'Adaptive Bitrate', children: [
Widget other() {
return _Card(title: 'Other', children: [
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs'),
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
]);
}

View File

@ -9,12 +9,25 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/popup_menu.dart';
enum LocationStatus { bread, textField }
/// status of location bar
enum LocationStatus {
/// normal bread crumb bar
bread,
/// show path text field
pathLocation,
/// show file search bar text field
fileSearchBar
}
class FileManagerPage extends StatefulWidget {
const FileManagerPage({Key? key, required this.id}) : super(key: key);
@ -40,7 +53,7 @@ class _FileManagerPageState extends State<FileManagerPage>
final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController();
final _dropMaskVisible = false.obs;
final _dropMaskVisible = false.obs; // TODO impl drop mask
ScrollController getBreadCrumbScrollController(bool isLocal) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
@ -84,6 +97,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Get.delete<FFI>(tag: 'ft_${widget.id}');
_locationNodeLocal.removeListener(onLocalLocationFocusChanged);
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
_locationNodeLocal.dispose();
_locationNodeRemote.dispose();
super.dispose();
}
@ -95,56 +110,64 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.dialogManager.setOverlayState(Overlay.of(context));
return ChangeNotifierProvider.value(
value: _ffi.fileModel,
child: Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
}
return false;
},
child: Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: Row(
children: [
Flexible(flex: 3, child: body(isLocal: true)),
Flexible(flex: 3, child: body(isLocal: false)),
Flexible(flex: 2, child: statusList())
],
),
));
child: Consumer<FileModel>(builder: (context, model, child) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: Row(
children: [
Flexible(flex: 3, child: body(isLocal: true)),
Flexible(flex: 3, child: body(isLocal: false)),
Flexible(flex: 2, child: statusList())
],
),
);
}));
})
]);
}
Widget menu({bool isLocal = false}) {
return PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
splashRadius: 20,
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: [
Icon(
model.getCurrentShowHidden(isLocal)
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: Colors.black),
SizedBox(width: 5),
Text(translate("Show Hidden Files"))
],
),
value: "hidden",
)
];
var menuPos = RelativeRect.fill;
final items = [
MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate("Show Hidden Files"),
getter: () async {
return model.getCurrentShowHidden(isLocal);
},
onSelected: (v) {
if (v == "hidden") {
model.toggleShowHidden(local: isLocal);
}
});
setter: (bool v) async {
model.toggleShowHidden(local: isLocal);
},
padding: kDesktopMenuPadding,
dismissOnClicked: true,
),
];
return Listener(
onPointerDown: (e) {
final x = e.position.dx;
final y = e.position.dy;
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
child: IconButton(
icon: const Icon(Icons.more_vert),
splashRadius: 20,
onPressed: () => mod_menu.showMenu(
context: context,
position: menuPos,
items: items
.map((e) => e.build(
context,
MenuConfig(
commonColor: CustomPopupMenuTheme.commonColor,
height: CustomPopupMenuTheme.height,
dividerHeight: CustomPopupMenuTheme.dividerHeight)))
.expand((i) => i)
.toList(),
elevation: 8,
),
));
}
Widget body({bool isLocal = false}) {
@ -153,13 +176,13 @@ class _FileManagerPageState extends State<FileManagerPage>
final sortIndex = (SortBy style) {
switch (style) {
case SortBy.Name:
return 1;
return 0;
case SortBy.Type:
return 0;
case SortBy.Modified:
return 2;
return 1;
case SortBy.Size:
return 3;
return 2;
}
}(model.getSortStyle(isLocal));
final sortAscending =
@ -187,13 +210,9 @@ class _FileManagerPageState extends State<FileManagerPage>
controller: ScrollController(),
child: ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isEmpty
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
if (searchText.isEmpty) {
return true;
} else {
return element.name.contains(searchText.value);
}
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
@ -201,16 +220,16 @@ class _FileManagerPageState extends State<FileManagerPage>
showCheckboxColumn: true,
dataRowHeight: 25,
headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8,
showBottomBorder: true,
sortColumnIndex: sortIndex,
sortAscending: sortAscending,
columns: [
DataColumn(label: Text(translate(" "))), // icon
DataColumn(
label: Text(
translate("Name"),
),
).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Name,
isLocal: isLocal, ascending: ascending);
@ -250,19 +269,27 @@ class _FileManagerPageState extends State<FileManagerPage>
selected:
getSelectedItem(isLocal).contains(entry),
cells: [
DataCell(Icon(
entry.isFile
? Icons.feed_outlined
: Icons.folder,
size: 25)),
DataCell(
ConstrainedBox(
constraints:
BoxConstraints(maxWidth: 100),
Container(
width: 180,
child: Tooltip(
message: entry.name,
child: Text(entry.name,
overflow: TextOverflow.ellipsis),
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);
@ -273,29 +300,27 @@ class _FileManagerPageState extends State<FileManagerPage>
}
} else {
// Perform file-related tasks.
final _selectedItems =
final selectedItems =
getSelectedItem(isLocal);
if (_selectedItems.contains(entry)) {
_selectedItems.remove(entry);
if (selectedItems.contains(entry)) {
selectedItems.remove(entry);
} else {
_selectedItems.add(isLocal, entry);
selectedItems.add(isLocal, entry);
}
setState(() {});
}
}),
DataCell(Text(
entry
.lastModified()
.toString()
.replaceAll(".000", "") +
" ",
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: 12, color: MyTheme.darkGray),
fontSize: 10, color: MyTheme.darkGray),
)),
]);
}).toList(growable: false),
@ -410,15 +435,10 @@ class _FileManagerPageState extends State<FileManagerPage>
));
}
goBack({bool? isLocal}) {
model.goToParentDirectory(isLocal: isLocal);
}
Widget headTools(bool isLocal) {
final _locationStatus =
final locationStatus =
isLocal ? _locationStatusLocal : _locationStatusRemote;
final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
return Container(
child: Column(
children: [
@ -463,77 +483,83 @@ class _FileManagerPageState extends State<FileManagerPage>
icon: const Icon(Icons.home_outlined),
splashRadius: 20,
),
IconButton(
icon: const Icon(Icons.arrow_back),
splashRadius: 20,
onPressed: () {
model.goBack(isLocal: isLocal);
},
),
IconButton(
icon: const Icon(Icons.arrow_upward),
splashRadius: 20,
onPressed: () {
goBack(isLocal: isLocal);
model.goToParentDirectory(isLocal: isLocal);
},
),
menu(isLocal: isLocal),
],
),
Expanded(
child: GestureDetector(
onTap: () {
_locationStatus.value =
_locationStatus.value == LocationStatus.bread
? LocationStatus.textField
locationStatus.value =
locationStatus.value == LocationStatus.bread
? LocationStatus.pathLocation
: LocationStatus.bread;
Future.delayed(Duration.zero, () {
if (_locationStatus.value == LocationStatus.textField) {
_locationFocus.requestFocus();
if (locationStatus.value == LocationStatus.pathLocation) {
locationFocus.requestFocus();
}
});
},
child: Container(
decoration:
BoxDecoration(border: Border.all(color: Colors.black12)),
child: Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: locationStatus.value == LocationStatus.bread
? Colors.black12
: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5))),
child: Row(
children: [
Expanded(
child: Obx(() =>
_locationStatus.value == LocationStatus.bread
? buildBread(isLocal)
: buildPathLocation(isLocal))),
DropdownButton<String>(
isDense: true,
underline: Offstage(),
items: [
// TODO: favourite
DropdownMenuItem(
child: Text('/'),
value: '/',
)
],
onChanged: (path) {
if (path is String && path.isNotEmpty) {
openDirectory(path, isLocal: isLocal);
}
})
child: locationStatus.value == LocationStatus.bread
? buildBread(isLocal)
: buildPathLocation(isLocal)),
],
)),
))),
)),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 200),
child: TextField(
controller:
TextEditingController(text: _searchTextObs.value),
autofocus: true,
decoration:
InputDecoration(prefixIcon: Icon(Icons.search)),
onChanged: (searchText) =>
onSearchText(searchText, isLocal),
),
))
],
splashRadius: 20,
child: const Icon(Icons.search),
),
Obx(() {
switch (locationStatus.value) {
case LocationStatus.bread:
return IconButton(
onPressed: () {
locationStatus.value = LocationStatus.fileSearchBar;
final focusNode =
isLocal ? _locationNodeLocal : _locationNodeRemote;
Future.delayed(
Duration.zero, () => focusNode.requestFocus());
},
splashRadius: 20,
icon: Icon(Icons.search));
case LocationStatus.pathLocation:
return IconButton(
color: Theme.of(context).disabledColor,
onPressed: null,
splashRadius: 20,
icon: Icon(Icons.close));
case LocationStatus.fileSearchBar:
return IconButton(
color: Theme.of(context).disabledColor,
onPressed: () {
onSearchText("", isLocal);
locationStatus.value = LocationStatus.bread;
},
splashRadius: 1,
icon: Icon(Icons.close));
}
}),
IconButton(
onPressed: () {
model.refresh(isLocal: isLocal);
@ -609,6 +635,7 @@ class _FileManagerPageState extends State<FileManagerPage>
},
splashRadius: 20,
icon: const Icon(Icons.delete_forever_outlined)),
menu(isLocal: isLocal),
],
),
),
@ -642,7 +669,9 @@ class _FileManagerPageState extends State<FileManagerPage>
// ignore
} else {
// lost focus, change to bread
_locationStatusLocal.value = LocationStatus.bread;
if (_locationStatusLocal.value != LocationStatus.fileSearchBar) {
_locationStatusLocal.value = LocationStatus.bread;
}
}
}
@ -652,7 +681,9 @@ class _FileManagerPageState extends State<FileManagerPage>
// ignore
} else {
// lost focus, change to bread
_locationStatusRemote.value = LocationStatus.bread;
if (_locationStatusRemote.value != LocationStatus.fileSearchBar) {
_locationStatusRemote.value = LocationStatus.bread;
}
}
}
@ -664,14 +695,33 @@ class _FileManagerPageState extends State<FileManagerPage>
}
openDirectory(path, isLocal: isLocal);
});
breadCrumbScrollToEnd(isLocal);
return items.isEmpty
? Offstage()
: BreadCrumb(
items: items,
divider: Text("/").paddingSymmetric(horizontal: 4.0),
overflow: ScrollableOverflow(
controller: getBreadCrumbScrollController(isLocal)),
);
: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Expanded(
child: BreadCrumb(
items: items,
divider: Text("/").paddingSymmetric(horizontal: 4.0),
overflow: ScrollableOverflow(
controller: getBreadCrumbScrollController(isLocal)),
)),
DropdownButton<String>(
isDense: true,
underline: Offstage(),
items: [
// TODO: favourite
DropdownMenuItem(
child: Text('/'),
value: '/',
)
],
onChanged: (path) {
if (path is String && path.isNotEmpty) {
openDirectory(path, isLocal: isLocal);
}
})
]);
}
List<BreadCrumbItem> getPathBreadCrumbItems(
@ -690,28 +740,49 @@ class _FileManagerPageState extends State<FileManagerPage>
breadCrumbScrollToEnd(bool isLocal) {
Future.delayed(Duration(milliseconds: 200), () {
final _breadCrumbScroller = getBreadCrumbScrollController(isLocal);
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
final breadCrumbScroller = getBreadCrumbScrollController(isLocal);
if (breadCrumbScroller.hasClients) {
breadCrumbScroller.animateTo(
breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
}
});
}
Widget buildPathLocation(bool isLocal) {
return TextField(
focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote,
decoration: InputDecoration(
border: InputBorder.none,
isDense: true,
prefix: Padding(padding: EdgeInsets.only(left: 4.0)),
),
controller:
TextEditingController(text: model.getCurrentDir(isLocal).path),
onSubmitted: (path) {
openDirectory(path, isLocal: isLocal);
},
);
final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
final locationStatus =
isLocal ? _locationStatusLocal : _locationStatusRemote;
final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote;
final text = locationStatus.value == LocationStatus.pathLocation
? model.getCurrentDir(isLocal).path
: searchTextObs.value;
final textController = TextEditingController(text: text)
..selection = TextSelection.collapsed(offset: text.length);
return Row(children: [
Icon(
locationStatus.value == LocationStatus.pathLocation
? Icons.folder
: Icons.search,
color: Theme.of(context).hintColor,
).paddingSymmetric(horizontal: 2),
Expanded(
child: TextField(
focusNode: focusNode,
decoration: InputDecoration(
border: InputBorder.none,
isDense: true,
prefix: Padding(padding: EdgeInsets.only(left: 4.0))),
controller: textController,
onSubmitted: (path) {
openDirectory(path, isLocal: isLocal);
},
onChanged: locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal)
: null,
))
]);
}
onSearchText(String searchText, bool isLocal) {
@ -734,7 +805,7 @@ class _FileManagerPageState extends State<FileManagerPage>
return;
}
var items = SelectedItems();
details.files.forEach((file) {
for (var file in details.files) {
final f = File(file.path);
items.add(
true,
@ -743,7 +814,7 @@ class _FileManagerPageState extends State<FileManagerPage>
..name = file.name
..size =
FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
});
}
model.sendFiles(items, isRemote: false);
}
}

View File

@ -10,7 +10,7 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import '../../mobile/widgets/dialog.dart';
import '../../models/platform_model.dart';
/// File Transfer for multi tabs
class FileManagerTabPage extends StatefulWidget {
@ -35,7 +35,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(params['id']),
onTabCloseButton: () => () => tabController.closeBy(params['id']),
page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
}
@ -58,7 +58,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(id),
onTabCloseButton: () => tabController.closeBy(id),
page: FileManagerPage(key: ValueKey(id), id: id)));
} else if (call.method == "onDestroy") {
tabController.clear();
@ -98,26 +98,19 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
return widget.params["windowId"];
}
void handleTabCloseButton(String peerId) {
final session = ffi('ft_$peerId');
if (session.ffiModel.pi.hostname.isNotEmpty) {
tabController.jumpBy(peerId);
clientClose(session.dialogManager);
} else {
tabController.closeBy(peerId);
}
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length;
if (connLength < 1) {
if (connLength <= 1) {
tabController.clear();
return true;
} else if (connLength == 1) {
final currentConn = tabController.state.value.tabs[0];
handleTabCloseButton(currentConn.key);
return false;
} else {
final res = await closeConfirmDialog();
final opt = "enable-confirm-closing-tabs";
final bool res;
if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
}

View File

@ -12,7 +12,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../../mobile/widgets/dialog.dart';
import '../../models/platform_model.dart';
class ConnectionTabPage extends StatefulWidget {
final Map<String, dynamic> params;
@ -42,7 +42,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: peerId,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(peerId),
onTabCloseButton: () => tabController.closeBy(peerId),
page: Obx(() => RemotePage(
key: ValueKey(peerId),
id: peerId,
@ -78,7 +78,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(id),
onTabCloseButton: () => tabController.closeBy(id),
page: Obx(() => RemotePage(
key: ValueKey(id),
id: id,
@ -173,29 +173,21 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
return widget.params["windowId"];
}
void handleTabCloseButton(String peerId) {
final session = ffi(peerId);
if (session.ffiModel.pi.hostname.isNotEmpty) {
tabController.jumpBy(peerId);
clientClose(session.dialogManager);
} else {
tabController.closeBy(peerId);
}
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength < 1) {
if (connLength <= 1) {
tabController.clear();
return true;
} else if (connLength == 1) {
final currentConn = tabController.state.value.tabs[0];
handleTabCloseButton(currentConn.key);
return false;
} else {
final res = await closeConfirmDialog();
final opt = "enable-confirm-closing-tabs";
final bool res;
if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
_update_remote_count();
}
return res;
}

View File

@ -407,7 +407,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
setState(() {
client.recording = enabled;
});
}, translate('Allow reco)rding session'))
}, translate('Allow recording session'))
],
)),
],

View File

@ -3,6 +3,7 @@ import 'dart:core';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
import './material_mod_popup_menu.dart' as mod_menu;
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
@ -637,3 +638,10 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
];
}
}
class CustomPopupMenuTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 3.0;
}

View File

@ -456,8 +456,15 @@ class WindowActionPanel extends StatelessWidget {
}
Future<bool> closeConfirmDialog() async {
var confirm = true;
final res = await gFFI.dialogManager.show<bool>((setState, close) {
submit() => close(true);
submit() {
final opt = "enable-confirm-closing-tabs";
String value = bool2option(opt, confirm);
bind.mainSetOption(key: opt, value: value);
close(true);
}
return CustomAlertDialog(
title: Row(children: [
const Icon(Icons.warning_amber_sharp,
@ -465,7 +472,25 @@ Future<bool> closeConfirmDialog() async {
const SizedBox(width: 10),
Text(translate("Warning")),
]),
content: Text(translate("Disconnect all devices?")),
content: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("Disconnect all devices?")),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Confirm before closing multiple tabs"),
),
value: confirm,
onChanged: (v) {
if (v == null) return;
setState(() => confirm = v);
},
)
]), // confirm checkbox
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),

View File

@ -16,12 +16,15 @@ class FileModel extends ChangeNotifier {
var _isLocal = false;
var _selectMode = false;
var _localOption = DirectoryOption();
var _remoteOption = DirectoryOption();
final _localOption = DirectoryOption();
final _remoteOption = DirectoryOption();
List<String> localHistory = [];
List<String> remoteHistory = [];
var _jobId = 0;
var _jobProgress = JobProgress(); // from rust update
final _jobProgress = JobProgress(); // from rust update
/// JobTable <jobId, JobProgress>
final _jobTable = List<JobProgress>.empty(growable: true).obs;
@ -368,8 +371,11 @@ class FileModel extends ChangeNotifier {
}
}
openDirectory(String path, {bool? isLocal}) async {
openDirectory(String path, {bool? isLocal, bool isBack = false}) async {
isLocal = isLocal ?? _isLocal;
if (!isBack) {
pushHistory(isLocal);
}
final showHidden =
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
final isWindows =
@ -397,11 +403,34 @@ class FileModel extends ChangeNotifier {
}
}
void pushHistory(bool isLocal) {
final history = isLocal ? localHistory : remoteHistory;
final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path;
if (history.isNotEmpty && history.last == currPath) {
return;
}
history.add(currPath);
}
goHome({bool? isLocal}) {
isLocal = isLocal ?? _isLocal;
openDirectory(getCurrentHome(isLocal), isLocal: isLocal);
}
goBack({bool? isLocal}) {
isLocal = isLocal ?? _isLocal;
final history = isLocal ? localHistory : remoteHistory;
if (history.isEmpty) return;
final path = history.removeAt(history.length - 1);
if (path.isEmpty) return;
final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path;
if (currPath == path) {
goBack(isLocal: isLocal);
return;
}
openDirectory(path, isLocal: isLocal, isBack: true);
}
goToParentDirectory({bool? isLocal}) {
isLocal = isLocal ?? _isLocal;
final isWindows =
@ -685,6 +714,8 @@ class FileModel extends ChangeNotifier {
}
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
final history = isLocal ? localHistory : remoteHistory;
history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs(
id: '${parent.target?.id}',
actId: _jobId,

File diff suppressed because it is too large Load Diff

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", "暂时无法访问远端设备因为远端设备正在请求用户账户权限请等待对方关闭UAC窗口。为避免这个问题建议在远端设备上安装或者以管理员权限运行本软件。"),
("elevated_foreground_window_warning", "暂时无法使用鼠标键盘,因为远端桌面的当前窗口需要更高的权限才能操作, 可以请求对方最小化当前窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"),
("Disconnected", "会话已结束"),
("Other", "其他"),
("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", "Afbrudt"),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -193,7 +193,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reboot required", "Redémarrage pour prendre effet"),
("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"),
("x11 expected", "Veuillez passer à x11"),
("Port", ""),
("Port", "Port"),
("Settings", "Paramètres"),
("Username", " Nom d'utilisateur"),
("Invalid port", "Port invalide"),
@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."),
("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."),
("android_start_service_tip", "Appuyez sur [Démarrer le service] ou sur l'autorisation OUVRIR [Capture d'écran] pour démarrer le service de partage d'écran."),
("Account", ""),
("Account", "Compte"),
("Overwrite", "Écraser"),
("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"),
("Quit", "Quitter"),
@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", "他の"),
("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", "Ostrzeżenie UAC"),
("elevated_foreground_window_warning", "Pierwszoplanowe okno ostrzeżenia o podwyższeniu uprawnień"),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", "暂时无法访问远端设备因为远端设备正在请求用户账户权限请等待对方关闭UAC窗口。为避免这个问题建议在远端设备上安装或者以管理员权限运行本软件。"),
("elevated_foreground_window_warning", "暫時無法使用鼠標鍵盤,因為遠端桌面的當前窗口需要更高的權限才能操作, 可以請求對方最小化當前窗口。為避免這個問題,建議在遠端設備上安裝或者以管理員權限運行本軟件。"),
("Disconnected", "會話已結束"),
("Other", "其他"),
("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"),
].iter().cloned().collect();
}

View File

@ -191,9 +191,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Warning", "Попередження"),
("Login screen using Wayland is not supported", "Вхід у систему з використанням Wayland не підтримується"),
("Reboot required", "Потрібне перезавантаження"),
("Unsupported display server", "Непідтримуваний сервер дисплея"),
("Unsupported display server ", ""),
("x11 expected", "Очікується X11"),
("Port", ""),
("Port", "Порт"),
("Settings", "Налаштування"),
("Username", "Ім'я користувача"),
("Invalid port", "Неправильний порт"),
@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_stop_service_tip", "Закриття служби автоматично закриє всі встановлені з'єднання."),
("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."),
("android_start_service_tip", "Натисніть [Запуск проміжного сервера] або ВІДКРИТИ роздільну здатність [Захоплення екрана], щоб запустити службу демонстрації екрана."),
("Account", ""),
("Account", "Акаунт"),
("Overwrite", "Перезаписати"),
("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити або перезаписати файл?"),
("Quit", "Вийти"),
@ -298,7 +298,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Connection not allowed", "Підключення не дозволено"),
("Legacy mode", ""),
("Map mode", ""),
("Режим перекладу", ""),
("Translate mode", ""),
("Use temporary password", "Використовувати тимчасовий пароль"),
("Use permanent password", "Використовувати постійний пароль"),
("Use both passwords", "Використовувати обидва паролі"),
@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""),
("elevated_foreground_window_warning", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect();
}