diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a71bd12b..af0be251 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 8e5c2d02..dabb3be8 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 070c4412..bb5dc560 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 2867d3bc..a4d89155 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 f5af9422..f6a6ef40 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 acd98eea..dccf83c7 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 0e4898fc..28bfb669 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 43273c54..d4e81a82 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 cbb7f747..6968b2f1 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 f2592fa2..6177c584 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 80809309..4b458f96 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 4d7ac3b2..1ce8648a 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 2fdf9b44..e4971d9a 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 559e8be3..e6cd26fa 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 82174754..5a7c7d4b 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 c9da2cde..41262a89 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 9b00da2a..58056fb8 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 868440bf..d17601d9 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 b22b6494..360c259b 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 857672ef..823d4f2e 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 d7151ee2..7794803d 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 5b4ffd9e..c05369cb 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 b098452f..814432d8 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 82569838..27e636a9 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 ea1d597d..f8deef4d 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 8299e609..9bc8ef13 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 98d0c408..aacd05d2 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 51d690bf..c37d4def 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 5ee56c42..13bc5854 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 f3ba7629..6d0dcd40 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 6b6f3151..11c5a10e 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 017637e4..fd7f436b 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 f1f6c731..1e457f51 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 53fa177d..51c42040 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 46550800..973c6904 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 64987f26..beff6939 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 4ae99987..4cce433b 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 e9b03fae..ab49e895 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 6a3f4380..c753b71c 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 7a58fc4b..6ecae0f7 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 9557a403..aa3a9dec 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 0bcc1189..d1fd25d7 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 c7d68b4f..5db5d9ab 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 cf2e2942..142a3996 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 30cb24ab..00cb0a0a 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 f193a720..921a491b 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 a0fb40ef..e8bdeb42 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 03b69bcf..dd6a05a8 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 dc430be2..6ce6a2cd 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 2bce33d4..6929fc43 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 b13c024a..dfe15278 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 4301b3b1..8497555f 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 3d0f5c45..686f5526 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(); }