diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index cb5413ba1..acd98eea0 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,3 +1,6 @@ +import 'dart:ui' as ui; + +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; @@ -6,6 +9,9 @@ import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; @@ -61,6 +67,7 @@ class _PeerTabPageState extends State ({dynamic hint}) => gFFI.groupModel.pull(force: hint == null), ), ]; + RelativeRect? mobileTabContextMenuPos; @override void initState() { @@ -100,7 +107,9 @@ class _PeerTabPageState extends State child: selectionWrap(Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _createSwitchBar(context)), + Expanded( + child: + visibleContextMenuListener(_createSwitchBar(context))), const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), _createRefresh(), _createMultiSelection(), @@ -145,7 +154,7 @@ class _PeerTabPageState extends State return ListView( scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - children: model.indexs.map((t) { + children: model.visibleIndexs.map((t) { final selected = model.currentTab == t; final color = selected ? MyTheme.tabbar(context).selectedTextColor @@ -164,8 +173,10 @@ class _PeerTabPageState extends State decoration: selected ? decoBorder : (hover.value ? deco : null), child: Tooltip( + preferBelow: false, message: model.tabTooltip(t, gFFI.groupModel.groupName.value), + onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, child: Icon(model.tabIcon(t), color: color), ).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4), @@ -182,14 +193,15 @@ class _PeerTabPageState extends State Widget _createPeersView() { final model = Provider.of(context); Widget child; - if (model.indexs.isEmpty) { - child = Center( - child: Text(translate('Right click to select tabs')), - ); + if (model.visibleIndexs.isEmpty) { + child = visibleContextMenuListener(Row( + children: [Expanded(child: InkWell())], + )); } else { - if (model.indexs.contains(model.currentTab)) { + if (model.visibleIndexs.contains(model.currentTab)) { child = entries[model.currentTab].widget; } else { + debugPrint("should not happen! currentTab not in visibleIndexs"); Future.delayed(Duration.zero, () { model.setCurrentTab(model.indexs[0]); }); @@ -268,6 +280,96 @@ class _PeerTabPageState extends State ); } + void mobileShowTabVisibilityMenu() { + final model = gFFI.peerTabModel; + final items = List.empty(growable: true); + for (int i = 0; i < model.tabNames.length; i++) { + items.add(PopupMenuItem( + height: kMinInteractiveDimension * 0.8, + onTap: () => model.setTabVisible(i, !model.isVisible[i]), + child: Row( + children: [ + Checkbox( + value: model.isVisible[i], + onChanged: (_) { + model.setTabVisible(i, !model.isVisible[i]); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }), + Expanded( + child: + Text(model.tabTooltip(i, gFFI.groupModel.groupName.value))), + ], + ), + )); + } + if (mobileTabContextMenuPos != null) { + showMenu( + context: context, position: mobileTabContextMenuPos!, items: items); + } + } + + Widget visibleContextMenuListener(Widget child) { + if (isMobile) { + return GestureDetector( + onLongPressDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onLongPressUp: () { + mobileShowTabVisibilityMenu(); + }, + child: child, + ); + } else { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + final model = Provider.of(context); + final menu = List.empty(growable: true); + for (int i = 0; i < model.tabNames.length; i++) { + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: model.tabTooltip(i, gFFI.groupModel.groupName.value), + getter: () async { + return model.isVisible[i]; + }, + setter: (show) async { + model.setTabVisible(i, show); + cancelFunc(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); + } + Widget createMultiSelectionBar() { final model = Provider.of(context); return Row( diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 2e65e64bd..2fdf9b449 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -36,7 +37,10 @@ class PeerTabModel with ChangeNotifier { IconFont.addressBook, Icons.group, ]; + final List _isVisible = List.filled(4, true, growable: false); + List get isVisible => _isVisible; List get indexs => List.generate(tabNames.length, (index) => index); + List get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); List _selectedPeers = List.empty(growable: true); List get selectedPeers => _selectedPeers; bool _multiSelectionMode = false; @@ -49,12 +53,29 @@ class PeerTabModel with ChangeNotifier { String get lastId => _lastId; PeerTabModel(this.parent) { + // visible + try { + final option = bind.getLocalFlutterOption(k: 'peer-tab-visible'); + if (option.isNotEmpty) { + List decodeList = jsonDecode(option); + if (decodeList.length == _isVisible.length) { + for (int i = 0; i < _isVisible.length; i++) { + if (decodeList[i] is bool) { + _isVisible[i] = decodeList[i]; + } + } + } + } + } catch (e) { + debugPrint("failed to get peer tab visible list:$e"); + } // init currentTab _currentTab = int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; if (_currentTab < 0 || _currentTab >= tabNames.length) { _currentTab = 0; } + _trySetCurrentTabToFirstVisible(); } setCurrentTab(int index) { @@ -158,4 +179,31 @@ class PeerTabModel with ChangeNotifier { } } } + + setTabVisible(int index, bool visible) { + if (index >= 0 && index < _isVisible.length) { + if (_isVisible[index] != visible) { + _isVisible[index] = visible; + if (index == _currentTab && !visible) { + _trySetCurrentTabToFirstVisible(); + } else if (visible && visibleIndexs.length == 1) { + _currentTab = index; + } + try { + bind.setLocalFlutterOption( + k: 'peer-tab-visible', v: jsonEncode(_isVisible)); + } catch (_) {} + notifyListeners(); + } + } + } + + _trySetCurrentTabToFirstVisible() { + if (!_isVisible[_currentTab]) { + int firstVisible = _isVisible.indexWhere((e) => e); + if (firstVisible >= 0) { + _currentTab = firstVisible; + } + } + } } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index cc0297297..64987f268 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -57,7 +57,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID serveris"), ("Relay Server", "Releja serveris"), ("API Server", "API serveris"), - ("Key", "Atslēga"), ("invalid_http", "jāsākas ar http:// vai https://"), ("Invalid IP", "Nederīga IP"), ("Invalid format", "Nederīgs formāts"), @@ -297,7 +296,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This file exists, skip or overwrite this file?", "Šis fails pastāv, izlaist vai pārrakstīt šo failu?"), ("Quit", "Iziet"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("Help", "Palīdzība"), ("Failed", "Neizdevās"), ("Succeeded", "Izdevās"), @@ -481,7 +479,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Me", "Es"), ("identical_file_tip", "Šis fails ir identisks sesijas failam."), ("show_monitors_tip", "Rādīt monitorus rīkjoslā"), - ("enter_rustdesk_passwd_tip", "Ievadiet RustDesk paroli"), ("View Mode", "Skatīšanas režīms"), ("login_linux_tip", "Jums ir jāpiesakās attālajā Linux kontā, lai iespējotu X darbvirsmas sesiju"), ("verify_rustdesk_password_tip", "Pārbaudīt RustDesk paroli"),