From 880a0d4209eb1d74d47d4e61fd7275a38b4a19ba Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 11 Dec 2022 21:40:35 +0800 Subject: [PATCH] add group peer card Signed-off-by: 21pages --- flutter/lib/common.dart | 9 +- flutter/lib/common/hbbs/hbbs.dart | 39 ++ flutter/lib/common/widgets/address_book.dart | 7 +- flutter/lib/common/widgets/my_group.dart | 183 +++++++++ flutter/lib/common/widgets/peer_card.dart | 39 ++ flutter/lib/common/widgets/peer_tab_page.dart | 353 ++++++++++++------ flutter/lib/common/widgets/peers_view.dart | 18 + .../desktop/pages/desktop_setting_page.dart | 22 +- flutter/lib/main.dart | 1 + flutter/lib/mobile/pages/settings_page.dart | 1 - flutter/lib/models/ab_model.dart | 12 +- flutter/lib/models/group_model.dart | 139 +++++++ flutter/lib/models/model.dart | 5 +- flutter/lib/models/user_model.dart | 88 ++--- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/gr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sq.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 42 files changed, 777 insertions(+), 195 deletions(-) create mode 100644 flutter/lib/common/hbbs/hbbs.dart create mode 100644 flutter/lib/common/widgets/my_group.dart create mode 100644 flutter/lib/models/group_model.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0f5502f54..d8a6cd30f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -99,22 +99,28 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.highlight, }); final Color? border; + final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + highlight: Color(0xFF3F3F3F), ); @override - ThemeExtension copyWith({Color? border}) { + ThemeExtension copyWith( + {Color? border, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + highlight: highlight ?? this.highlight, ); } @@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + highlight: Color.lerp(highlight, other.highlight, t), ); } } diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 000000000..856f88d20 --- /dev/null +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,39 @@ +import 'package:flutter_hbb/models/peer_model.dart'; + +class UserPayload { + String name = ''; + String email = ''; + String note = ''; + int? status; + String grp = ''; + bool is_admin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + status = json['status'], + grp = json['grp'] ?? '', + is_admin = json['is_admin'] == true; +} + +class PeerPayload { + String id = ''; + String info = ''; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = json['info'] ?? '', + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({"id": p.id}); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 799b0be67..fbeca25b2 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -28,7 +28,6 @@ class _AddressBookState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb()); } @override @@ -45,11 +44,7 @@ class _AddressBookState extends State { handleLogin() { // TODO refactor login dialog for desktop and mobile if (isDesktop) { - loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }); + loginDialog(); } else { showLogin(gFFI.dialogManager); } diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 000000000..77ddf779b --- /dev/null +++ b/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + static final RxString selectedUser = ''.obs; + static final RxString searchUserText = ''.obs; + static TextEditingController searchUserController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.groupModel.userLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.groupModel.userLoadError.isNotEmpty) { + return _buildShowError(gFFI.groupModel.userLoadError.value); + } + return Row( + children: [ + _buildLeftDesktop(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ); + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.groupModel.pull(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildLeftDesktop() { + return Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + ], + ); + } + + Widget _buildLeftHeader() { + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchUserText.value = value; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + hintText: translate("Search"), + hintStyle: + TextStyle(fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + )), + ], + ); + } + + Widget _buildUserContacts() { + return Obx(() { + return Column( + children: gFFI.groupModel.users + .where((p0) { + if (searchUserText.isNotEmpty) { + return p0.name.contains(searchUserText.value); + } + return true; + }) + .map((e) => _buildUserItem(e.name)) + .toList()); + }); + } + + Widget _buildUserItem(String username) { + return InkWell(onTap: () { + if (selectedUser.value != username) { + selectedUser.value = username; + gFFI.groupModel.pullUserPeers(username); + } + }, child: Obx( + () { + bool selected = selectedUser.value == username; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16) + .marginOnly(right: 4), + Expanded(child: Text(username)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12); + } +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 449b67092..44f82575e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -321,6 +321,7 @@ enum CardType { fav, lan, ab, + grp, } abstract class BasePeerCard extends StatelessWidget { @@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget { case CardType.ab: gFFI.abModel.pullAb(); break; + case CardType.grp: + gFFI.groupModel.pull(); + break; } } } @@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard { } } +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + cardType: CardType.grp, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + return menuItems; + } +} + void _rdpDialog(String id, CardType card) async { String port, username; if (card == CardType.ab) { diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 1711e7b72..f6f5c0403 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -4,6 +4,7 @@ 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/my_group.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; @@ -16,6 +17,151 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; +const int groupTabIndex = 4; + +class StatePeerTab { + final RxInt currentTab = 0.obs; + static const List tabIndexs = [0, 1, 2, 3, 4]; + List tabOrder = List.empty(growable: true); + final RxList visibleTabOrder = RxList.empty(growable: true); + int tabHiddenFlag = 0; + final RxList tabNames = [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book'), + translate('Group'), + ].obs; + + StatePeerTab._() { + tabHiddenFlag = (int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0); + currentTab.value = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabIndexs.contains(currentTab.value)) { + currentTab.value = tabIndexs[0]; + } + tabOrder = tabIndexs.toList(); + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == tabOrder.length && + tabOrder.every((e) => list.contains(e))) { + tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + visibleTabOrder.value = tabOrder.where((e) => !isTabHidden(e)).toList(); + visibleTabOrder.remove(groupTabIndex); + } + static final StatePeerTab instance = StatePeerTab._(); + + check() { + List oldOrder = visibleTabOrder; + if (filterGroupCard()) { + visibleTabOrder.remove(groupTabIndex); + if (currentTab.value == groupTabIndex) { + currentTab.value = + visibleTabOrder.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; + bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: currentTab.value.toString()); + } + } else { + if (gFFI.userModel.isAdmin.isFalse && + gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = translate('Group'); + } + if (isTabHidden(groupTabIndex)) { + visibleTabOrder.remove(groupTabIndex); + } else { + if (!visibleTabOrder.contains(groupTabIndex)) { + addTabInOrder(visibleTabOrder, groupTabIndex); + } + } + if (visibleTabOrder.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + currentTab.value = groupTabIndex; + } + } + if (oldOrder != visibleTabOrder) { + saveTabOrder(); + } + } + + bool isTabHidden(int tabindex) { + return tabHiddenFlag & (1 << tabindex) != 0; + } + + bool filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + addTabInOrder(List list, int tabIndex) { + if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } + + saveTabOrder() { + var list = statePeerTab.visibleTabOrder.toList(); + var left = tabOrder + .where((e) => !statePeerTab.visibleTabOrder.contains(e)) + .toList(); + for (var t in left) { + addTabInOrder(list, t); + } + statePeerTab.tabOrder = list; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(list)); + } +} + +final statePeerTab = StatePeerTab.instance; + class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -23,10 +169,9 @@ class PeerTabPage extends StatefulWidget { } class _TabEntry { - final String name; final Widget widget; final Function() load; - _TabEntry(this.name, this.widget, this.load); + _TabEntry(this.widget, this.load); } EdgeInsets? _menuPadding() { @@ -35,65 +180,36 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - late final RxInt tabHiddenFlag; - late final RxString currentTab; - late final RxList visibleOrderedTabs; final List<_TabEntry> entries = [ _TabEntry( - 'Recent Sessions', RecentPeersView( menuPadding: _menuPadding(), ), bind.mainLoadRecentPeers), _TabEntry( - 'Favorites', FavoritePeersView( menuPadding: _menuPadding(), ), bind.mainLoadFavPeers), _TabEntry( - 'Discovered', DiscoveredPeersView( menuPadding: _menuPadding(), ), bind.mainDiscover), _TabEntry( - 'Address Book', AddressBook( menuPadding: _menuPadding(), ), () => {}), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + () => {}), ]; @override void initState() { - tabHiddenFlag = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0) - .obs; - currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; - visibleOrderedTabs = entries - .where((e) => !isTabHidden(e.name)) - .map((e) => e.name) - .toList() - .obs; - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = json.map((e) => e.toString()).toList(); - if (list.length == visibleOrderedTabs.length && - visibleOrderedTabs.every((e) => list.contains(e))) { - visibleOrderedTabs.value = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); @@ -105,10 +221,11 @@ class _PeerTabPageState extends State super.initState(); } - Future handleTabSelection(String tabName) async { - currentTab.value = tabName; - await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); - entries.firstWhereOrNull((e) => e.name == tabName)?.load(); + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + statePeerTab.currentTab.value = tabIndex; + entries[tabIndex].load(); + } } @override @@ -148,25 +265,26 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; + statePeerTab.visibleTabOrder + .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Obx(() { int indexCounter = -1; return ReorderableListView( buildDefaultDragHandles: false, onReorder: (oldIndex, newIndex) { - var list = visibleOrderedTabs.toList(); + var list = statePeerTab.visibleTabOrder.toList(); if (oldIndex < newIndex) { newIndex -= 1; } - final String item = list.removeAt(oldIndex); + final int item = list.removeAt(oldIndex); list.insert(newIndex, item); - bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(list)); - visibleOrderedTabs.value = list; + statePeerTab.visibleTabOrder.value = list; + statePeerTab.saveTabOrder(); }, scrollDirection: Axis.horizontal, shrinkWrap: true, scrollController: ScrollController(), - children: visibleOrderedTabs.map((t) { + children: statePeerTab.visibleTabOrder.map((t) { indexCounter++; return ReorderableDragStartListener( key: ValueKey(t), @@ -175,7 +293,7 @@ class _PeerTabPageState extends State child: Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( - color: currentTab.value == t + color: statePeerTab.currentTab.value == t ? Theme.of(context).backgroundColor : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), @@ -183,16 +301,22 @@ class _PeerTabPageState extends State child: Align( alignment: Alignment.center, child: Text( - translate(t), + statePeerTab.tabNames[t], // TODO textAlign: TextAlign.center, style: TextStyle( height: 1, fontSize: 14, - color: currentTab.value == t ? textColor : textColor + color: statePeerTab.currentTab.value == t + ? textColor + : textColor ?..withOpacity(0.5)), ), )), - onTap: () async => await handleTabSelection(t), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, ), ); }).toList()); @@ -201,13 +325,24 @@ class _PeerTabPageState extends State Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; + statePeerTab.visibleTabOrder + .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Expanded( - child: Obx(() => - entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ?? - visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - ))).marginSymmetric(vertical: verticalMargin), - ); + child: Obx(() { + if (statePeerTab.visibleTabOrder.isEmpty) { + return visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (statePeerTab.visibleTabOrder + .contains(statePeerTab.currentTab.value)) { + return entries[statePeerTab.currentTab.value].widget; + } else { + statePeerTab.currentTab.value = statePeerTab.visibleTabOrder[0]; + return entries[statePeerTab.currentTab.value].widget; + } + } + }).marginSymmetric(vertical: verticalMargin)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -240,22 +375,14 @@ class _PeerTabPageState extends State ); } - bool isTabHidden(String name) { - int index = entries.indexWhere((e) => e.name == name); - if (index >= 0) { - return tabHiddenFlag & (1 << index) != 0; - } - assert(false); - return false; - } - adjustTab() { - if (visibleOrderedTabs.isNotEmpty) { - if (!visibleOrderedTabs.contains(currentTab.value)) { - handleTabSelection(visibleOrderedTabs[0]); + if (statePeerTab.visibleTabOrder.isNotEmpty) { + if (!statePeerTab.visibleTabOrder + .contains(statePeerTab.currentTab.value)) { + handleTabSelection(statePeerTab.visibleTabOrder[0]); } } else { - currentTab.value = ''; + statePeerTab.currentTab.value = 0; } } @@ -278,47 +405,53 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - final List menu = entries.asMap().entries.map((e) { - int bitMask = 1 << e.key; - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(e.value.name), - getter: () async { - return tabHiddenFlag.value & bitMask == 0; - }, - setter: (show) async { - if (show) { - tabHiddenFlag.value &= ~bitMask; - } else { - tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2)); - visibleOrderedTabs.removeWhere((e) => isTabHidden(e)); - visibleOrderedTabs.addAll(entries - .where((e) => - !visibleOrderedTabs.contains(e.name) && - !isTabHidden(e.name)) - .map((e) => e.name) - .toList()); - await bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs)); - cancelFunc(); - adjustTab(); - }); - }).toList(); - 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(), - ); + return Obx(() { + final List menu = List.empty(growable: true); + for (int i = 0; i < statePeerTab.tabNames.length; i++) { + if (i == groupTabIndex && statePeerTab.filterGroupCard()) { + continue; + } + int bitMask = 1 << i; + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: statePeerTab.tabNames[i], + getter: () async { + return statePeerTab.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + if (show) { + statePeerTab.tabHiddenFlag &= ~bitMask; + } else { + statePeerTab.tabHiddenFlag |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', + v: statePeerTab.tabHiddenFlag.toRadixString(2)); + statePeerTab.visibleTabOrder + .removeWhere((e) => statePeerTab.isTabHidden(e)); + for (int j = 0; j < statePeerTab.tabNames.length; j++) { + if (!statePeerTab.visibleTabOrder.contains(j) && + !statePeerTab.isTabHidden(j)) { + statePeerTab.visibleTabOrder.add(j); + } + } + statePeerTab.saveTabOrder(); + cancelFunc(); + adjustTab(); + })); + } + 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()); + }); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 6e52bfeb8..9c98f24b8 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView { return true; } } + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'my group peer', + loadEvent: 'load_my_group_peers', + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 06cabebe7..422b4d3e1 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1059,21 +1059,13 @@ class _AccountState extends State<_Account> { } Widget accountAction() { - return _futureBuilder(future: () async { - return await gFFI.userModel.getUserName(); - }(), hasData: (_) { - return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', - () => { - gFFI.userModel.userName.value.isEmpty - ? loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }) - : gFFI.userModel.logOut() - })); - }); + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2015c02b2..6d09ef139 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -117,6 +117,7 @@ void runMainApp(bool startService) async { // await windowManager.ensureInitialized(); gFFI.serverModel.startService(); } + gFFI.userModel.refreshCurrentUser(); runApp(App()); // restore the location of the main window before window hide or show await restoreWindowPosition(WindowType.Main); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 269439b1d..8c7cdb5c7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -547,7 +547,6 @@ void showLogin(OverlayDialogManager dialogManager) { error = resp['error']; return; } - gFFI.abModel.pullAb(); } close(); }, diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index ab5a7cb80..d8a0e8f99 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -21,10 +21,8 @@ class AbModel { AbModel(this.parent); - FFI? get _ffi => parent.target; - Future pullAb() async { - if (_ffi!.userModel.userName.isEmpty) return; + if (gFFI.userModel.userName.isEmpty) return; abLoading.value = true; abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; @@ -63,7 +61,8 @@ class AbModel { return null; } - void reset() { + Future reset() async { + await bind.mainSetLocalOption(key: "selected-tags", value: ''); tags.clear(); peers.clear(); } @@ -188,9 +187,4 @@ class AbModel { await pushAb(); } } - - void clear() { - peers.clear(); - tags.clear(); - } } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart new file mode 100644 index 000000000..4dfcf189b --- /dev/null +++ b/flutter/lib/models/group_model.dart @@ -0,0 +1,139 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class GroupModel { + final RxBool userLoading = false.obs; + final RxString userLoadError = "".obs; + final RxBool peerLoading = false.obs; //to-do: not used + final RxString peerLoadError = "".obs; + final RxList users = RxList.empty(growable: true); + final RxList peerPayloads = RxList.empty(growable: true); + final RxList peersShow = RxList.empty(growable: true); + WeakReference parent; + + GroupModel(this.parent); + + Future reset() async { + userLoading.value = false; + userLoadError.value = ""; + peerLoading.value = false; + peerLoadError.value = ""; + users.clear(); + peerPayloads.clear(); + peersShow.clear(); + } + + Future pull() async { + await reset(); + if (gFFI.userModel.userName.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + statePeerTab.check(); + return; + } + userLoading.value = true; + userLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/users"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 1; + do { + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + if (gFFI.userModel.isAdmin.isFalse) + 'grp': gFFI.userModel.groupName.value, + }); + current += pageSize; + final resp = await http.get(uri, headers: await getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + users.add(UserPayload.fromJson(user)); + } + } + } + } + } + } while (current < total); + } catch (err) { + debugPrint('$err'); + userLoadError.value = err.toString(); + } finally { + userLoading.value = false; + statePeerTab.check(); + } + } + + Future pullUserPeers(String username) async { + peerPayloads.clear(); + peersShow.clear(); + peerLoading.value = true; + peerLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/peers"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 1; + do { + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + 'user_name': username + }); + current += pageSize; + final resp = await http.get(uri, headers: await getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final p in data) { + final peer = PeerPayload.fromJson(p); + peerPayloads.add(peer); + peersShow.add(PeerPayload.toPeer(peer)); + } + } + } + } + } + } while (current < total); + } catch (err) { + debugPrint('$err'); + peerLoadError.value = err.toString(); + } finally { + peerLoading.value = false; + } + } +} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 37c246fe1..3659e8d58 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -1221,6 +1222,7 @@ class FFI { late final ChatModel chatModel; // session late final FileModel fileModel; // session late final AbModel abModel; // global + late final GroupModel groupModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // recording @@ -1234,8 +1236,9 @@ class FFI { serverModel = ServerModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); - abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e6065743c..751b01637 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -10,17 +11,19 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = ''.obs; + final RxString userName = ''.obs; + final RxString groupName = ''.obs; + final RxBool isAdmin = false.obs; WeakReference parent; - UserModel(this.parent) { - refreshCurrentUser(); - } + UserModel(this.parent); void refreshCurrentUser() async { - await getUserName(); final token = bind.mainGetLocalOption(key: 'access_token'); - if (token == '') return; + if (token == '') { + await _updateOtherModels(); + return; + } final url = await bind.mainGetApiServer(); final body = { 'id': await bind.mainGetMyId(), @@ -35,55 +38,42 @@ class UserModel { body: json.encode(body)); final status = response.statusCode; if (status == 401 || status == 400) { - resetToken(); + reset(); return; } - await _parseResp(response.body); + final data = json.decode(response.body); + final error = data['error']; + if (error != null) { + throw error; + } + await _parseUserInfo(data); } catch (e) { print('Failed to refreshCurrentUser: $e'); + } finally { + await _updateOtherModels(); } } - void resetToken() async { + Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: ''); + await gFFI.abModel.reset(); + await gFFI.groupModel.reset(); userName.value = ''; + groupName.value = ''; + statePeerTab.check(); } - Future _parseResp(String body) async { - final data = json.decode(body); - final error = data['error']; - if (error != null) { - return error!; - } - final token = data['access_token']; - if (token != null) { - await bind.mainSetLocalOption(key: 'access_token', value: token); - } - final info = data['user']; - if (info != null) { - final value = json.encode(info); - await bind.mainSetOption(key: 'user_info', value: value); - userName.value = info['name']; - } - return ''; + Future _parseUserInfo(dynamic userinfo) async { + bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); + userName.value = userinfo['name'] ?? ''; + groupName.value = userinfo['grp'] ?? ''; + isAdmin.value = userinfo['is_admin'] == true; } - Future getUserName() async { - if (userName.isNotEmpty) { - return userName.value; - } - final userInfo = bind.mainGetLocalOption(key: 'user_info'); - if (userInfo.trim().isEmpty) { - return ''; - } - final m = jsonDecode(userInfo); - if (m == null) { - userName.value = ''; - } else { - userName.value = m['name'] ?? ''; - } - return userName.value; + Future _updateOtherModels() async { + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); } Future logOut() async { @@ -95,13 +85,7 @@ class UserModel { 'uuid': await bind.mainGetUuid(), }, headers: await getHttpHeaders()); - await Future.wait([ - bind.mainSetLocalOption(key: 'access_token', value: ''), - bind.mainSetLocalOption(key: 'user_info', value: ''), - bind.mainSetLocalOption(key: 'selected-tags', value: ''), - ]); - parent.target?.abModel.clear(); - userName.value = ''; + await reset(); gFFI.dialogManager.dismissByTag(tag); } @@ -119,12 +103,12 @@ class UserModel { final body = jsonDecode(resp.body); bind.mainSetLocalOption( key: 'access_token', value: body['access_token'] ?? ''); - bind.mainSetLocalOption( - key: 'user_info', value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ''; + await _parseUserInfo(body['user']); return body; } catch (err) { return {'error': '$err'}; + } finally { + await _updateOtherModels(); } } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 70190729c..720c448e9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index daa2af065..bc5708987 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -401,5 +401,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), + ("Group", "小组"), + ("Search", "搜索"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 33c6492f7..fe0087d40 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1aa53ca57..a17f26918 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 6bd2a9bbe..877b5c9ac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), ("Add to Address Book", "Zum Adressbuch hinzufügen"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c2748a9bc..d0705af1b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 8690f8b3f..3e7def305 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Add to Address Book", "Añadir a la libreta de direcciones"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1e1689cbb..88f2e0841 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), ("Add to Address Book", "افزودن به دفترچه آدرس"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c3d241bf8..6339919aa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index ecabd8f31..98dc87470 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d0f2f4412..ee77b53e6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b8f9e392d..173a21e31 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b7d449a62..84a41a96a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), ("Add to Address Book", "Aggiungi alla rubrica"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4ca33e76a..e9914c0fe 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 93338165b..6f514f706 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a7d6f299d..69c4115ca 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d3f991d44..1a6fceb12 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4a457218c..f279d6e7a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af59e4f2e..18b803ec3 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8d990fc66..629308f89 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), ("Add to Address Book", "Добавить в адресную книгу"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 13672d086..7f7c865cb 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5ec59c4be..132b8fcdc 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 1feb5d55e..b68537a65 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 6993cb43c..99033faea 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 7b66af60e..32cd4a374 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6f0e8806b..2ff28f970 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", "右鍵選擇選項卡"), ("Add to Address Book", "添加到地址簿"), + ("Group", "小組"), + ("Search", "搜索"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92fd2db8a..854514cfc 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1d32aad5e..0667e2629 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); }