reordered peer tab (#7604)

* reordered peer tab

Signed-off-by: 21pages <pages21@163.com>

* opt peer tab visible menu, avoid checkbox value splash

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2024-04-06 17:53:03 +08:00 committed by GitHub
parent 5a0333ddaf
commit 0c294eefae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 184 additions and 69 deletions

View File

@ -137,11 +137,13 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) { Widget _createSwitchBar(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
var counter = -1;
return ListView( return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: model.reorder,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: model.visibleIndexs.map((t) { children: model.visibleEnabledOrderedIndexs.map((t) {
final selected = model.currentTab == t; final selected = model.currentTab == t;
final color = selected final color = selected
? MyTheme.tabbar(context).selectedTextColor ? MyTheme.tabbar(context).selectedTextColor
@ -155,43 +157,47 @@ class _PeerTabPageState extends State<PeerTabPage>
border: Border( border: Border(
bottom: BorderSide(width: 2, color: color!), bottom: BorderSide(width: 2, color: color!),
)); ));
return Obx(() => Tooltip( counter += 1;
preferBelow: false, return ReorderableDragStartListener(
message: model.tabTooltip(t), key: ValueKey(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, index: counter,
child: InkWell( child: Obx(() => Tooltip(
child: Container( preferBelow: false,
decoration: (hover.value message: model.tabTooltip(t),
? (selected ? decoBorder : deco) onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
: (selected ? decoBorder : null)), child: InkWell(
child: Icon(model.tabIcon(t), color: color) child: Container(
.paddingSymmetric(horizontal: 4), decoration: (hover.value
).paddingSymmetric(horizontal: 4), ? (selected ? decoBorder : deco)
onTap: () async { : (selected ? decoBorder : null)),
await handleTabSelection(t); child: Icon(model.tabIcon(t), color: color)
await bind.setLocalFlutterOption( .paddingSymmetric(horizontal: 4),
k: 'peer-tab-index', v: t.toString()); ).paddingSymmetric(horizontal: 4),
}, onTap: () async {
onHover: (value) => hover.value = value, await handleTabSelection(t);
), await bind.setLocalFlutterOption(
)); k: PeerTabModel.kPeerTabIndex, v: t.toString());
},
onHover: (value) => hover.value = value,
),
)));
}).toList()); }).toList());
} }
Widget _createPeersView() { Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget child; Widget child;
if (model.visibleIndexs.isEmpty) { if (model.visibleEnabledOrderedIndexs.isEmpty) {
child = visibleContextMenuListener(Row( child = visibleContextMenuListener(Row(
children: [Expanded(child: InkWell())], children: [Expanded(child: InkWell())],
)); ));
} else { } else {
if (model.visibleIndexs.contains(model.currentTab)) { if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget; child = entries[model.currentTab].widget;
} else { } else {
debugPrint("should not happen! currentTab not in visibleIndexs"); debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]); model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]);
}); });
child = entries[0].widget; child = entries[0].widget;
} }
@ -255,16 +261,17 @@ class _PeerTabPageState extends State<PeerTabPage>
void mobileShowTabVisibilityMenu() { void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel; final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true); final items = List<PopupMenuItem>.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( items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8, height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]), onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
child: Row( child: Row(
children: [ children: [
Checkbox( Checkbox(
value: model.isVisible[i], value: model.isVisibleEnabled[i],
onChanged: (_) { onChanged: (_) {
model.setTabVisible(i, !model.isVisible[i]); model.setTabVisible(i, !model.isVisibleEnabled[i]);
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
Navigator.pop(context); Navigator.pop(context);
} }
@ -314,16 +321,17 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget visibleContextMenu(CancelFunc cancelFunc) { Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true); final menu = List<MenuEntrySwitchSync>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) { for (int i = 0; i < model.orders.length; i++) {
menu.add(MenuEntrySwitch( int tabIndex = model.orders[i];
if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue;
if (!model.isEnabled[tabIndex]) continue;
menu.add(MenuEntrySwitchSync(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: model.tabTooltip(i), text: model.tabTooltip(tabIndex),
getter: () async { currentValue: model.isVisibleEnabled[tabIndex],
return model.isVisible[i];
},
setter: (show) async { setter: (show) async {
model.setTabVisible(i, show); model.setTabVisible(tabIndex, show);
cancelFunc(); cancelFunc();
})); }));
} }
@ -434,7 +442,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Icon(model.icons[PeerTabIndex.fav.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: isMobile ? 11 : 6),
); );
} }
@ -455,7 +463,7 @@ class _PeerTabPageState extends State<PeerTabPage>
addPeersToAbDialog(peers); addPeersToAbDialog(peers);
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: Icon(model.icons[PeerTabIndex.ab.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: isMobile ? 11 : 6),
); );
} }
@ -563,7 +571,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize = 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 availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120; final searchWidth = 120;
final otherActionWidth = 18 + 10; final otherActionWidth = 18 + 10;

View File

@ -568,6 +568,47 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
} }
} }
// Compatible with MenuEntrySwitch, it uses value instead of getter
class MenuEntrySwitchSync<T> extends MenuEntrySwitchBase<T> {
final SwitchSetter setter;
final RxBool _curOption = false.obs;
MenuEntrySwitchSync({
required SwitchType switchType,
required String text,
required bool currentValue,
required this.setter,
Rx<TextStyle>? 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 Switch2Getter = RxBool Function();
typedef Switch2Setter = Future<void> Function(bool); typedef Switch2Setter = Future<void> Function(bool);

View File

@ -21,24 +21,43 @@ class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent; WeakReference<FFI> parent;
int get currentTab => _currentTab; int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames int _currentTab = 0; // index in tabNames
List<String> 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<String> tabNames = [
'Recent sessions', 'Recent sessions',
'Favorites', 'Favorites',
if (!isWeb) 'Discovered', 'Discovered',
if (!(bind.isDisableAb() || bind.isDisableAccount())) 'Address book', 'Address book',
if (!bind.isDisableAccount()) 'Group', 'Group',
]; ];
final List<IconData> icons = [ static const List<IconData> icons = [
Icons.access_time_filled, Icons.access_time_filled,
Icons.star, Icons.star,
if (!isWeb) Icons.explore, Icons.explore,
if (!(bind.isDisableAb() || bind.isDisableAccount())) IconFont.addressBook, IconFont.addressBook,
if (!bind.isDisableAccount()) Icons.group, Icons.group,
]; ];
final List<bool> _isVisible = List.filled(5, true, growable: false); List<bool> isEnabled = List.from([
List<bool> get isVisible => _isVisible; true,
List<int> get indexs => List.generate(tabNames.length, (index) => index); true,
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); !isWeb,
!(bind.isDisableAb() || bind.isDisableAccount()),
!bind.isDisableAccount(),
]);
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
List<bool> get isVisibleEnabled => () {
final list = _isVisible.toList();
for (int i = 0; i < maxTabCount; i++) {
list[i] = list[i] && isEnabled[i];
}
return list;
}();
final List<int> orders =
List.generate(maxTabCount, (index) => index, growable: false);
List<int> get visibleEnabledOrderedIndexs =>
orders.where((e) => isVisibleEnabled[e]).toList();
List<Peer> _selectedPeers = List.empty(growable: true); List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers; List<Peer> get selectedPeers => _selectedPeers;
bool _multiSelectionMode = false; bool _multiSelectionMode = false;
@ -53,7 +72,7 @@ class PeerTabModel with ChangeNotifier {
PeerTabModel(this.parent) { PeerTabModel(this.parent) {
// visible // visible
try { try {
final option = bind.getLocalFlutterOption(k: 'peer-tab-visible'); final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
if (option.isNotEmpty) { if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option); List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == _isVisible.length) { if (decodeList.length == _isVisible.length) {
@ -67,13 +86,37 @@ class PeerTabModel with ChangeNotifier {
} catch (e) { } catch (e) {
debugPrint("failed to get peer tab visible list:$e"); debugPrint("failed to get peer tab visible list:$e");
} }
// order
try {
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
if (option.isNotEmpty) {
List<dynamic> 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 // init currentTab
_currentTab = _currentTab =
int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
if (_currentTab < 0 || _currentTab >= tabNames.length) { if (_currentTab < 0 || _currentTab >= maxTabCount) {
_currentTab = 0; _currentTab = 0;
} }
_trySetCurrentTabToFirstVisible(); _trySetCurrentTabToFirstVisibleEnabled();
} }
setCurrentTab(int index) { setCurrentTab(int index) {
@ -87,15 +130,13 @@ class PeerTabModel with ChangeNotifier {
if (index >= 0 && index < tabNames.length) { if (index >= 0 && index < tabNames.length) {
return translate(tabNames[index]); return translate(tabNames[index]);
} }
assert(false);
return index.toString(); return index.toString();
} }
IconData tabIcon(int index) { IconData tabIcon(int index) {
if (index >= 0 && index < tabNames.length) { if (index >= 0 && index < icons.length) {
return icons[index]; return icons[index];
} }
assert(false);
return Icons.help; return Icons.help;
} }
@ -171,29 +212,54 @@ class PeerTabModel with ChangeNotifier {
} }
setTabVisible(int index, bool visible) { setTabVisible(int index, bool visible) {
if (index >= 0 && index < _isVisible.length) { if (index >= 0 && index < maxTabCount) {
if (_isVisible[index] != visible) { if (_isVisible[index] != visible) {
_isVisible[index] = visible; _isVisible[index] = visible;
if (index == _currentTab && !visible) { if (index == _currentTab && !visible) {
_trySetCurrentTabToFirstVisible(); _trySetCurrentTabToFirstVisibleEnabled();
} else if (visible && visibleIndexs.length == 1) { } else if (visible && visibleEnabledOrderedIndexs.length == 1) {
_currentTab = index; _currentTab = index;
} }
try { try {
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: 'peer-tab-visible', v: jsonEncode(_isVisible)); k: kPeerTabVisible, v: jsonEncode(_isVisible));
} catch (_) {} } catch (_) {}
notifyListeners(); notifyListeners();
} }
} }
} }
_trySetCurrentTabToFirstVisible() { _trySetCurrentTabToFirstVisibleEnabled() {
if (!_isVisible[_currentTab]) { if (!visibleEnabledOrderedIndexs.contains(_currentTab)) {
int firstVisible = _isVisible.indexWhere((e) => e); if (visibleEnabledOrderedIndexs.isNotEmpty) {
if (firstVisible >= 0) { _currentTab = visibleEnabledOrderedIndexs.first;
_currentTab = firstVisible;
} }
} }
} }
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();
}
}
} }