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) {
final model = Provider.of<PeerTabModel>(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<PeerTabPage>
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<PeerTabModel>(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<PeerTabPage>
void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel;
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(
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<PeerTabPage>
Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch(
final menu = List<MenuEntrySwitchSync>.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<PeerTabPage>
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<PeerTabPage>
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<PeerTabPage>
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;

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 Switch2Setter = Future<void> Function(bool);

View File

@ -21,24 +21,43 @@ class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent;
int get currentTab => _currentTab;
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',
'Favorites',
if (!isWeb) 'Discovered',
if (!(bind.isDisableAb() || bind.isDisableAccount())) 'Address book',
if (!bind.isDisableAccount()) 'Group',
'Discovered',
'Address book',
'Group',
];
final List<IconData> icons = [
static const List<IconData> 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<bool> _isVisible = List.filled(5, true, growable: false);
List<bool> get isVisible => _isVisible;
List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList();
List<bool> isEnabled = List.from([
true,
true,
!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> 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<dynamic> 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<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
_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();
}
}
}