From 09d380ba8f04478520087302ed7cb479acd40e25 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 14 Sep 2023 16:30:45 +0800 Subject: [PATCH 1/2] allow hide peer tab Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 116 ++++++++++++++++-- flutter/lib/models/peer_tab_model.dart | 48 ++++++++ src/lang/lv.rs | 3 - 3 files changed, 157 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index cb5413ba1..acd98eea0 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,3 +1,6 @@ +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/dialog.dart'; @@ -6,6 +9,9 @@ import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; @@ -61,6 +67,7 @@ class _PeerTabPageState extends State ({dynamic hint}) => gFFI.groupModel.pull(force: hint == null), ), ]; + RelativeRect? mobileTabContextMenuPos; @override void initState() { @@ -100,7 +107,9 @@ class _PeerTabPageState extends State child: selectionWrap(Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _createSwitchBar(context)), + Expanded( + child: + visibleContextMenuListener(_createSwitchBar(context))), const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), _createRefresh(), _createMultiSelection(), @@ -145,7 +154,7 @@ class _PeerTabPageState extends State return ListView( scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - children: model.indexs.map((t) { + children: model.visibleIndexs.map((t) { final selected = model.currentTab == t; final color = selected ? MyTheme.tabbar(context).selectedTextColor @@ -164,8 +173,10 @@ class _PeerTabPageState extends State decoration: selected ? decoBorder : (hover.value ? deco : null), child: Tooltip( + preferBelow: false, message: model.tabTooltip(t, gFFI.groupModel.groupName.value), + onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, child: Icon(model.tabIcon(t), color: color), ).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4), @@ -182,14 +193,15 @@ class _PeerTabPageState extends State Widget _createPeersView() { final model = Provider.of(context); Widget child; - if (model.indexs.isEmpty) { - child = Center( - child: Text(translate('Right click to select tabs')), - ); + if (model.visibleIndexs.isEmpty) { + child = visibleContextMenuListener(Row( + children: [Expanded(child: InkWell())], + )); } else { - if (model.indexs.contains(model.currentTab)) { + if (model.visibleIndexs.contains(model.currentTab)) { child = entries[model.currentTab].widget; } else { + debugPrint("should not happen! currentTab not in visibleIndexs"); Future.delayed(Duration.zero, () { model.setCurrentTab(model.indexs[0]); }); @@ -268,6 +280,96 @@ class _PeerTabPageState extends State ); } + void mobileShowTabVisibilityMenu() { + final model = gFFI.peerTabModel; + final items = List.empty(growable: true); + for (int i = 0; i < model.tabNames.length; i++) { + items.add(PopupMenuItem( + height: kMinInteractiveDimension * 0.8, + onTap: () => model.setTabVisible(i, !model.isVisible[i]), + child: Row( + children: [ + Checkbox( + value: model.isVisible[i], + onChanged: (_) { + model.setTabVisible(i, !model.isVisible[i]); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }), + Expanded( + child: + Text(model.tabTooltip(i, gFFI.groupModel.groupName.value))), + ], + ), + )); + } + if (mobileTabContextMenuPos != null) { + showMenu( + context: context, position: mobileTabContextMenuPos!, items: items); + } + } + + Widget visibleContextMenuListener(Widget child) { + if (isMobile) { + return GestureDetector( + onLongPressDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onLongPressUp: () { + mobileShowTabVisibilityMenu(); + }, + child: child, + ); + } else { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + final model = Provider.of(context); + final menu = List.empty(growable: true); + for (int i = 0; i < model.tabNames.length; i++) { + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: model.tabTooltip(i, gFFI.groupModel.groupName.value), + getter: () async { + return model.isVisible[i]; + }, + setter: (show) async { + model.setTabVisible(i, show); + cancelFunc(); + })); + } + 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()); + } + Widget createMultiSelectionBar() { final model = Provider.of(context); return Row( diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 2e65e64bd..2fdf9b449 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -36,7 +37,10 @@ class PeerTabModel with ChangeNotifier { IconFont.addressBook, Icons.group, ]; + final List _isVisible = List.filled(4, true, growable: false); + List get isVisible => _isVisible; List get indexs => List.generate(tabNames.length, (index) => index); + List get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); List _selectedPeers = List.empty(growable: true); List get selectedPeers => _selectedPeers; bool _multiSelectionMode = false; @@ -49,12 +53,29 @@ class PeerTabModel with ChangeNotifier { String get lastId => _lastId; PeerTabModel(this.parent) { + // visible + try { + final option = bind.getLocalFlutterOption(k: 'peer-tab-visible'); + if (option.isNotEmpty) { + List decodeList = jsonDecode(option); + if (decodeList.length == _isVisible.length) { + for (int i = 0; i < _isVisible.length; i++) { + if (decodeList[i] is bool) { + _isVisible[i] = decodeList[i]; + } + } + } + } + } catch (e) { + debugPrint("failed to get peer tab visible list:$e"); + } // init currentTab _currentTab = int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; if (_currentTab < 0 || _currentTab >= tabNames.length) { _currentTab = 0; } + _trySetCurrentTabToFirstVisible(); } setCurrentTab(int index) { @@ -158,4 +179,31 @@ class PeerTabModel with ChangeNotifier { } } } + + setTabVisible(int index, bool visible) { + if (index >= 0 && index < _isVisible.length) { + if (_isVisible[index] != visible) { + _isVisible[index] = visible; + if (index == _currentTab && !visible) { + _trySetCurrentTabToFirstVisible(); + } else if (visible && visibleIndexs.length == 1) { + _currentTab = index; + } + try { + bind.setLocalFlutterOption( + k: 'peer-tab-visible', v: jsonEncode(_isVisible)); + } catch (_) {} + notifyListeners(); + } + } + } + + _trySetCurrentTabToFirstVisible() { + if (!_isVisible[_currentTab]) { + int firstVisible = _isVisible.indexWhere((e) => e); + if (firstVisible >= 0) { + _currentTab = firstVisible; + } + } + } } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index cc0297297..64987f268 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -57,7 +57,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID serveris"), ("Relay Server", "Releja serveris"), ("API Server", "API serveris"), - ("Key", "Atslēga"), ("invalid_http", "jāsākas ar http:// vai https://"), ("Invalid IP", "Nederīga IP"), ("Invalid format", "Nederīgs formāts"), @@ -297,7 +296,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This file exists, skip or overwrite this file?", "Šis fails pastāv, izlaist vai pārrakstīt šo failu?"), ("Quit", "Iziet"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("Help", "Palīdzība"), ("Failed", "Neizdevās"), ("Succeeded", "Izdevās"), @@ -481,7 +479,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Me", "Es"), ("identical_file_tip", "Šis fails ir identisks sesijas failam."), ("show_monitors_tip", "Rādīt monitorus rīkjoslā"), - ("enter_rustdesk_passwd_tip", "Ievadiet RustDesk paroli"), ("View Mode", "Skatīšanas režīms"), ("login_linux_tip", "Jums ir jāpiesakās attālajā Linux kontā, lai iespējotu X darbvirsmas sesiju"), ("verify_rustdesk_password_tip", "Pārbaudīt RustDesk paroli"), From b2a4f11e0bf23508d492bc7b21492814bdf66d4b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 14 Sep 2023 10:17:03 +0800 Subject: [PATCH 2/2] enable group, show accessible users and peers Signed-off-by: 21pages --- flutter/lib/common.dart | 56 ++++ flutter/lib/common/hbbs/hbbs.dart | 43 ++- flutter/lib/common/widgets/address_book.dart | 72 +---- flutter/lib/common/widgets/my_group.dart | 58 ++-- flutter/lib/common/widgets/peer_card.dart | 13 +- flutter/lib/common/widgets/peer_tab_page.dart | 31 +- flutter/lib/common/widgets/peers_view.dart | 23 +- flutter/lib/main.dart | 2 + flutter/lib/models/ab_model.dart | 18 +- flutter/lib/models/group_model.dart | 270 +++++++++++------- flutter/lib/models/native_model.dart | 10 +- flutter/lib/models/peer_model.dart | 58 ++-- flutter/lib/models/peer_tab_model.dart | 18 +- flutter/lib/models/user_model.dart | 12 +- libs/hbb_common/src/config.rs | 102 +++++++ src/flutter_ffi.rs | 18 ++ src/lang/ar.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 53 files changed, 568 insertions(+), 273 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a71bd12b9..af0be2519 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2480,3 +2480,59 @@ String toCapitalized(String s) { } return s.substring(0, 1).toUpperCase() + s.substring(1); } + +Widget buildErrorBanner(BuildContext context, + {required RxBool loading, + required RxString err, + required Function? retry, + required Function close}) { + const double height = 25; + return Obx(() => Offstage( + offstage: !(!loading.value && err.value.isNotEmpty), + child: Center( + child: Container( + height: height, + color: MyTheme.color(context).errorBannerBg, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FittedBox( + child: Icon( + Icons.info, + color: Color.fromARGB(255, 249, 81, 81), + ), + ).marginAll(4), + Flexible( + child: Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: translate(err.value), + child: Text( + translate(err.value), + overflow: TextOverflow.ellipsis, + ), + )).marginSymmetric(vertical: 2), + ), + if (retry != null) + InkWell( + onTap: () { + retry.call(); + }, + child: Text( + translate("Retry"), + style: TextStyle(color: MyTheme.accent), + )).marginSymmetric(horizontal: 5), + FittedBox( + child: InkWell( + onTap: () { + close.call(); + }, + child: Icon(Icons.close).marginSymmetric(horizontal: 5), + ), + ).marginAll(4) + ], + ), + )).marginOnly(bottom: 14), + )); +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 8e5c2d02a..dabb3be8f 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_model.dart'; @@ -48,11 +49,18 @@ class UserPayload { }; return map; } + + Map toGroupCacheJson() { + final Map map = { + 'name': name, + }; + return map; + } } class PeerPayload { String id = ''; - String info = ''; + Map info = {}; int? status; String user = ''; String user_name = ''; @@ -67,7 +75,38 @@ class PeerPayload { note = json['note'] ?? ''; static Peer toPeer(PeerPayload p) { - return Peer.fromJson({"id": p.id, "username": p.user_name}); + return Peer.fromJson({ + "id": p.id, + 'loginName': p.user_name, + "username": p.info['username'] ?? '', + "platform": _platform(p.info['os']), + "hostname": p.info['device_name'], + }); + } + + static String? _platform(dynamic field) { + if (field == null) { + return null; + } + final fieldStr = field.toString(); + List list = fieldStr.split(' / '); + if (list.isEmpty) return null; + final os = list[0]; + switch (os.toLowerCase()) { + case 'windows': + return kPeerPlatformWindows; + case 'linux': + return kPeerPlatformLinux; + case 'macos': + return kPeerPlatformMacOS; + case 'android': + return kPeerPlatformAndroid; + default: + if (fieldStr.toLowerCase().contains('linux')) { + return kPeerPlatformLinux; + } + return null; + } } } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 070c44126..bb5dc5605 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -35,7 +35,7 @@ class _AddressBookState extends State { @override Widget build(BuildContext context) => Obx(() { - if (gFFI.userModel.userName.value.isEmpty) { + if (!gFFI.userModel.isLogin) { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); @@ -49,11 +49,13 @@ class _AddressBookState extends State { children: [ // NOT use Offstage to wrap LinearProgressIndicator if (gFFI.abModel.retrying.value) LinearProgressIndicator(), - _buildErrorBanner( + buildErrorBanner(context, + loading: gFFI.abModel.abLoading, err: gFFI.abModel.pullError, retry: null, close: () => gFFI.abModel.pullError.value = ''), - _buildErrorBanner( + buildErrorBanner(context, + loading: gFFI.abModel.abLoading, err: gFFI.abModel.pushError, retry: () => gFFI.abModel.pushAb(isRetry: true), close: () => gFFI.abModel.pushError.value = ''), @@ -66,61 +68,6 @@ class _AddressBookState extends State { } }); - Widget _buildErrorBanner( - {required RxString err, - required Function? retry, - required Function close}) { - const double height = 25; - return Obx(() => Offstage( - offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty), - child: Center( - child: Container( - height: height, - color: MyTheme.color(context).errorBannerBg, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FittedBox( - child: Icon( - Icons.info, - color: Color.fromARGB(255, 249, 81, 81), - ), - ).marginAll(4), - Flexible( - child: Align( - alignment: Alignment.centerLeft, - child: Tooltip( - message: translate(err.value), - child: Text( - translate(err.value), - overflow: TextOverflow.ellipsis, - ), - )).marginSymmetric(vertical: 2), - ), - if (retry != null) - InkWell( - onTap: () { - retry.call(); - }, - child: Text( - translate("Retry"), - style: TextStyle(color: MyTheme.accent), - )).marginSymmetric(horizontal: 5), - FittedBox( - child: InkWell( - onTap: () { - close.call(); - }, - child: Icon(Icons.close).marginSymmetric(horizontal: 5), - ), - ).marginAll(4) - ], - ), - )).marginOnly(bottom: 14), - )); - } - Widget _buildAddressBookDesktop() { return Row( children: [ @@ -230,11 +177,10 @@ class _AddressBookState extends State { return Expanded( child: Align( alignment: Alignment.topLeft, - child: Obx(() => AddressBookPeersView( - menuPadding: widget.menuPadding, - // ignore: invalid_use_of_protected_member - initPeers: gFFI.abModel.peers.value, - ))), + child: AddressBookPeersView( + menuPadding: widget.menuPadding, + initPeers: gFFI.abModel.peers, + )), ); } diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 2867d3bce..a4d89155a 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -29,49 +29,28 @@ class _MyGroupState extends State { @override Widget build(BuildContext context) { return Obx(() { - // use username to be same with ab - if (gFFI.userModel.userName.value.isEmpty) { + if (!gFFI.userModel.isLogin) { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); - } - return buildBody(context); - }); - } - - Widget buildBody(BuildContext context) { - return Obx(() { - if (gFFI.groupModel.groupLoading.value) { + } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { return const Center( child: CircularProgressIndicator(), ); } - if (gFFI.groupModel.groupLoadError.isNotEmpty) { - return _buildShowError(gFFI.groupModel.groupLoadError.value); - } - if (isDesktop) { - return _buildDesktop(); - } else { - return _buildMobile(); - } + return Column( + children: [ + buildErrorBanner(context, + loading: gFFI.groupModel.groupLoading, + err: gFFI.groupModel.groupLoadError, + retry: null, + close: () => gFFI.groupModel.groupLoadError.value = ''), + Expanded(child: isDesktop ? _buildDesktop() : _buildMobile()) + ], + ); }); } - 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 _buildDesktop() { return Row( children: [ @@ -100,10 +79,9 @@ class _MyGroupState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: Obx(() => MyGroupPeerView( + child: MyGroupPeerView( menuPadding: widget.menuPadding, - // ignore: invalid_use_of_protected_member - initPeers: gFFI.groupModel.peersShow.value))), + initPeers: gFFI.groupModel.peers)), ) ], ); @@ -133,10 +111,9 @@ class _MyGroupState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: Obx(() => MyGroupPeerView( + child: MyGroupPeerView( menuPadding: widget.menuPadding, - // ignore: invalid_use_of_protected_member - initPeers: gFFI.groupModel.peersShow.value))), + initPeers: gFFI.groupModel.peers)), ) ], ); @@ -195,6 +172,7 @@ class _MyGroupState extends State { }, child: Obx( () { bool selected = selectedUser.value == username; + final isMe = username == gFFI.userModel.userName.value; return Container( decoration: BoxDecoration( color: selected ? MyTheme.color(context).highlight : null, @@ -208,7 +186,7 @@ class _MyGroupState extends State { children: [ Icon(Icons.person_rounded, color: Colors.grey, size: 16) .marginOnly(right: 4), - Expanded(child: Text(username)), + Expanded(child: Text(isMe ? translate('Me') : username)), ], ).paddingSymmetric(vertical: 4), ), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f5af94220..f6a6ef40e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1093,7 +1093,7 @@ class MyGroupPeerCard extends BasePeerCard { menuItems.add(_tcpTunnelingAction(context, peer.id)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); - menuItems.add(await _forceAlwaysRelayAction(peer.id)); + // menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (peer.platform == 'Windows') { menuItems.add(_rdpAction(context, peer.id)); } @@ -1101,9 +1101,14 @@ class MyGroupPeerCard extends BasePeerCard { 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)); + // menuItems.add(_renameAction(peer.id)); + // if (await bind.mainPeerHasPassword(id: peer.id)) { + // menuItems.add(_unrememberPasswordAction(peer.id)); + // } + if (gFFI.userModel.userName.isNotEmpty) { + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } return menuItems; } diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index acd98eea0..dccf83c7b 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -111,7 +111,11 @@ class _PeerTabPageState extends State child: visibleContextMenuListener(_createSwitchBar(context))), const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), - _createRefresh(), + _createRefresh( + index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading), + _createRefresh( + index: PeerTabIndex.group, + loading: gFFI.groupModel.groupLoading), _createMultiSelection(), Offstage( offstage: !isDesktop, @@ -170,12 +174,12 @@ class _PeerTabPageState extends State )); return Obx(() => InkWell( child: Container( - decoration: - selected ? decoBorder : (hover.value ? deco : null), + decoration: (hover.value + ? (selected ? decoBorder : deco) + : (selected ? decoBorder : null)), child: Tooltip( preferBelow: false, - message: - model.tabTooltip(t, gFFI.groupModel.groupName.value), + message: model.tabTooltip(t), onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, child: Icon(model.tabIcon(t), color: color), ).paddingSymmetric(horizontal: 4), @@ -212,17 +216,19 @@ class _PeerTabPageState extends State child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); } - Widget _createRefresh() { + Widget _createRefresh( + {required PeerTabIndex index, required RxBool loading}) { + final model = Provider.of(context); final textColor = Theme.of(context).textTheme.titleLarge?.color; return Offstage( - offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index, + offstage: model.currentTab != index.index, child: RefreshWidget( onPressed: () { if (gFFI.peerTabModel.currentTab < entries.length) { entries[gFFI.peerTabModel.currentTab].load(); } }, - spinning: gFFI.abModel.abLoading, + spinning: loading, child: RotatedBox( quarterTurns: 2, child: Tooltip( @@ -297,9 +303,7 @@ class _PeerTabPageState extends State Navigator.pop(context); } }), - Expanded( - child: - Text(model.tabTooltip(i, gFFI.groupModel.groupName.value))), + Expanded(child: Text(model.tabTooltip(i))), ], ), )); @@ -348,7 +352,7 @@ class _PeerTabPageState extends State for (int i = 0; i < model.tabNames.length; i++) { menu.add(MenuEntrySwitch( switchType: SwitchType.scheckbox, - text: model.tabTooltip(i, gFFI.groupModel.groupName.value), + text: model.tabTooltip(i), getter: () async { return model.isVisible[i]; }, @@ -388,6 +392,9 @@ class _PeerTabPageState extends State Widget deleteSelection() { final model = Provider.of(context); + if (model.currentTab == PeerTabIndex.group.index) { + return Offstage(); + } return _hoverAction( context: context, onTap: () { diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 0e4898fc2..28bfb6690 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -35,6 +35,7 @@ class LoadEvent { static const String favorite = 'load_fav_peers'; static const String lan = 'load_lan_peers'; static const String addressBook = 'load_address_book_peers'; + static const String group = 'load_group_peers'; } /// for peer search text, global obs value @@ -312,7 +313,7 @@ abstract class BasePeersView extends StatelessWidget { final String loadEvent; final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; - final List initPeers; + final RxList? initPeers; const BasePeersView({ Key? key, @@ -326,7 +327,7 @@ abstract class BasePeersView extends StatelessWidget { @override Widget build(BuildContext context) { return _PeersView( - peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), + peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers), peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } @@ -343,7 +344,7 @@ class RecentPeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: [], + initPeers: null, ); @override @@ -365,7 +366,7 @@ class FavoritePeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: [], + initPeers: null, ); @override @@ -387,7 +388,7 @@ class DiscoveredPeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: [], + initPeers: null, ); @override @@ -403,7 +404,7 @@ class AddressBookPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController, - required List initPeers}) + required RxList initPeers}) : super( key: key, name: 'address book peer', @@ -435,11 +436,11 @@ class MyGroupPeerView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController, - required List initPeers}) + required RxList initPeers}) : super( key: key, - name: 'my group peer', - loadEvent: 'load_my_group_peers', + name: 'group peer', + loadEvent: LoadEvent.group, peerFilter: filter, peerCardBuilder: (Peer peer) => MyGroupPeerCard( peer: peer, @@ -450,12 +451,12 @@ class MyGroupPeerView extends BasePeersView { static bool filter(Peer peer) { if (gFFI.groupModel.searchUserText.isNotEmpty) { - if (!peer.username.contains(gFFI.groupModel.searchUserText)) { + if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) { return false; } } if (gFFI.groupModel.selectedUser.isNotEmpty) { - if (gFFI.groupModel.selectedUser.value != peer.username) { + if (gFFI.groupModel.selectedUser.value != peer.loginName) { return false; } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 43273c547..d4e81a82d 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -126,6 +126,7 @@ void runMainApp(bool startService) async { bind.pluginListReload(); } gFFI.abModel.loadCache(); + gFFI.groupModel.loadCache(); gFFI.userModel.refreshCurrentUser(); runApp(App()); // Set window option. @@ -154,6 +155,7 @@ void runMobileApp() async { if (isAndroid) androidChannelInit(); platformFFI.syncAndroidServiceAppDirConfigPath(); gFFI.abModel.loadCache(); + gFFI.groupModel.loadCache(); gFFI.userModel.refreshCurrentUser(); runApp(App()); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index cbb7f7471..6968b2f10 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; @@ -115,9 +116,10 @@ class AbModel { _timerCounter = 0; if (pullError.isNotEmpty) { if (statusCode == 401) { - gFFI.userModel.reset(clearAbCache: true); + gFFI.userModel.reset(resetOther: true); } } + platformFFI.tryHandle({'name': LoadEvent.addressBook}); } } @@ -241,7 +243,8 @@ class AbModel { ret = true; _saveCache(); } else { - Map json = _jsonDecodeResp(resp.body, resp.statusCode); + Map json = + _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } else if (resp.statusCode == 200) { @@ -479,11 +482,12 @@ class AbModel { loadCache() async { try { - if (_cacheLoadOnceFlag || abLoading.value) return; + if (_cacheLoadOnceFlag || abLoading.value || initialized) return; _cacheLoadOnceFlag = true; final access_token = bind.mainGetLocalOption(key: 'access_token'); if (access_token.isEmpty) return; final cache = await bind.mainLoadAb(); + if (abLoading.value) return; final data = jsonDecode(cache); if (data == null || data['access_token'] != access_token) return; _deserialize(data); @@ -561,4 +565,12 @@ class AbModel { } }); } + + reset() async { + pullError.value = ''; + pushError.value = ''; + tags.clear(); + peers.clear(); + await bind.mainClearAb(); + } } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index f2592fa2b..6177c5843 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -11,57 +12,74 @@ import 'package:http/http.dart' as http; class GroupModel { final RxBool groupLoading = false.obs; final RxString groupLoadError = "".obs; - final RxString groupId = ''.obs; - RxString groupName = ''.obs; final RxList users = RxList.empty(growable: true); - final RxList peersShow = RxList.empty(growable: true); + final RxList peers = RxList.empty(growable: true); final RxString selectedUser = ''.obs; final RxString searchUserText = ''.obs; WeakReference parent; var initialized = false; + var _cacheLoadOnceFlag = false; + var _statusCode = 200; + + bool get emtpy => users.isEmpty && peers.isEmpty; GroupModel(this.parent); - reset() { - groupName.value = ''; - groupId.value = ''; - users.clear(); - peersShow.clear(); - initialized = false; - } - Future pull({force = true, quiet = false}) async { - /* + if (!gFFI.userModel.isLogin || groupLoading.value) return; if (!force && initialized) return; if (!quiet) { groupLoading.value = true; groupLoadError.value = ""; } - await _pull(); + try { + await _pull(); + } catch (_) {} groupLoading.value = false; initialized = true; - */ + platformFFI.tryHandle({'name': LoadEvent.group}); + if (_statusCode == 401) { + gFFI.userModel.reset(resetOther: true); + } else { + _saveCache(); + } } Future _pull() async { - reset(); - if (bind.mainGetLocalOption(key: 'access_token') == '') { + List tmpUsers = List.empty(growable: true); + if (!await _getUsers(tmpUsers)) { return; } - try { - if (!await _getGroup()) { - reset(); - return; - } - } catch (e) { - debugPrint('$e'); - reset(); + List tmpPeers = List.empty(growable: true); + if (!await _getPeers(tmpPeers)) { return; } + // me first + var index = tmpUsers + .indexWhere((user) => user.name == gFFI.userModel.userName.value); + if (index != -1) { + var user = tmpUsers.removeAt(index); + tmpUsers.insert(0, user); + } + users.value = tmpUsers; + if (!users.any((u) => u.name == selectedUser.value)) { + selectedUser.value = ''; + } + // recover online + final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList(); + peers.value = tmpPeers; + peers + .where((e) => oldOnlineIDs.contains(e.id)) + .map((e) => e.online = true) + .toList(); + groupLoadError.value = ''; + } + + Future _getUsers(List tmpUsers) async { final api = "${await bind.mainGetApiServer()}/api/users"; try { var uri0 = Uri.parse(api); - final pageSize = 20; + final pageSize = 100; var total = 0; int current = 0; do { @@ -74,84 +92,63 @@ class GroupModel { queryParameters: { 'current': current.toString(), 'pageSize': pageSize.toString(), - if (gFFI.userModel.isAdmin.isFalse) 'grp': groupId.value, + 'accessible': '', + 'status': '1', }); final resp = await http.get(uri, headers: getHttpHeaders()); - if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { - Map json = jsonDecode(utf8.decode(resp.bodyBytes)); - if (json.containsKey('error')) { - throw json['error']; + _statusCode = resp.statusCode; + Map json = + _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + if (json['error'] == 'Admin required!') { + throw translate('upgrade_rustdesk_server_pro_to_{1.1.10}_tip'); } else { - if (json.containsKey('total')) { - if (total == 0) total = json['total']; - if (json.containsKey('data')) { - final data = json['data']; - if (data is List) { - for (final user in data) { - final u = UserPayload.fromJson(user); - if (!users.any((e) => e.name == u.name)) { - users.add(u); - } - } + throw json['error']; + } + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + final u = UserPayload.fromJson(user); + int index = tmpUsers.indexWhere((e) => e.name == u.name); + if (index < 0) { + tmpUsers.add(u); + } else { + tmpUsers[index] = u; } } } } } } while (current * pageSize < total); + return true; } catch (err) { - debugPrint('$err'); + debugPrint('get accessible users: $err'); groupLoadError.value = err.toString(); - } finally { - _pullUserPeers(); } - } - - Future _getGroup() async { - final url = await bind.mainGetApiServer(); - final body = { - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid() - }; - try { - final response = await http.post(Uri.parse('$url/api/currentGroup'), - headers: getHttpHeaders(), body: json.encode(body)); - final status = response.statusCode; - if (status == 401 || status == 400) { - return false; - } - final data = json.decode(utf8.decode(response.bodyBytes)); - final error = data['error']; - if (error != null) { - throw error; - } - groupName.value = data['name'] ?? ''; - groupId.value = data['guid'] ?? ''; - return groupId.value.isNotEmpty && groupName.isNotEmpty; - } catch (e) { - debugPrint('$e'); - groupLoadError.value = e.toString(); - } finally {} - return false; } - Future _pullUserPeers() async { - peersShow.clear(); - final api = "${await bind.mainGetApiServer()}/api/peers"; + Future _getPeers(List tmpPeers) async { try { + final api = "${await bind.mainGetApiServer()}/api/peers"; var uri0 = Uri.parse(api); - final pageSize = - 20; // ????????????????????????????????????????????????????? stupid stupis, how about >20 peers + final pageSize = 100; var total = 0; int current = 0; var queryParameters = { 'current': current.toString(), 'pageSize': pageSize.toString(), + 'accessible': '', + 'status': '1', + 'user_status': '1', }; - if (!gFFI.userModel.isAdmin.value) { - queryParameters.addAll({'grp': groupId.value}); - } do { current += 1; var uri = Uri( @@ -161,32 +158,107 @@ class GroupModel { port: uri0.port, queryParameters: queryParameters); final resp = await http.get(uri, headers: getHttpHeaders()); - if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { - Map json = jsonDecode(utf8.decode(resp.bodyBytes)); - if (json.containsKey('error')) { - throw json['error']; - } else { - if (json.containsKey('total')) { - if (total == 0) total = json['total']; - if (json.containsKey('data')) { - final data = json['data']; - if (data is List) { - for (final p in data) { - final peerPayload = PeerPayload.fromJson(p); - final peer = PeerPayload.toPeer(peerPayload); - if (!peersShow.any((e) => e.id == peer.id)) { - peersShow.add(peer); - } - } + _statusCode = resp.statusCode; + + Map json = + _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (total > 1000) { + total = 1000; + } + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final p in data) { + final peerPayload = PeerPayload.fromJson(p); + final peer = PeerPayload.toPeer(peerPayload); + int index = tmpPeers.indexWhere((e) => e.id == peer.id); + if (index < 0) { + tmpPeers.add(peer); + } else { + tmpPeers[index] = peer; + } + if (tmpPeers.length >= 1000) { + break; } } } } } } while (current * pageSize < total); + return true; } catch (err) { - debugPrint('$err'); + debugPrint('get accessible peers: $err'); groupLoadError.value = err.toString(); - } finally {} + } + return false; + } + + Map _jsonDecodeResp(String body, int statusCode) { + try { + Map json = jsonDecode(body); + return json; + } catch (e) { + final err = body.isNotEmpty && body.length < 128 ? body : e.toString(); + if (statusCode != 200) { + throw 'HTTP $statusCode, $err'; + } + throw err; + } + } + + void _saveCache() { + try { + final map = ({ + "access_token": bind.mainGetLocalOption(key: 'access_token'), + "users": users.map((e) => e.toGroupCacheJson()).toList(), + 'peers': peers.map((e) => e.toGroupCacheJson()).toList() + }); + bind.mainSaveGroup(json: jsonEncode(map)); + } catch (e) { + debugPrint('group save:$e'); + } + } + + loadCache() async { + try { + if (_cacheLoadOnceFlag || groupLoading.value || initialized) return; + _cacheLoadOnceFlag = true; + final access_token = bind.mainGetLocalOption(key: 'access_token'); + if (access_token.isEmpty) return; + final cache = await bind.mainLoadGroup(); + if (groupLoading.value) return; + final data = jsonDecode(cache); + if (data == null || data['access_token'] != access_token) return; + users.clear(); + peers.clear(); + if (data['users'] is List) { + for (var u in data['users']) { + users.add(UserPayload.fromJson(u)); + } + } + if (data['peers'] is List) { + for (final peer in data['peers']) { + peers.add(Peer.fromJson(peer)); + } + } + } catch (e) { + debugPrint("load group cache: $e"); + } + } + + reset() async { + groupLoadError.value = ''; + users.clear(); + peers.clear(); + selectedUser.value = ''; + await bind.mainClearGroup(); } } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 80809309a..4b458f962 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -98,7 +98,8 @@ class PlatformFFI { int getRgbaSize(SessionID sessionId) => _ffiBind.sessionGetRgbaSize(sessionId: sessionId); - void nextRgba(SessionID sessionId) => _ffiBind.sessionNextRgba(sessionId: sessionId); + void nextRgba(SessionID sessionId) => + _ffiBind.sessionNextRgba(sessionId: sessionId); void registerTexture(SessionID sessionId, int ptr) => _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr); @@ -198,7 +199,7 @@ class PlatformFFI { version = await getVersion(); } - Future _tryHandle(Map evt) async { + Future tryHandle(Map evt) async { final name = evt['name']; if (name != null) { final handlers = _eventHandlers[name]; @@ -216,14 +217,15 @@ class PlatformFFI { /// Start listening to the Rust core's events and frames. void _startListenEvent(RustdeskImpl rustdeskImpl) { - final appType = _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType; + final appType = + _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType; var sink = rustdeskImpl.startGlobalEventStream(appType: appType); sink.listen((message) { () async { try { Map event = json.decode(message); // _tryHandle here may be more flexible than _eventCallback - if (!await _tryHandle(event)) { + if (!await tryHandle(event)) { if (_eventCallback != null) { await _eventCallback!(event); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 4d7ac3b28..1ce8648ab 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; import 'platform_model.dart'; // ignore: depend_on_referenced_packages import 'package:collection/collection.dart'; @@ -7,7 +8,7 @@ import 'package:collection/collection.dart'; class Peer { final String id; String hash; - String username; + String username; // pc username String hostname; String platform; String alias; @@ -16,6 +17,7 @@ class Peer { String rdpPort; String rdpUsername; bool online = false; + String loginName; //login username String getId() { if (alias != '') { @@ -34,7 +36,8 @@ class Peer { tags = json['tags'] ?? [], forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', rdpPort = json['rdpPort'] ?? '', - rdpUsername = json['rdpUsername'] ?? ''; + rdpUsername = json['rdpUsername'] ?? '', + loginName = json['loginName'] ?? ''; Map toJson() { return { @@ -48,6 +51,7 @@ class Peer { "forceAlwaysRelay": forceAlwaysRelay.toString(), "rdpPort": rdpPort, "rdpUsername": rdpUsername, + 'loginName': loginName, }; } @@ -63,6 +67,16 @@ class Peer { }; } + Map toGroupCacheJson() { + return { + "id": id, + "username": username, + "hostname": hostname, + "platform": platform, + "login_name": loginName, + }; + } + Peer({ required this.id, required this.hash, @@ -74,6 +88,7 @@ class Peer { required this.forceAlwaysRelay, required this.rdpPort, required this.rdpUsername, + required this.loginName, }); Peer.loading() @@ -88,6 +103,7 @@ class Peer { forceAlwaysRelay: false, rdpPort: '', rdpUsername: '', + loginName: '', ); bool equal(Peer other) { return id == other.id && @@ -99,21 +115,24 @@ class Peer { tags.equals(other.tags) && forceAlwaysRelay == other.forceAlwaysRelay && rdpPort == other.rdpPort && - rdpUsername == other.rdpUsername; + rdpUsername == other.rdpUsername && + loginName == other.loginName; } Peer.copy(Peer other) : this( - id: other.id, - hash: other.hash, - username: other.username, - hostname: other.hostname, - platform: other.platform, - alias: other.alias, - tags: other.tags.toList(), - forceAlwaysRelay: other.forceAlwaysRelay, - rdpPort: other.rdpPort, - rdpUsername: other.rdpUsername); + id: other.id, + hash: other.hash, + username: other.username, + hostname: other.hostname, + platform: other.platform, + alias: other.alias, + tags: other.tags.toList(), + forceAlwaysRelay: other.forceAlwaysRelay, + rdpPort: other.rdpPort, + rdpUsername: other.rdpUsername, + loginName: other.loginName, + ); } enum UpdateEvent { online, load } @@ -121,11 +140,14 @@ enum UpdateEvent { online, load } class Peers extends ChangeNotifier { final String name; final String loadEvent; - List peers; + List peers = List.empty(growable: true); + final RxList? initPeers; UpdateEvent event = UpdateEvent.load; static const _cbQueryOnlines = 'callback_query_onlines'; - Peers({required this.name, required this.peers, required this.loadEvent}) { + Peers( + {required this.name, required this.initPeers, required this.loadEvent}) { + peers = initPeers ?? []; platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async { _updateOnlineState(evt); }); @@ -176,7 +198,11 @@ class Peers extends ChangeNotifier { void _updatePeers(Map evt) { final onlineStates = _getOnlineStates(); - peers = _decodePeers(evt['peers']); + if (initPeers != null) { + peers = initPeers!; + } else { + peers = _decodePeers(evt['peers']); + } for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 2fdf9b449..e4971d9ac 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -17,8 +17,6 @@ enum PeerTabIndex { group, } -const String defaultGroupTabname = 'Group'; - class PeerTabModel with ChangeNotifier { WeakReference parent; int get currentTab => _currentTab; @@ -28,7 +26,7 @@ class PeerTabModel with ChangeNotifier { 'Favorites', 'Discovered', 'Address Book', - //defaultGroupTabname, + 'Group', ]; final List icons = [ Icons.access_time_filled, @@ -37,7 +35,7 @@ class PeerTabModel with ChangeNotifier { IconFont.addressBook, Icons.group, ]; - final List _isVisible = List.filled(4, true, growable: false); + final List _isVisible = List.filled(5, true, growable: false); List get isVisible => _isVisible; List get indexs => List.generate(tabNames.length, (index) => index); List get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); @@ -85,17 +83,9 @@ class PeerTabModel with ChangeNotifier { } } - String tabTooltip(int index, String groupName) { + String tabTooltip(int index) { if (index >= 0 && index < tabNames.length) { - if (index == PeerTabIndex.group.index) { - if (gFFI.userModel.isAdmin.value || groupName.isEmpty) { - return translate(defaultGroupTabname); - } else { - return '${translate('Group')}: $groupName'; - } - } else { - return translate(tabNames[index]); - } + return translate(tabNames[index]); } assert(false); return index.toString(); diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 559e8be36..e6cd26fac 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -45,7 +45,7 @@ class UserModel { refreshingUser = false; final status = response.statusCode; if (status == 401 || status == 400) { - reset(clearAbCache: status == 401); + reset(resetOther: status == 401); return; } final data = json.decode(utf8.decode(response.bodyBytes)); @@ -84,11 +84,13 @@ class UserModel { } } - Future reset({bool clearAbCache = false}) async { + Future reset({bool resetOther = false}) async { await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: ''); - if (clearAbCache) await bind.mainClearAb(); - await gFFI.groupModel.reset(); + if (resetOther) { + await gFFI.abModel.reset(); + await gFFI.groupModel.reset(); + } userName.value = ''; } @@ -120,7 +122,7 @@ class UserModel { } catch (e) { debugPrint("request /api/logout failed: err=$e"); } finally { - await reset(clearAbCache: true); + await reset(resetOther: true); gFFI.dialogManager.dismissByTag(tag); } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 82174754c..5a7c7d4b4 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1650,6 +1650,106 @@ macro_rules! deserialize_default { }; } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GroupPeer { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub id: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub username: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub hostname: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub platform: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub login_name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GroupUser { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Group { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub access_token: String, + #[serde(default, deserialize_with = "deserialize_vec_groupuser")] + pub users: Vec, + #[serde(default, deserialize_with = "deserialize_vec_grouppeer")] + pub peers: Vec, +} + +impl Group { + fn path() -> PathBuf { + let filename = format!("{}_group", APP_NAME.read().unwrap().clone()); + Config::path(filename) + } + + pub fn store(json: String) { + if let Ok(mut file) = std::fs::File::create(Self::path()) { + let data = compress(json.as_bytes()); + let max_len = 64 * 1024 * 1024; + if data.len() > max_len { + // maxlen of function decompress + return; + } + if let Ok(data) = symmetric_crypt(&data, true) { + file.write_all(&data).ok(); + } + }; + } + + pub fn load() -> Self { + if let Ok(mut file) = std::fs::File::open(Self::path()) { + let mut data = vec![]; + if file.read_to_end(&mut data).is_ok() { + if let Ok(data) = symmetric_crypt(&data, false) { + let data = decompress(&data); + if let Ok(group) = serde_json::from_str::(&String::from_utf8_lossy(&data)) + { + return group; + } + } + } + }; + Self::remove(); + Self::default() + } + + pub fn remove() { + std::fs::remove_file(Self::path()).ok(); + } +} + deserialize_default!(deserialize_string, String); deserialize_default!(deserialize_bool, bool); deserialize_default!(deserialize_i32, i32); @@ -1658,6 +1758,8 @@ deserialize_default!(deserialize_vec_string, Vec); deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); deserialize_default!(deserialize_vec_discoverypeer, Vec); deserialize_default!(deserialize_vec_abpeer, Vec); +deserialize_default!(deserialize_vec_groupuser, Vec); +deserialize_default!(deserialize_vec_grouppeer, Vec); deserialize_default!(deserialize_keypair, KeyPair); deserialize_default!(deserialize_size, Size); deserialize_default!(deserialize_hashmap_string_string, HashMap); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c9da2cde2..41262a891 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1187,6 +1187,24 @@ pub fn main_load_ab() -> String { serde_json::to_string(&config::Ab::load()).unwrap_or_default() } +pub fn main_save_group(json: String) { + if json.len() > 1024 { + std::thread::spawn(|| { + config::Group::store(json); + }); + } else { + config::Group::store(json); + } +} + +pub fn main_clear_group() { + config::Group::remove(); +} + +pub fn main_load_group() -> String { + serde_json::to_string(&config::Group::load()).unwrap_or_default() +} + pub fn session_send_pointer(session_id: SessionID, msg: String) { super::flutter::session_send_pointer(session_id, msg); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 9b00da2a6..58056fb8d 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 868440bfa..d17601d92 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index b22b6494d..360c259bf 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "自动关闭不活跃的会话"), ("Connection failed due to inactivity", "由于长时间无操作, 连接被自动断开"), ("Check for software update on startup", "启动时检查软件更新"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "请升级专业版服务器到{}或更高版本!"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 857672eff..823d4f2e2 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index d7151ee2f..7794803da 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 5b4ffd9e9..c05369cbf 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Automatisches Schließen eingehender Sitzungen bei Inaktivität des Benutzers"), ("Connection failed due to inactivity", "Automatische Trennung der Verbindung aufgrund von Inaktivität"), ("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index b098452f6..814432d83 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 825698385..27e636a9d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -90,5 +90,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Decline", "Decline"), ("auto_disconnect_option_tip", "Automatically close incoming sessions on user inactivity"), ("Connection failed due to inactivity", "Automatically disconnected due to inactivity"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Please upgrade RustDesk Server Pro to version {} or newer!") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ea1d597d4..f8deef4d9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 8299e609e..9bc8ef13a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 98d0c4087..aacd05d27 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 51d690bf3..c37d4defb 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5ee56c422..13bc5854b 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f3ba76299..6d0dcd406 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Secara otomatis akan menutup sesi ketika pengguna tidak beraktivitas"), ("Connection failed due to inactivity", "Secara otomatis akan terputus ketik tidak ada aktivitas."), ("Check for software update on startup", "Periksa pembaruan aplikasi saat sistem dinyalakan."), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6b6f31510..11c5a10e0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", "Connessione non riuscita a causa di inattività"), ("Check for software update on startup", "All'avvio programma verifica presenza di aggiornamenti"), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 017637e48..fd7f436b6 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f1f6c7311..1e457f51c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 53fa177df..51c42040f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 465508005..973c69049 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 64987f268..beff69393 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Automātiski aizvērt ienākošās sesijas lietotāja neaktivitātes gadījumā"), ("Connection failed due to inactivity", "Automātiski atvienots neaktivitātes dēļ"), ("Check for software update on startup", "Startējot pārbaudīt, vai nav programmatūras atjauninājumu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 4ae999878..4cce433bb 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e9b03faea..ab49e895b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Automatycznie rozłącz sesje przychodzące przy braku aktywności użytkownika"), ("Connection failed due to inactivity", "Automatycznie rozłącz przy bezczynności"), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6a3f43800..c753b71cb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 7a58fc4bc..6ecae0f7d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 9557a4031..aa3a9dec1 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0bcc11897..d1fd25d77 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Автоматически закрывать входящие сеансы при неактивности пользователя"), ("Connection failed due to inactivity", "Подключение не выполнено из-за неактивности"), ("Check for software update on startup", "Проверять обновления программы при запуске"), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c7d68b4fe..5db5d9abf 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index cf2e29428..142a39967 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 30cb24abe..00cb0a0a7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f193a720b..921a491b4 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a0fb40efe..e8bdeb429 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 03b69bcf6..dd6a05a8d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index dc430be2b..6ce6a2cd5 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2bce33d47..6929fc431 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b13c024a8..dfe152789 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 4301b3b1d..8497555f6 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 3d0f5c455..686f55269 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ].iter().cloned().collect(); }