enable group, show accessible users and peers

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2023-09-14 10:17:03 +08:00
parent 09d380ba8f
commit b2a4f11e0b
53 changed files with 568 additions and 273 deletions

View File

@ -2480,3 +2480,59 @@ String toCapitalized(String s) {
} }
return s.substring(0, 1).toUpperCase() + s.substring(1); 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),
));
}

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
@ -48,11 +49,18 @@ class UserPayload {
}; };
return map; return map;
} }
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
} }
class PeerPayload { class PeerPayload {
String id = ''; String id = '';
String info = ''; Map<String, dynamic> info = {};
int? status; int? status;
String user = ''; String user = '';
String user_name = ''; String user_name = '';
@ -67,7 +75,38 @@ class PeerPayload {
note = json['note'] ?? ''; note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) { 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<String> 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;
}
} }
} }

View File

@ -35,7 +35,7 @@ class _AddressBookState extends State<AddressBook> {
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (gFFI.userModel.userName.value.isEmpty) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
@ -49,11 +49,13 @@ class _AddressBookState extends State<AddressBook> {
children: [ children: [
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(), if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pullError, err: gFFI.abModel.pullError,
retry: null, retry: null,
close: () => gFFI.abModel.pullError.value = ''), close: () => gFFI.abModel.pullError.value = ''),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pushError, err: gFFI.abModel.pushError,
retry: () => gFFI.abModel.pushAb(isRetry: true), retry: () => gFFI.abModel.pushAb(isRetry: true),
close: () => gFFI.abModel.pushError.value = ''), close: () => gFFI.abModel.pushError.value = ''),
@ -66,61 +68,6 @@ class _AddressBookState extends State<AddressBook> {
} }
}); });
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() { Widget _buildAddressBookDesktop() {
return Row( return Row(
children: [ children: [
@ -230,11 +177,10 @@ class _AddressBookState extends State<AddressBook> {
return Expanded( return Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.abModel.peers,
initPeers: gFFI.abModel.peers.value, )),
))),
); );
} }

View File

@ -29,49 +29,28 @@ class _MyGroupState extends State<MyGroup> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
// use username to be same with ab if (!gFFI.userModel.isLogin) {
if (gFFI.userModel.userName.value.isEmpty) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return buildBody(context);
});
}
Widget buildBody(BuildContext context) {
return Obx(() {
if (gFFI.groupModel.groupLoading.value) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
if (gFFI.groupModel.groupLoadError.isNotEmpty) { return Column(
return _buildShowError(gFFI.groupModel.groupLoadError.value); children: [
} buildErrorBanner(context,
if (isDesktop) { loading: gFFI.groupModel.groupLoading,
return _buildDesktop(); err: gFFI.groupModel.groupLoadError,
} else { retry: null,
return _buildMobile(); 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() { Widget _buildDesktop() {
return Row( return Row(
children: [ children: [
@ -100,10 +79,9 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
@ -133,10 +111,9 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
@ -195,6 +172,7 @@ class _MyGroupState extends State<MyGroup> {
}, child: Obx( }, child: Obx(
() { () {
bool selected = selectedUser.value == username; bool selected = selectedUser.value == username;
final isMe = username == gFFI.userModel.userName.value;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null, color: selected ? MyTheme.color(context).highlight : null,
@ -208,7 +186,7 @@ class _MyGroupState extends State<MyGroup> {
children: [ children: [
Icon(Icons.person_rounded, color: Colors.grey, size: 16) Icon(Icons.person_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4), .marginOnly(right: 4),
Expanded(child: Text(username)), Expanded(child: Text(isMe ? translate('Me') : username)),
], ],
).paddingSymmetric(vertical: 4), ).paddingSymmetric(vertical: 4),
), ),

View File

@ -1093,7 +1093,7 @@ class MyGroupPeerCard extends BasePeerCard {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
@ -1101,9 +1101,14 @@ class MyGroupPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id)); // menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) { // if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id)); // menuItems.add(_unrememberPasswordAction(peer.id));
// }
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
} }
return menuItems; return menuItems;
} }

View File

@ -111,7 +111,11 @@ class _PeerTabPageState extends State<PeerTabPage>
child: child:
visibleContextMenuListener(_createSwitchBar(context))), visibleContextMenuListener(_createSwitchBar(context))),
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), 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(), _createMultiSelection(),
Offstage( Offstage(
offstage: !isDesktop, offstage: !isDesktop,
@ -170,12 +174,12 @@ class _PeerTabPageState extends State<PeerTabPage>
)); ));
return Obx(() => InkWell( return Obx(() => InkWell(
child: Container( child: Container(
decoration: decoration: (hover.value
selected ? decoBorder : (hover.value ? deco : null), ? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Tooltip( child: Tooltip(
preferBelow: false, preferBelow: false,
message: message: model.tabTooltip(t),
model.tabTooltip(t, gFFI.groupModel.groupName.value),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color), child: Icon(model.tabIcon(t), color: color),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
@ -212,17 +216,19 @@ class _PeerTabPageState extends State<PeerTabPage>
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
} }
Widget _createRefresh() { Widget _createRefresh(
{required PeerTabIndex index, required RxBool loading}) {
final model = Provider.of<PeerTabModel>(context);
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage( return Offstage(
offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index, offstage: model.currentTab != index.index,
child: RefreshWidget( child: RefreshWidget(
onPressed: () { onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) { if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load(); entries[gFFI.peerTabModel.currentTab].load();
} }
}, },
spinning: gFFI.abModel.abLoading, spinning: loading,
child: RotatedBox( child: RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: Tooltip( child: Tooltip(
@ -297,9 +303,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Navigator.pop(context); Navigator.pop(context);
} }
}), }),
Expanded( Expanded(child: Text(model.tabTooltip(i))),
child:
Text(model.tabTooltip(i, gFFI.groupModel.groupName.value))),
], ],
), ),
)); ));
@ -348,7 +352,7 @@ class _PeerTabPageState extends State<PeerTabPage>
for (int i = 0; i < model.tabNames.length; i++) { for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch( menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: model.tabTooltip(i, gFFI.groupModel.groupName.value), text: model.tabTooltip(i),
getter: () async { getter: () async {
return model.isVisible[i]; return model.isVisible[i];
}, },
@ -388,6 +392,9 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget deleteSelection() { Widget deleteSelection() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
if (model.currentTab == PeerTabIndex.group.index) {
return Offstage();
}
return _hoverAction( return _hoverAction(
context: context, context: context,
onTap: () { onTap: () {

View File

@ -35,6 +35,7 @@ class LoadEvent {
static const String favorite = 'load_fav_peers'; static const String favorite = 'load_fav_peers';
static const String lan = 'load_lan_peers'; static const String lan = 'load_lan_peers';
static const String addressBook = 'load_address_book_peers'; static const String addressBook = 'load_address_book_peers';
static const String group = 'load_group_peers';
} }
/// for peer search text, global obs value /// for peer search text, global obs value
@ -312,7 +313,7 @@ abstract class BasePeersView extends StatelessWidget {
final String loadEvent; final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final List<Peer> initPeers; final RxList<Peer>? initPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
@ -326,7 +327,7 @@ abstract class BasePeersView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _PeersView( return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
peerFilter: peerFilter, peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder); peerCardBuilder: peerCardBuilder);
} }
@ -343,7 +344,7 @@ class RecentPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@ -365,7 +366,7 @@ class FavoritePeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@ -387,7 +388,7 @@ class DiscoveredPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@ -403,7 +404,7 @@ class AddressBookPeersView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', name: 'address book peer',
@ -435,11 +436,11 @@ class MyGroupPeerView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'my group peer', name: 'group peer',
loadEvent: 'load_my_group_peers', loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
@ -450,12 +451,12 @@ class MyGroupPeerView extends BasePeersView {
static bool filter(Peer peer) { static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) { if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.username.contains(gFFI.groupModel.searchUserText)) { if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
return false; return false;
} }
} }
if (gFFI.groupModel.selectedUser.isNotEmpty) { if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.username) { if (gFFI.groupModel.selectedUser.value != peer.loginName) {
return false; return false;
} }
} }

View File

@ -126,6 +126,7 @@ void runMainApp(bool startService) async {
bind.pluginListReload(); bind.pluginListReload();
} }
gFFI.abModel.loadCache(); gFFI.abModel.loadCache();
gFFI.groupModel.loadCache();
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
// Set window option. // Set window option.
@ -154,6 +155,7 @@ void runMobileApp() async {
if (isAndroid) androidChannelInit(); if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath(); platformFFI.syncAndroidServiceAppDirConfigPath();
gFFI.abModel.loadCache(); gFFI.abModel.loadCache();
gFFI.groupModel.loadCache();
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
} }

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; 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/model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
@ -115,9 +116,10 @@ class AbModel {
_timerCounter = 0; _timerCounter = 0;
if (pullError.isNotEmpty) { if (pullError.isNotEmpty) {
if (statusCode == 401) { 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; ret = true;
_saveCache(); _saveCache();
} else { } else {
Map<String, dynamic> json = _jsonDecodeResp(resp.body, resp.statusCode); Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) { if (json.containsKey('error')) {
throw json['error']; throw json['error'];
} else if (resp.statusCode == 200) { } else if (resp.statusCode == 200) {
@ -479,11 +482,12 @@ class AbModel {
loadCache() async { loadCache() async {
try { try {
if (_cacheLoadOnceFlag || abLoading.value) return; if (_cacheLoadOnceFlag || abLoading.value || initialized) return;
_cacheLoadOnceFlag = true; _cacheLoadOnceFlag = true;
final access_token = bind.mainGetLocalOption(key: 'access_token'); final access_token = bind.mainGetLocalOption(key: 'access_token');
if (access_token.isEmpty) return; if (access_token.isEmpty) return;
final cache = await bind.mainLoadAb(); final cache = await bind.mainLoadAb();
if (abLoading.value) return;
final data = jsonDecode(cache); final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return; if (data == null || data['access_token'] != access_token) return;
_deserialize(data); _deserialize(data);
@ -561,4 +565,12 @@ class AbModel {
} }
}); });
} }
reset() async {
pullError.value = '';
pushError.value = '';
tags.clear();
peers.clear();
await bind.mainClearAb();
}
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.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/model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
@ -11,57 +12,74 @@ import 'package:http/http.dart' as http;
class GroupModel { class GroupModel {
final RxBool groupLoading = false.obs; final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs; final RxString groupLoadError = "".obs;
final RxString groupId = ''.obs;
RxString groupName = ''.obs;
final RxList<UserPayload> users = RxList.empty(growable: true); final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true); final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs; final RxString selectedUser = ''.obs;
final RxString searchUserText = ''.obs; final RxString searchUserText = ''.obs;
WeakReference<FFI> parent; WeakReference<FFI> parent;
var initialized = false; var initialized = false;
var _cacheLoadOnceFlag = false;
var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty;
GroupModel(this.parent); GroupModel(this.parent);
reset() {
groupName.value = '';
groupId.value = '';
users.clear();
peersShow.clear();
initialized = false;
}
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
/* if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (!force && initialized) return; if (!force && initialized) return;
if (!quiet) { if (!quiet) {
groupLoading.value = true; groupLoading.value = true;
groupLoadError.value = ""; groupLoadError.value = "";
} }
await _pull(); try {
await _pull();
} catch (_) {}
groupLoading.value = false; groupLoading.value = false;
initialized = true; initialized = true;
*/ platformFFI.tryHandle({'name': LoadEvent.group});
if (_statusCode == 401) {
gFFI.userModel.reset(resetOther: true);
} else {
_saveCache();
}
} }
Future<void> _pull() async { Future<void> _pull() async {
reset(); List<UserPayload> tmpUsers = List.empty(growable: true);
if (bind.mainGetLocalOption(key: 'access_token') == '') { if (!await _getUsers(tmpUsers)) {
return; return;
} }
try { List<Peer> tmpPeers = List.empty(growable: true);
if (!await _getGroup()) { if (!await _getPeers(tmpPeers)) {
reset();
return;
}
} catch (e) {
debugPrint('$e');
reset();
return; 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<bool> _getUsers(List<UserPayload> tmpUsers) async {
final api = "${await bind.mainGetApiServer()}/api/users"; final api = "${await bind.mainGetApiServer()}/api/users";
try { try {
var uri0 = Uri.parse(api); var uri0 = Uri.parse(api);
final pageSize = 20; final pageSize = 100;
var total = 0; var total = 0;
int current = 0; int current = 0;
do { do {
@ -74,84 +92,63 @@ class GroupModel {
queryParameters: { queryParameters: {
'current': current.toString(), 'current': current.toString(),
'pageSize': pageSize.toString(), 'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse) 'grp': groupId.value, 'accessible': '',
'status': '1',
}); });
final resp = await http.get(uri, headers: getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { _statusCode = resp.statusCode;
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes)); Map<String, dynamic> json =
if (json.containsKey('error')) { _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
throw json['error']; if (json.containsKey('error')) {
if (json['error'] == 'Admin required!') {
throw translate('upgrade_rustdesk_server_pro_to_{1.1.10}_tip');
} else { } else {
if (json.containsKey('total')) { throw json['error'];
if (total == 0) total = json['total']; }
if (json.containsKey('data')) { }
final data = json['data']; if (resp.statusCode != 200) {
if (data is List) { throw 'HTTP ${resp.statusCode}';
for (final user in data) { }
final u = UserPayload.fromJson(user); if (json.containsKey('total')) {
if (!users.any((e) => e.name == u.name)) { if (total == 0) total = json['total'];
users.add(u); 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); } while (current * pageSize < total);
return true;
} catch (err) { } catch (err) {
debugPrint('$err'); debugPrint('get accessible users: $err');
groupLoadError.value = err.toString(); groupLoadError.value = err.toString();
} finally {
_pullUserPeers();
} }
}
Future<bool> _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; return false;
} }
Future<void> _pullUserPeers() async { Future<bool> _getPeers(List<Peer> tmpPeers) async {
peersShow.clear();
final api = "${await bind.mainGetApiServer()}/api/peers";
try { try {
final api = "${await bind.mainGetApiServer()}/api/peers";
var uri0 = Uri.parse(api); var uri0 = Uri.parse(api);
final pageSize = final pageSize = 100;
20; // ????????????????????????????????????????????????????? stupid stupis, how about >20 peers
var total = 0; var total = 0;
int current = 0; int current = 0;
var queryParameters = { var queryParameters = {
'current': current.toString(), 'current': current.toString(),
'pageSize': pageSize.toString(), 'pageSize': pageSize.toString(),
'accessible': '',
'status': '1',
'user_status': '1',
}; };
if (!gFFI.userModel.isAdmin.value) {
queryParameters.addAll({'grp': groupId.value});
}
do { do {
current += 1; current += 1;
var uri = Uri( var uri = Uri(
@ -161,32 +158,107 @@ class GroupModel {
port: uri0.port, port: uri0.port,
queryParameters: queryParameters); queryParameters: queryParameters);
final resp = await http.get(uri, headers: getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { _statusCode = resp.statusCode;
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes));
if (json.containsKey('error')) { Map<String, dynamic> json =
throw json['error']; _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
} else { if (json.containsKey('error')) {
if (json.containsKey('total')) { throw json['error'];
if (total == 0) total = json['total']; }
if (json.containsKey('data')) { if (resp.statusCode != 200) {
final data = json['data']; throw 'HTTP ${resp.statusCode}';
if (data is List) { }
for (final p in data) { if (json.containsKey('total')) {
final peerPayload = PeerPayload.fromJson(p); if (total == 0) total = json['total'];
final peer = PeerPayload.toPeer(peerPayload); if (total > 1000) {
if (!peersShow.any((e) => e.id == peer.id)) { total = 1000;
peersShow.add(peer); }
} 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); } while (current * pageSize < total);
return true;
} catch (err) { } catch (err) {
debugPrint('$err'); debugPrint('get accessible peers: $err');
groupLoadError.value = err.toString(); groupLoadError.value = err.toString();
} finally {} }
return false;
}
Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try {
Map<String, dynamic> 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 = (<String, dynamic>{
"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();
} }
} }

View File

@ -98,7 +98,8 @@ class PlatformFFI {
int getRgbaSize(SessionID sessionId) => int getRgbaSize(SessionID sessionId) =>
_ffiBind.sessionGetRgbaSize(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) => void registerTexture(SessionID sessionId, int ptr) =>
_ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr); _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
@ -198,7 +199,7 @@ class PlatformFFI {
version = await getVersion(); version = await getVersion();
} }
Future<bool> _tryHandle(Map<String, dynamic> evt) async { Future<bool> tryHandle(Map<String, dynamic> evt) async {
final name = evt['name']; final name = evt['name'];
if (name != null) { if (name != null) {
final handlers = _eventHandlers[name]; final handlers = _eventHandlers[name];
@ -216,14 +217,15 @@ class PlatformFFI {
/// Start listening to the Rust core's events and frames. /// Start listening to the Rust core's events and frames.
void _startListenEvent(RustdeskImpl rustdeskImpl) { void _startListenEvent(RustdeskImpl rustdeskImpl) {
final appType = _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType; final appType =
_appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType;
var sink = rustdeskImpl.startGlobalEventStream(appType: appType); var sink = rustdeskImpl.startGlobalEventStream(appType: appType);
sink.listen((message) { sink.listen((message) {
() async { () async {
try { try {
Map<String, dynamic> event = json.decode(message); Map<String, dynamic> event = json.decode(message);
// _tryHandle here may be more flexible than _eventCallback // _tryHandle here may be more flexible than _eventCallback
if (!await _tryHandle(event)) { if (!await tryHandle(event)) {
if (_eventCallback != null) { if (_eventCallback != null) {
await _eventCallback!(event); await _eventCallback!(event);
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'platform_model.dart'; import 'platform_model.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -7,7 +8,7 @@ import 'package:collection/collection.dart';
class Peer { class Peer {
final String id; final String id;
String hash; String hash;
String username; String username; // pc username
String hostname; String hostname;
String platform; String platform;
String alias; String alias;
@ -16,6 +17,7 @@ class Peer {
String rdpPort; String rdpPort;
String rdpUsername; String rdpUsername;
bool online = false; bool online = false;
String loginName; //login username
String getId() { String getId() {
if (alias != '') { if (alias != '') {
@ -34,7 +36,8 @@ class Peer {
tags = json['tags'] ?? [], tags = json['tags'] ?? [],
forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', forceAlwaysRelay = json['forceAlwaysRelay'] == 'true',
rdpPort = json['rdpPort'] ?? '', rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? ''; rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
@ -48,6 +51,7 @@ class Peer {
"forceAlwaysRelay": forceAlwaysRelay.toString(), "forceAlwaysRelay": forceAlwaysRelay.toString(),
"rdpPort": rdpPort, "rdpPort": rdpPort,
"rdpUsername": rdpUsername, "rdpUsername": rdpUsername,
'loginName': loginName,
}; };
} }
@ -63,6 +67,16 @@ class Peer {
}; };
} }
Map<String, dynamic> toGroupCacheJson() {
return <String, dynamic>{
"id": id,
"username": username,
"hostname": hostname,
"platform": platform,
"login_name": loginName,
};
}
Peer({ Peer({
required this.id, required this.id,
required this.hash, required this.hash,
@ -74,6 +88,7 @@ class Peer {
required this.forceAlwaysRelay, required this.forceAlwaysRelay,
required this.rdpPort, required this.rdpPort,
required this.rdpUsername, required this.rdpUsername,
required this.loginName,
}); });
Peer.loading() Peer.loading()
@ -88,6 +103,7 @@ class Peer {
forceAlwaysRelay: false, forceAlwaysRelay: false,
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
loginName: '',
); );
bool equal(Peer other) { bool equal(Peer other) {
return id == other.id && return id == other.id &&
@ -99,21 +115,24 @@ class Peer {
tags.equals(other.tags) && tags.equals(other.tags) &&
forceAlwaysRelay == other.forceAlwaysRelay && forceAlwaysRelay == other.forceAlwaysRelay &&
rdpPort == other.rdpPort && rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername; rdpUsername == other.rdpUsername &&
loginName == other.loginName;
} }
Peer.copy(Peer other) Peer.copy(Peer other)
: this( : this(
id: other.id, id: other.id,
hash: other.hash, hash: other.hash,
username: other.username, username: other.username,
hostname: other.hostname, hostname: other.hostname,
platform: other.platform, platform: other.platform,
alias: other.alias, alias: other.alias,
tags: other.tags.toList(), tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay, forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort, rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername); rdpUsername: other.rdpUsername,
loginName: other.loginName,
);
} }
enum UpdateEvent { online, load } enum UpdateEvent { online, load }
@ -121,11 +140,14 @@ enum UpdateEvent { online, load }
class Peers extends ChangeNotifier { class Peers extends ChangeNotifier {
final String name; final String name;
final String loadEvent; final String loadEvent;
List<Peer> peers; List<Peer> peers = List.empty(growable: true);
final RxList<Peer>? initPeers;
UpdateEvent event = UpdateEvent.load; UpdateEvent event = UpdateEvent.load;
static const _cbQueryOnlines = 'callback_query_onlines'; 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 { platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async {
_updateOnlineState(evt); _updateOnlineState(evt);
}); });
@ -176,7 +198,11 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates(); final onlineStates = _getOnlineStates();
peers = _decodePeers(evt['peers']); if (initPeers != null) {
peers = initPeers!;
} else {
peers = _decodePeers(evt['peers']);
}
for (var peer in peers) { for (var peer in peers) {
final state = onlineStates[peer.id]; final state = onlineStates[peer.id];
peer.online = state != null && state != false; peer.online = state != null && state != false;

View File

@ -17,8 +17,6 @@ enum PeerTabIndex {
group, group,
} }
const String defaultGroupTabname = 'Group';
class PeerTabModel with ChangeNotifier { class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent; WeakReference<FFI> parent;
int get currentTab => _currentTab; int get currentTab => _currentTab;
@ -28,7 +26,7 @@ class PeerTabModel with ChangeNotifier {
'Favorites', 'Favorites',
'Discovered', 'Discovered',
'Address Book', 'Address Book',
//defaultGroupTabname, 'Group',
]; ];
final List<IconData> icons = [ final List<IconData> icons = [
Icons.access_time_filled, Icons.access_time_filled,
@ -37,7 +35,7 @@ class PeerTabModel with ChangeNotifier {
IconFont.addressBook, IconFont.addressBook,
Icons.group, Icons.group,
]; ];
final List<bool> _isVisible = List.filled(4, true, growable: false); final List<bool> _isVisible = List.filled(5, true, growable: false);
List<bool> get isVisible => _isVisible; List<bool> get isVisible => _isVisible;
List<int> get indexs => List.generate(tabNames.length, (index) => index); List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); List<int> 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 >= 0 && index < tabNames.length) {
if (index == PeerTabIndex.group.index) { return translate(tabNames[index]);
if (gFFI.userModel.isAdmin.value || groupName.isEmpty) {
return translate(defaultGroupTabname);
} else {
return '${translate('Group')}: $groupName';
}
} else {
return translate(tabNames[index]);
}
} }
assert(false); assert(false);
return index.toString(); return index.toString();

View File

@ -45,7 +45,7 @@ class UserModel {
refreshingUser = false; refreshingUser = false;
final status = response.statusCode; final status = response.statusCode;
if (status == 401 || status == 400) { if (status == 401 || status == 400) {
reset(clearAbCache: status == 401); reset(resetOther: status == 401);
return; return;
} }
final data = json.decode(utf8.decode(response.bodyBytes)); final data = json.decode(utf8.decode(response.bodyBytes));
@ -84,11 +84,13 @@ class UserModel {
} }
} }
Future<void> reset({bool clearAbCache = false}) async { Future<void> reset({bool resetOther = false}) async {
await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: '');
if (clearAbCache) await bind.mainClearAb(); if (resetOther) {
await gFFI.groupModel.reset(); await gFFI.abModel.reset();
await gFFI.groupModel.reset();
}
userName.value = ''; userName.value = '';
} }
@ -120,7 +122,7 @@ class UserModel {
} catch (e) { } catch (e) {
debugPrint("request /api/logout failed: err=$e"); debugPrint("request /api/logout failed: err=$e");
} finally { } finally {
await reset(clearAbCache: true); await reset(resetOther: true);
gFFI.dialogManager.dismissByTag(tag); gFFI.dialogManager.dismissByTag(tag);
} }
} }

View File

@ -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<GroupUser>,
#[serde(default, deserialize_with = "deserialize_vec_grouppeer")]
pub peers: Vec<GroupPeer>,
}
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::<Self>(&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_string, String);
deserialize_default!(deserialize_bool, bool); deserialize_default!(deserialize_bool, bool);
deserialize_default!(deserialize_i32, i32); deserialize_default!(deserialize_i32, i32);
@ -1658,6 +1758,8 @@ deserialize_default!(deserialize_vec_string, Vec<String>);
deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>);
deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>); deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>);
deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>); deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>);
deserialize_default!(deserialize_vec_groupuser, Vec<GroupUser>);
deserialize_default!(deserialize_vec_grouppeer, Vec<GroupPeer>);
deserialize_default!(deserialize_keypair, KeyPair); deserialize_default!(deserialize_keypair, KeyPair);
deserialize_default!(deserialize_size, Size); deserialize_default!(deserialize_size, Size);
deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>); deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>);

View File

@ -1187,6 +1187,24 @@ pub fn main_load_ab() -> String {
serde_json::to_string(&config::Ab::load()).unwrap_or_default() 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) { pub fn session_send_pointer(session_id: SessionID, msg: String) {
super::flutter::session_send_pointer(session_id, msg); super::flutter::session_send_pointer(session_id, msg);
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", "自动关闭不活跃的会话"), ("auto_disconnect_option_tip", "自动关闭不活跃的会话"),
("Connection failed due to inactivity", "由于长时间无操作, 连接被自动断开"), ("Connection failed due to inactivity", "由于长时间无操作, 连接被自动断开"),
("Check for software update on startup", "启动时检查软件更新"), ("Check for software update on startup", "启动时检查软件更新"),
("upgrade_rustdesk_server_pro_to_{}_tip", "请升级专业版服务器到{}或更高版本!"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -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"), ("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"), ("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"), ("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -90,5 +90,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Decline", "Decline"), ("Decline", "Decline"),
("auto_disconnect_option_tip", "Automatically close incoming sessions on user inactivity"), ("auto_disconnect_option_tip", "Automatically close incoming sessions on user inactivity"),
("Connection failed due to inactivity", "Automatically disconnected due to 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(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -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"), ("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."), ("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."), ("Check for software update on startup", "Periksa pembaruan aplikasi saat sistem dinyalakan."),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", "Connessione non riuscita a causa di inattività"), ("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"), ("Check for software update on startup", "All'avvio programma verifica presenza di aggiornamenti"),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -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ā"), ("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ēļ"), ("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"), ("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(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -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"), ("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"), ("Connection failed due to inactivity", "Automatycznie rozłącz przy bezczynności"),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", "Автоматически закрывать входящие сеансы при неактивности пользователя"), ("auto_disconnect_option_tip", "Автоматически закрывать входящие сеансы при неактивности пользователя"),
("Connection failed due to inactivity", "Подключение не выполнено из-за неактивности"), ("Connection failed due to inactivity", "Подключение не выполнено из-за неактивности"),
("Check for software update on startup", "Проверять обновления программы при запуске"), ("Check for software update on startup", "Проверять обновления программы при запуске"),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,5 +555,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", ""),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }