diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0f5502f5..d8a6cd30 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 00000000..856f88d2 --- /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 799b0be6..fbeca25b 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 00000000..77ddf779 --- /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 449b6709..44f82575 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 1711e7b7..f6f5c040 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 6e52bfeb..9c98f24b 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 06cabebe..422b4d3e 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 2015c02b..6d09ef13 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 269439b1..8c7cdb5c 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 ab5a7cb8..d8a0e8f9 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 00000000..4dfcf189 --- /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 37c246fe..3659e8d5 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 e6065743..751b0163 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 70190729..720c448e 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 daa2af06..bc570898 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 33c6492f..fe0087d4 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 1aa53ca5..a17f2691 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 6bd2a9bb..877b5c9a 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 c2748a9b..d0705af1 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 8690f8b3..3e7def30 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 1e1689cb..88f2e084 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 c3d241bf..6339919a 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 ecabd8f3..98dc8747 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 d0f2f441..ee77b53e 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 b8f9e392..173a21e3 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 b7d449a6..84a41a96 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 4ca33e76..e9914c0f 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 93338165..6f514f70 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 a7d6f299..69c4115c 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 d3f991d4..1a6fceb1 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 4a457218..f279d6e7 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 af59e4f2..18b803ec 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 8d990fc6..629308f8 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 13672d08..7f7c865c 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 5ec59c4b..132b8fcd 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 1feb5d55..b68537a6 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 6993cb43..99033fae 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 7b66af60..32cd4a37 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 6f0e8806..2ff28f97 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 92fd2db8..854514cf 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 1d32aad5..0667e262 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(); }