diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 19d28513c..c99cf2e68 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -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>> Function(BuildContext); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index bc0eb7b67..55fd93bcc 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -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'), ]); } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index cc91a698f..f6fae1e31 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -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 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 Get.delete(tag: 'ft_${widget.id}'); _locationNodeLocal.removeListener(onLocalLocationFocusChanged); _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + _locationNodeLocal.dispose(); + _locationNodeRemote.dispose(); super.dispose(); } @@ -95,56 +110,64 @@ class _FileManagerPageState extends State _ffi.dialogManager.setOverlayState(Overlay.of(context)); return ChangeNotifierProvider.value( value: _ffi.fileModel, - child: Consumer(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(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( - 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( + 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 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 controller: ScrollController(), child: ObxValue( (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 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 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 } } 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 )); } - 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 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( - 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 }, splashRadius: 20, icon: const Icon(Icons.delete_forever_outlined)), + menu(isLocal: isLocal), ], ), ), @@ -642,7 +669,9 @@ class _FileManagerPageState extends State // 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 // 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 } 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( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + openDirectory(path, isLocal: isLocal); + } + }) + ]); } List getPathBreadCrumbItems( @@ -690,28 +740,49 @@ class _FileManagerPageState extends State 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 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 ..name = file.name ..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); - }); + } model.sendFiles(items, isRemote: false); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 24a36eddb..f844edc36 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -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 { 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 { 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 { 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 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(); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 352e4682a..1d3daf7b3 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -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 params; @@ -42,7 +42,7 @@ class _ConnectionTabPageState extends State { 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 { 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 { 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 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; } diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 3710b2932..e69557981 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -407,7 +407,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { setState(() { client.recording = enabled; }); - }, translate('Allow reco)rding session')) + }, translate('Allow recording session')) ], )), ], diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 71d1ec417..5f06aebfe 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -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 extends MenuEntryBase { ]; } } + +class CustomPopupMenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 3.0; +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index a98b3af20..e3fcf0f53 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -456,8 +456,15 @@ class WindowActionPanel extends StatelessWidget { } Future closeConfirmDialog() async { + var confirm = true; final res = await gFFI.dialogManager.show((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 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"))), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 3fdbe7099..1ba66b864 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -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 localHistory = []; + List remoteHistory = []; var _jobId = 0; - var _jobProgress = JobProgress(); // from rust update + final _jobProgress = JobProgress(); // from rust update /// JobTable final _jobTable = List.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, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b7971eb92..fb28840b4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,238 +5,238 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "49.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.9.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.1" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.4" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.2" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.3.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.15" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" desktop_multi_window: @@ -252,91 +252,91 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "5.2.0+1" + version: "5.2.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -348,21 +348,21 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_custom_cursor: @@ -378,14 +378,14 @@ packages: dependency: "direct main" description: name: flutter_improved_scrolling - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" flutter_localizations: @@ -397,14 +397,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -420,7 +420,7 @@ packages: dependency: "direct main" description: name: flutter_svg - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.5" flutter_web_plugins: @@ -432,406 +432,406 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.7.0" lints: dependency: transitive description: name: lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.5" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: @@ -847,91 +847,91 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -943,84 +943,84 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.5" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.3+1" + version: "2.1.0+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: @@ -1036,189 +1036,189 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.19" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" window_manager: @@ -1243,30 +1243,30 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.1" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 35e77990c..0d0b9e208 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -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(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e849dfa3f..9ca84a28b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -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(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3dd2e0a51..10cd64dd4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -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(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 38c0f657a..f45a583f9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -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(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index cee1975b0..f243eb28a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -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(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d3da5b402..413cef26b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 70a62e49d..79c93a4d9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -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(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 191165b65..b4ddebc94 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -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(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 96ac3892b..a751c11c0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -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(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1954eeaae..ae98a5690 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -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(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 0c11db665..46428d74c 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -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(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d59d71dc9..3c857424e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -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(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b4f29894d..f83b8b04b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -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(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 03f030507..9130d6b9a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -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(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 835fae801..0fd796cb3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -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(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 280390d7f..a20fdd557 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -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(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7784d74f0..c86d5cc31 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -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(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 0319ddc68..8d4105118 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -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(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b292f917c..02bfc95c0 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -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(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1a7912f23..e4ee58e23 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -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(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b588f2a33..5b0dc48aa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -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(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3a5c4afd6..7d5037807 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -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(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c245bdd75..4e441abff 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -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(); }