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/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart'; 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>>> typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
Function(BuildContext); Function(BuildContext);

View File

@ -232,11 +232,11 @@ class _GeneralState extends State<_General> {
controller: scrollController, controller: scrollController,
children: [ children: [
theme(), theme(),
abr(),
hwcodec(), hwcodec(),
audio(context), audio(context),
record(context), record(context),
_Card(title: 'Language', children: [language()]), _Card(title: 'Language', children: [language()]),
other()
], ],
).marginOnly(bottom: _kListViewBottomMargin)); ).marginOnly(bottom: _kListViewBottomMargin));
} }
@ -267,8 +267,10 @@ class _GeneralState extends State<_General> {
]); ]);
} }
Widget abr() { Widget other() {
return _Card(title: 'Adaptive Bitrate', children: [ return _Card(title: 'Other', children: [
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs'),
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), _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:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../common.dart'; import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_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 { class FileManagerPage extends StatefulWidget {
const FileManagerPage({Key? key, required this.id}) : super(key: key); const FileManagerPage({Key? key, required this.id}) : super(key: key);
@ -40,7 +53,7 @@ class _FileManagerPageState extends State<FileManagerPage>
final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController(); final _breadCrumbScrollerRemote = ScrollController();
final _dropMaskVisible = false.obs; final _dropMaskVisible = false.obs; // TODO impl drop mask
ScrollController getBreadCrumbScrollController(bool isLocal) { ScrollController getBreadCrumbScrollController(bool isLocal) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
@ -84,6 +97,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
_locationNodeLocal.removeListener(onLocalLocationFocusChanged); _locationNodeLocal.removeListener(onLocalLocationFocusChanged);
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged); _locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
_locationNodeLocal.dispose();
_locationNodeRemote.dispose();
super.dispose(); super.dispose();
} }
@ -95,15 +110,8 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.dialogManager.setOverlayState(Overlay.of(context)); _ffi.dialogManager.setOverlayState(Overlay.of(context));
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: _ffi.fileModel, value: _ffi.fileModel,
child: Consumer<FileModel>(builder: (_context, _model, _child) { child: Consumer<FileModel>(builder: (context, model, child) {
return WillPopScope( return Scaffold(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
}
return false;
},
child: Scaffold(
backgroundColor: Theme.of(context).backgroundColor, backgroundColor: Theme.of(context).backgroundColor,
body: Row( body: Row(
children: [ children: [
@ -112,39 +120,54 @@ class _FileManagerPageState extends State<FileManagerPage>
Flexible(flex: 2, child: statusList()) Flexible(flex: 2, child: statusList())
], ],
), ),
)); );
})); }));
}) })
]); ]);
} }
Widget menu({bool isLocal = false}) { Widget menu({bool isLocal = false}) {
return PopupMenuButton<String>( var menuPos = RelativeRect.fill;
final items = [
MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate("Show Hidden Files"),
getter: () async {
return model.getCurrentShowHidden(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), icon: const Icon(Icons.more_vert),
splashRadius: 20, splashRadius: 20,
itemBuilder: (context) { onPressed: () => mod_menu.showMenu(
return [ context: context,
PopupMenuItem( position: menuPos,
child: Row( items: items
children: [ .map((e) => e.build(
Icon( context,
model.getCurrentShowHidden(isLocal) MenuConfig(
? Icons.check_box_outlined commonColor: CustomPopupMenuTheme.commonColor,
: Icons.check_box_outline_blank, height: CustomPopupMenuTheme.height,
color: Colors.black), dividerHeight: CustomPopupMenuTheme.dividerHeight)))
SizedBox(width: 5), .expand((i) => i)
Text(translate("Show Hidden Files")) .toList(),
], elevation: 8,
), ),
value: "hidden", ));
)
];
},
onSelected: (v) {
if (v == "hidden") {
model.toggleShowHidden(local: isLocal);
}
});
} }
Widget body({bool isLocal = false}) { Widget body({bool isLocal = false}) {
@ -153,13 +176,13 @@ class _FileManagerPageState extends State<FileManagerPage>
final sortIndex = (SortBy style) { final sortIndex = (SortBy style) {
switch (style) { switch (style) {
case SortBy.Name: case SortBy.Name:
return 1; return 0;
case SortBy.Type: case SortBy.Type:
return 0; return 0;
case SortBy.Modified: case SortBy.Modified:
return 2; return 1;
case SortBy.Size: case SortBy.Size:
return 3; return 2;
} }
}(model.getSortStyle(isLocal)); }(model.getSortStyle(isLocal));
final sortAscending = final sortAscending =
@ -187,13 +210,9 @@ class _FileManagerPageState extends State<FileManagerPage>
controller: ScrollController(), controller: ScrollController(),
child: ObxValue<RxString>( child: ObxValue<RxString>(
(searchText) { (searchText) {
final filteredEntries = searchText.isEmpty final filteredEntries = searchText.isNotEmpty
? entries.where((element) { ? entries.where((element) {
if (searchText.isEmpty) {
return true;
} else {
return element.name.contains(searchText.value); return element.name.contains(searchText.value);
}
}).toList(growable: false) }).toList(growable: false)
: entries; : entries;
return DataTable( return DataTable(
@ -201,16 +220,16 @@ class _FileManagerPageState extends State<FileManagerPage>
showCheckboxColumn: true, showCheckboxColumn: true,
dataRowHeight: 25, dataRowHeight: 25,
headingRowHeight: 30, headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8, columnSpacing: 8,
showBottomBorder: true, showBottomBorder: true,
sortColumnIndex: sortIndex, sortColumnIndex: sortIndex,
sortAscending: sortAscending, sortAscending: sortAscending,
columns: [ columns: [
DataColumn(label: Text(translate(" "))), // icon
DataColumn( DataColumn(
label: Text( label: Text(
translate("Name"), translate("Name"),
), ).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) { onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Name, model.changeSortStyle(SortBy.Name,
isLocal: isLocal, ascending: ascending); isLocal: isLocal, ascending: ascending);
@ -250,19 +269,27 @@ class _FileManagerPageState extends State<FileManagerPage>
selected: selected:
getSelectedItem(isLocal).contains(entry), getSelectedItem(isLocal).contains(entry),
cells: [ cells: [
DataCell(Icon( DataCell(
Container(
width: 180,
child: Tooltip(
message: entry.name,
child: Row(children: [
Icon(
entry.isFile entry.isFile
? Icons.feed_outlined ? Icons.feed_outlined
: Icons.folder, : Icons.folder,
size: 25)), size: 20,
DataCell( color: Theme.of(context)
ConstrainedBox( .iconTheme
constraints: .color
BoxConstraints(maxWidth: 100), ?.withOpacity(0.7),
child: Tooltip( ).marginSymmetric(horizontal: 2),
message: entry.name, Expanded(
child: Text(entry.name, child: Text(entry.name,
overflow: TextOverflow.ellipsis), overflow:
TextOverflow.ellipsis))
]),
)), onTap: () { )), onTap: () {
if (entry.isDirectory) { if (entry.isDirectory) {
openDirectory(entry.path, isLocal: isLocal); openDirectory(entry.path, isLocal: isLocal);
@ -273,29 +300,27 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
} else { } else {
// Perform file-related tasks. // Perform file-related tasks.
final _selectedItems = final selectedItems =
getSelectedItem(isLocal); getSelectedItem(isLocal);
if (_selectedItems.contains(entry)) { if (selectedItems.contains(entry)) {
_selectedItems.remove(entry); selectedItems.remove(entry);
} else { } else {
_selectedItems.add(isLocal, entry); selectedItems.add(isLocal, entry);
} }
setState(() {}); setState(() {});
} }
}), }),
DataCell(Text( DataCell(FittedBox(
entry child: Text(
.lastModified() "${entry.lastModified().toString().replaceAll(".000", "")} ",
.toString()
.replaceAll(".000", "") +
" ",
style: TextStyle( style: TextStyle(
fontSize: 12, color: MyTheme.darkGray), fontSize: 12, color: MyTheme.darkGray),
)), ))),
DataCell(Text( DataCell(Text(
sizeStr, sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, color: MyTheme.darkGray), fontSize: 10, color: MyTheme.darkGray),
)), )),
]); ]);
}).toList(growable: false), }).toList(growable: false),
@ -410,15 +435,10 @@ class _FileManagerPageState extends State<FileManagerPage>
)); ));
} }
goBack({bool? isLocal}) {
model.goToParentDirectory(isLocal: isLocal);
}
Widget headTools(bool isLocal) { Widget headTools(bool isLocal) {
final _locationStatus = final locationStatus =
isLocal ? _locationStatusLocal : _locationStatusRemote; isLocal ? _locationStatusLocal : _locationStatusRemote;
final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
return Container( return Container(
child: Column( child: Column(
children: [ children: [
@ -463,77 +483,83 @@ class _FileManagerPageState extends State<FileManagerPage>
icon: const Icon(Icons.home_outlined), icon: const Icon(Icons.home_outlined),
splashRadius: 20, splashRadius: 20,
), ),
IconButton(
icon: const Icon(Icons.arrow_back),
splashRadius: 20,
onPressed: () {
model.goBack(isLocal: isLocal);
},
),
IconButton( IconButton(
icon: const Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
splashRadius: 20, splashRadius: 20,
onPressed: () { onPressed: () {
goBack(isLocal: isLocal); model.goToParentDirectory(isLocal: isLocal);
}, },
), ),
menu(isLocal: isLocal),
], ],
), ),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
_locationStatus.value = locationStatus.value =
_locationStatus.value == LocationStatus.bread locationStatus.value == LocationStatus.bread
? LocationStatus.textField ? LocationStatus.pathLocation
: LocationStatus.bread; : LocationStatus.bread;
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
if (_locationStatus.value == LocationStatus.textField) { if (locationStatus.value == LocationStatus.pathLocation) {
_locationFocus.requestFocus(); locationFocus.requestFocus();
} }
}); });
}, },
child: Container( child: Obx(() => Container(
decoration: decoration: BoxDecoration(
BoxDecoration(border: Border.all(color: Colors.black12)), border: Border.all(
color: locationStatus.value == LocationStatus.bread
? Colors.black12
: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5))),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Obx(() => child: locationStatus.value == LocationStatus.bread
_locationStatus.value == LocationStatus.bread
? buildBread(isLocal) ? buildBread(isLocal)
: buildPathLocation(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);
}
})
], ],
))),
)), )),
)), Obx(() {
PopupMenuButton( switch (locationStatus.value) {
itemBuilder: (context) => [ case LocationStatus.bread:
PopupMenuItem( return IconButton(
enabled: false, onPressed: () {
child: ConstrainedBox( locationStatus.value = LocationStatus.fileSearchBar;
constraints: BoxConstraints(minWidth: 200), final focusNode =
child: TextField( isLocal ? _locationNodeLocal : _locationNodeRemote;
controller: Future.delayed(
TextEditingController(text: _searchTextObs.value), Duration.zero, () => focusNode.requestFocus());
autofocus: true, },
decoration:
InputDecoration(prefixIcon: Icon(Icons.search)),
onChanged: (searchText) =>
onSearchText(searchText, isLocal),
),
))
],
splashRadius: 20, splashRadius: 20,
child: const Icon(Icons.search), 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( IconButton(
onPressed: () { onPressed: () {
model.refresh(isLocal: isLocal); model.refresh(isLocal: isLocal);
@ -609,6 +635,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}, },
splashRadius: 20, splashRadius: 20,
icon: const Icon(Icons.delete_forever_outlined)), icon: const Icon(Icons.delete_forever_outlined)),
menu(isLocal: isLocal),
], ],
), ),
), ),
@ -642,9 +669,11 @@ class _FileManagerPageState extends State<FileManagerPage>
// ignore // ignore
} else { } else {
// lost focus, change to bread // lost focus, change to bread
if (_locationStatusLocal.value != LocationStatus.fileSearchBar) {
_locationStatusLocal.value = LocationStatus.bread; _locationStatusLocal.value = LocationStatus.bread;
} }
} }
}
void onRemoteLocationFocusChanged() { void onRemoteLocationFocusChanged() {
debugPrint("focus changed on remote"); debugPrint("focus changed on remote");
@ -652,9 +681,11 @@ class _FileManagerPageState extends State<FileManagerPage>
// ignore // ignore
} else { } else {
// lost focus, change to bread // lost focus, change to bread
if (_locationStatusRemote.value != LocationStatus.fileSearchBar) {
_locationStatusRemote.value = LocationStatus.bread; _locationStatusRemote.value = LocationStatus.bread;
} }
} }
}
Widget buildBread(bool isLocal) { Widget buildBread(bool isLocal) {
final items = getPathBreadCrumbItems(isLocal, (list) { final items = getPathBreadCrumbItems(isLocal, (list) {
@ -664,14 +695,33 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
openDirectory(path, isLocal: isLocal); openDirectory(path, isLocal: isLocal);
}); });
breadCrumbScrollToEnd(isLocal);
return items.isEmpty return items.isEmpty
? Offstage() ? Offstage()
: BreadCrumb( : Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Expanded(
child: BreadCrumb(
items: items, items: items,
divider: Text("/").paddingSymmetric(horizontal: 4.0), divider: Text("/").paddingSymmetric(horizontal: 4.0),
overflow: ScrollableOverflow( overflow: ScrollableOverflow(
controller: getBreadCrumbScrollController(isLocal)), 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( List<BreadCrumbItem> getPathBreadCrumbItems(
@ -690,28 +740,49 @@ class _FileManagerPageState extends State<FileManagerPage>
breadCrumbScrollToEnd(bool isLocal) { breadCrumbScrollToEnd(bool isLocal) {
Future.delayed(Duration(milliseconds: 200), () { Future.delayed(Duration(milliseconds: 200), () {
final _breadCrumbScroller = getBreadCrumbScrollController(isLocal); final breadCrumbScroller = getBreadCrumbScrollController(isLocal);
_breadCrumbScroller.animateTo( if (breadCrumbScroller.hasClients) {
_breadCrumbScroller.position.maxScrollExtent, breadCrumbScroller.animateTo(
breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200), duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn); curve: Curves.fastLinearToSlowEaseIn);
}
}); });
} }
Widget buildPathLocation(bool isLocal) { Widget buildPathLocation(bool isLocal) {
return TextField( final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, 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( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
prefix: Padding(padding: EdgeInsets.only(left: 4.0)), prefix: Padding(padding: EdgeInsets.only(left: 4.0))),
), controller: textController,
controller:
TextEditingController(text: model.getCurrentDir(isLocal).path),
onSubmitted: (path) { onSubmitted: (path) {
openDirectory(path, isLocal: isLocal); openDirectory(path, isLocal: isLocal);
}, },
); onChanged: locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal)
: null,
))
]);
} }
onSearchText(String searchText, bool isLocal) { onSearchText(String searchText, bool isLocal) {
@ -734,7 +805,7 @@ class _FileManagerPageState extends State<FileManagerPage>
return; return;
} }
var items = SelectedItems(); var items = SelectedItems();
details.files.forEach((file) { for (var file in details.files) {
final f = File(file.path); final f = File(file.path);
items.add( items.add(
true, true,
@ -743,7 +814,7 @@ class _FileManagerPageState extends State<FileManagerPage>
..name = file.name ..name = file.name
..size = ..size =
FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
}); }
model.sendFiles(items, isRemote: false); 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:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../mobile/widgets/dialog.dart'; import '../../models/platform_model.dart';
/// File Transfer for multi tabs /// File Transfer for multi tabs
class FileManagerTabPage extends StatefulWidget { class FileManagerTabPage extends StatefulWidget {
@ -35,7 +35,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(params['id']), onTabCloseButton: () => () => tabController.closeBy(params['id']),
page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
} }
@ -58,7 +58,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(id), onTabCloseButton: () => tabController.closeBy(id),
page: FileManagerPage(key: ValueKey(id), id: id))); page: FileManagerPage(key: ValueKey(id), id: id)));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
tabController.clear(); tabController.clear();
@ -98,26 +98,19 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
return widget.params["windowId"]; 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 { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length; final connLength = tabController.state.value.tabs.length;
if (connLength < 1) { if (connLength <= 1) {
tabController.clear();
return true; return true;
} else if (connLength == 1) {
final currentConn = tabController.state.value.tabs[0];
handleTabCloseButton(currentConn.key);
return false;
} else { } 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) { if (res) {
tabController.clear(); 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:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../mobile/widgets/dialog.dart'; import '../../models/platform_model.dart';
class ConnectionTabPage extends StatefulWidget { class ConnectionTabPage extends StatefulWidget {
final Map<String, dynamic> params; final Map<String, dynamic> params;
@ -42,7 +42,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: peerId, label: peerId,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(peerId), onTabCloseButton: () => tabController.closeBy(peerId),
page: Obx(() => RemotePage( page: Obx(() => RemotePage(
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId, id: peerId,
@ -78,7 +78,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => handleTabCloseButton(id), onTabCloseButton: () => tabController.closeBy(id),
page: Obx(() => RemotePage( page: Obx(() => RemotePage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
@ -173,29 +173,21 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
return widget.params["windowId"]; 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 { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length; final connLength = tabController.length;
if (connLength < 1) { if (connLength <= 1) {
tabController.clear();
return true; return true;
} else if (connLength == 1) {
final currentConn = tabController.state.value.tabs[0];
handleTabCloseButton(currentConn.key);
return false;
} else { } 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) { if (res) {
tabController.clear(); tabController.clear();
_update_remote_count();
} }
return res; return res;
} }

View File

@ -407,7 +407,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
setState(() { setState(() {
client.recording = enabled; 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart';
import './material_mod_popup_menu.dart' as mod_menu; import './material_mod_popup_menu.dart' as mod_menu;
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-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 { Future<bool> closeConfirmDialog() async {
var confirm = true;
final res = await gFFI.dialogManager.show<bool>((setState, close) { 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( return CustomAlertDialog(
title: Row(children: [ title: Row(children: [
const Icon(Icons.warning_amber_sharp, const Icon(Icons.warning_amber_sharp,
@ -465,7 +472,25 @@ Future<bool> closeConfirmDialog() async {
const SizedBox(width: 10), const SizedBox(width: 10),
Text(translate("Warning")), 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: [ actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))), TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(onPressed: submit, child: Text(translate("OK"))), ElevatedButton(onPressed: submit, child: Text(translate("OK"))),

View File

@ -16,12 +16,15 @@ class FileModel extends ChangeNotifier {
var _isLocal = false; var _isLocal = false;
var _selectMode = false; var _selectMode = false;
var _localOption = DirectoryOption(); final _localOption = DirectoryOption();
var _remoteOption = DirectoryOption(); final _remoteOption = DirectoryOption();
List<String> localHistory = [];
List<String> remoteHistory = [];
var _jobId = 0; var _jobId = 0;
var _jobProgress = JobProgress(); // from rust update final _jobProgress = JobProgress(); // from rust update
/// JobTable <jobId, JobProgress> /// JobTable <jobId, JobProgress>
final _jobTable = List<JobProgress>.empty(growable: true).obs; 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; isLocal = isLocal ?? _isLocal;
if (!isBack) {
pushHistory(isLocal);
}
final showHidden = final showHidden =
isLocal ? _localOption.showHidden : _remoteOption.showHidden; isLocal ? _localOption.showHidden : _remoteOption.showHidden;
final isWindows = 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}) { goHome({bool? isLocal}) {
isLocal = isLocal ?? _isLocal; isLocal = isLocal ?? _isLocal;
openDirectory(getCurrentHome(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}) { goToParentDirectory({bool? isLocal}) {
isLocal = isLocal ?? _isLocal; isLocal = isLocal ?? _isLocal;
final isWindows = final isWindows =
@ -685,6 +714,8 @@ class FileModel extends ChangeNotifier {
} }
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
final history = isLocal ? localHistory : remoteHistory;
history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs( bind.sessionRemoveAllEmptyDirs(
id: '${parent.target?.id}', id: '${parent.target?.id}',
actId: _jobId, 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窗口。为避免这个问题建议在远端设备上安装或者以管理员权限运行本软件。"), ("uac_warning", "暂时无法访问远端设备因为远端设备正在请求用户账户权限请等待对方关闭UAC窗口。为避免这个问题建议在远端设备上安装或者以管理员权限运行本软件。"),
("elevated_foreground_window_warning", "暂时无法使用鼠标键盘,因为远端桌面的当前窗口需要更高的权限才能操作, 可以请求对方最小化当前窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), ("elevated_foreground_window_warning", "暂时无法使用鼠标键盘,因为远端桌面的当前窗口需要更高的权限才能操作, 可以请求对方最小化当前窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"),
("Disconnected", "会话已结束"), ("Disconnected", "会话已结束"),
("Other", "其他"),
("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""), ("uac_warning", ""),
("elevated_foreground_window_warning", ""), ("elevated_foreground_window_warning", ""),
("Disconnected", ""), ("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect(); ].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"), ("Reboot required", "Redémarrage pour prendre effet"),
("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"),
("x11 expected", "Veuillez passer à x11"), ("x11 expected", "Veuillez passer à x11"),
("Port", ""), ("Port", "Port"),
("Settings", "Paramètres"), ("Settings", "Paramètres"),
("Username", " Nom d'utilisateur"), ("Username", " Nom d'utilisateur"),
("Invalid port", "Port invalide"), ("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_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_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."), ("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"), ("Overwrite", "Écraser"),
("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"),
("Quit", "Quitter"), ("Quit", "Quitter"),
@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("uac_warning", ""), ("uac_warning", ""),
("elevated_foreground_window_warning", ""), ("elevated_foreground_window_warning", ""),
("Disconnected", ""), ("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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