diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 8840fa474..7011e722e 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -137,11 +137,13 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final model = Provider.of(context); - - return ListView( + var counter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: model.reorder, scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - children: model.visibleIndexs.map((t) { + children: model.visibleEnabledOrderedIndexs.map((t) { final selected = model.currentTab == t; final color = selected ? MyTheme.tabbar(context).selectedTextColor @@ -155,43 +157,47 @@ class _PeerTabPageState extends State border: Border( bottom: BorderSide(width: 2, color: color!), )); - return Obx(() => Tooltip( - preferBelow: false, - message: model.tabTooltip(t), - onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, - child: InkWell( - child: Container( - decoration: (hover.value - ? (selected ? decoBorder : deco) - : (selected ? decoBorder : null)), - child: Icon(model.tabIcon(t), color: color) - .paddingSymmetric(horizontal: 4), - ).paddingSymmetric(horizontal: 4), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterOption( - k: 'peer-tab-index', v: t.toString()); - }, - onHover: (value) => hover.value = value, - ), - )); + counter += 1; + return ReorderableDragStartListener( + key: ValueKey(t), + index: counter, + child: Obx(() => Tooltip( + preferBelow: false, + message: model.tabTooltip(t), + onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, + child: InkWell( + child: Container( + decoration: (hover.value + ? (selected ? decoBorder : deco) + : (selected ? decoBorder : null)), + child: Icon(model.tabIcon(t), color: color) + .paddingSymmetric(horizontal: 4), + ).paddingSymmetric(horizontal: 4), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterOption( + k: PeerTabModel.kPeerTabIndex, v: t.toString()); + }, + onHover: (value) => hover.value = value, + ), + ))); }).toList()); } Widget _createPeersView() { final model = Provider.of(context); Widget child; - if (model.visibleIndexs.isEmpty) { + if (model.visibleEnabledOrderedIndexs.isEmpty) { child = visibleContextMenuListener(Row( children: [Expanded(child: InkWell())], )); } else { - if (model.visibleIndexs.contains(model.currentTab)) { + if (model.visibleEnabledOrderedIndexs.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]); + model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]); }); child = entries[0].widget; } @@ -255,16 +261,17 @@ 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++) { + for (int i = 0; i < PeerTabModel.maxTabCount; i++) { + if (!model.isEnabled[i]) continue; items.add(PopupMenuItem( height: kMinInteractiveDimension * 0.8, - onTap: () => model.setTabVisible(i, !model.isVisible[i]), + onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]), child: Row( children: [ Checkbox( - value: model.isVisible[i], + value: model.isVisibleEnabled[i], onChanged: (_) { - model.setTabVisible(i, !model.isVisible[i]); + model.setTabVisible(i, !model.isVisibleEnabled[i]); if (Navigator.canPop(context)) { Navigator.pop(context); } @@ -314,16 +321,17 @@ class _PeerTabPageState extends State 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( + final menu = List.empty(growable: true); + for (int i = 0; i < model.orders.length; i++) { + int tabIndex = model.orders[i]; + if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue; + if (!model.isEnabled[tabIndex]) continue; + menu.add(MenuEntrySwitchSync( switchType: SwitchType.scheckbox, - text: model.tabTooltip(i), - getter: () async { - return model.isVisible[i]; - }, + text: model.tabTooltip(tabIndex), + currentValue: model.isVisibleEnabled[tabIndex], setter: (show) async { - model.setTabVisible(i, show); + model.setTabVisible(tabIndex, show); cancelFunc(); })); } @@ -434,7 +442,7 @@ class _PeerTabPageState extends State model.setMultiSelectionMode(false); showToast(translate('Successful')); }, - child: Icon(model.icons[PeerTabIndex.fav.index]), + child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), ).marginOnly(left: isMobile ? 11 : 6), ); } @@ -455,7 +463,7 @@ class _PeerTabPageState extends State addPeersToAbDialog(peers); model.setMultiSelectionMode(false); }, - child: Icon(model.icons[PeerTabIndex.ab.index]), + child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), ).marginOnly(left: isMobile ? 11 : 6), ); } @@ -563,7 +571,7 @@ class _PeerTabPageState extends State final screenWidth = MediaQuery.of(context).size.width; final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftActionsSize = - (leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length; + (leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length; final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2; final searchWidth = 120; final otherActionWidth = 18 + 10; diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 9833dcbca..086d7a622 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -568,6 +568,47 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { } } +// Compatible with MenuEntrySwitch, it uses value instead of getter +class MenuEntrySwitchSync extends MenuEntrySwitchBase { + final SwitchSetter setter; + final RxBool _curOption = false.obs; + + MenuEntrySwitchSync({ + required SwitchType switchType, + required String text, + required bool currentValue, + required this.setter, + Rx? textStyle, + EdgeInsets? padding, + dismissOnClicked = false, + RxBool? enabled, + dismissCallback, + }) : super( + switchType: switchType, + text: text, + textStyle: textStyle, + padding: padding, + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ) { + _curOption.value = currentValue; + } + + @override + RxBool get curOption => _curOption; + @override + setOption(bool? option) async { + if (option != null) { + await setter(option); + // Notice: no ensure with getter, best used on menus that are destroyed on click + if (_curOption.value != option) { + _curOption.value = option; + } + } + } +} + typedef Switch2Getter = RxBool Function(); typedef Switch2Setter = Future Function(bool); diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 43bbfda72..52e43bfea 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -21,24 +21,43 @@ class PeerTabModel with ChangeNotifier { WeakReference parent; int get currentTab => _currentTab; int _currentTab = 0; // index in tabNames - List tabNames = [ + static const int maxTabCount = 5; + static const String kPeerTabIndex = 'peer-tab-index'; + static const String kPeerTabOrder = 'peer-tab-order'; + static const String kPeerTabVisible = 'peer-tab-visible'; + static const List tabNames = [ 'Recent sessions', 'Favorites', - if (!isWeb) 'Discovered', - if (!(bind.isDisableAb() || bind.isDisableAccount())) 'Address book', - if (!bind.isDisableAccount()) 'Group', + 'Discovered', + 'Address book', + 'Group', ]; - final List icons = [ + static const List icons = [ Icons.access_time_filled, Icons.star, - if (!isWeb) Icons.explore, - if (!(bind.isDisableAb() || bind.isDisableAccount())) IconFont.addressBook, - if (!bind.isDisableAccount()) Icons.group, + Icons.explore, + IconFont.addressBook, + Icons.group, ]; - final List _isVisible = List.filled(5, 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 isEnabled = List.from([ + true, + true, + !isWeb, + !(bind.isDisableAb() || bind.isDisableAccount()), + !bind.isDisableAccount(), + ]); + final List _isVisible = List.filled(maxTabCount, true, growable: false); + List get isVisibleEnabled => () { + final list = _isVisible.toList(); + for (int i = 0; i < maxTabCount; i++) { + list[i] = list[i] && isEnabled[i]; + } + return list; + }(); + final List orders = + List.generate(maxTabCount, (index) => index, growable: false); + List get visibleEnabledOrderedIndexs => + orders.where((e) => isVisibleEnabled[e]).toList(); List _selectedPeers = List.empty(growable: true); List get selectedPeers => _selectedPeers; bool _multiSelectionMode = false; @@ -53,7 +72,7 @@ class PeerTabModel with ChangeNotifier { PeerTabModel(this.parent) { // visible try { - final option = bind.getLocalFlutterOption(k: 'peer-tab-visible'); + final option = bind.getLocalFlutterOption(k: kPeerTabVisible); if (option.isNotEmpty) { List decodeList = jsonDecode(option); if (decodeList.length == _isVisible.length) { @@ -67,13 +86,37 @@ class PeerTabModel with ChangeNotifier { } catch (e) { debugPrint("failed to get peer tab visible list:$e"); } + // order + try { + final option = bind.getLocalFlutterOption(k: kPeerTabOrder); + if (option.isNotEmpty) { + List decodeList = jsonDecode(option); + if (decodeList.length == maxTabCount) { + var sortedList = decodeList.toList(); + sortedList.sort(); + bool valid = true; + for (int i = 0; i < maxTabCount; i++) { + if (sortedList[i] is! int || sortedList[i] != i) { + valid = false; + } + } + if (valid) { + for (int i = 0; i < orders.length; i++) { + orders[i] = decodeList[i]; + } + } + } + } + } catch (e) { + debugPrint("failed to get peer tab order list: $e"); + } // init currentTab _currentTab = - int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; - if (_currentTab < 0 || _currentTab >= tabNames.length) { + int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0; + if (_currentTab < 0 || _currentTab >= maxTabCount) { _currentTab = 0; } - _trySetCurrentTabToFirstVisible(); + _trySetCurrentTabToFirstVisibleEnabled(); } setCurrentTab(int index) { @@ -87,15 +130,13 @@ class PeerTabModel with ChangeNotifier { if (index >= 0 && index < tabNames.length) { return translate(tabNames[index]); } - assert(false); return index.toString(); } IconData tabIcon(int index) { - if (index >= 0 && index < tabNames.length) { + if (index >= 0 && index < icons.length) { return icons[index]; } - assert(false); return Icons.help; } @@ -171,29 +212,54 @@ class PeerTabModel with ChangeNotifier { } setTabVisible(int index, bool visible) { - if (index >= 0 && index < _isVisible.length) { + if (index >= 0 && index < maxTabCount) { if (_isVisible[index] != visible) { _isVisible[index] = visible; if (index == _currentTab && !visible) { - _trySetCurrentTabToFirstVisible(); - } else if (visible && visibleIndexs.length == 1) { + _trySetCurrentTabToFirstVisibleEnabled(); + } else if (visible && visibleEnabledOrderedIndexs.length == 1) { _currentTab = index; } try { bind.setLocalFlutterOption( - k: 'peer-tab-visible', v: jsonEncode(_isVisible)); + k: kPeerTabVisible, v: jsonEncode(_isVisible)); } catch (_) {} notifyListeners(); } } } - _trySetCurrentTabToFirstVisible() { - if (!_isVisible[_currentTab]) { - int firstVisible = _isVisible.indexWhere((e) => e); - if (firstVisible >= 0) { - _currentTab = firstVisible; + _trySetCurrentTabToFirstVisibleEnabled() { + if (!visibleEnabledOrderedIndexs.contains(_currentTab)) { + if (visibleEnabledOrderedIndexs.isNotEmpty) { + _currentTab = visibleEnabledOrderedIndexs.first; } } } + + reorder(int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex < 0 || oldIndex >= visibleEnabledOrderedIndexs.length) { + return; + } + if (newIndex < 0 || newIndex >= visibleEnabledOrderedIndexs.length) { + return; + } + final oldTabValue = visibleEnabledOrderedIndexs[oldIndex]; + final newTabValue = visibleEnabledOrderedIndexs[newIndex]; + int oldValueIndex = orders.indexOf(oldTabValue); + int newValueIndex = orders.indexOf(newTabValue); + final list = orders.toList(); + if (oldIndex != -1 && newIndex != -1) { + list.removeAt(oldValueIndex); + list.insert(newValueIndex, oldTabValue); + for (int i = 0; i < list.length; i++) { + orders[i] = list[i]; + } + bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders)); + notifyListeners(); + } + } }