opt password sync, opt ab widgets (#7582)
* Opt sync conctrl with password source, add some comments * For sync from recent, legacy ab remove forceRelay, rdpPort, rdpUsername, because it's not used, personal ab add sync hash * Opt style of add Id dialog Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
parent
74af7ef8b2
commit
d7b47b49d2
@ -87,7 +87,10 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildAbDropdown(),
|
_buildAbDropdown(),
|
||||||
_buildTagHeader().marginOnly(left: 8.0, right: 0),
|
_buildTagHeader().marginOnly(
|
||||||
|
left: 8.0,
|
||||||
|
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
|
||||||
|
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -415,6 +418,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
var passwordVisible = false;
|
||||||
IDTextEditingController idController = IDTextEditingController(text: '');
|
IDTextEditingController idController = IDTextEditingController(text: '');
|
||||||
TextEditingController aliasController = TextEditingController(text: '');
|
TextEditingController aliasController = TextEditingController(text: '');
|
||||||
TextEditingController passwordController = TextEditingController(text: '');
|
TextEditingController passwordController = TextEditingController(text: '');
|
||||||
@ -460,6 +464,24 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double marginBottom = 4;
|
double marginBottom = 4;
|
||||||
|
|
||||||
|
row({required Widget lable, required Widget input}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
!isMobile
|
||||||
|
? ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: lable.marginOnly(right: 10))
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
Expanded(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 200),
|
||||||
|
child: input),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: !isMobile ? 8 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Add ID")),
|
title: Text(translate("Add ID")),
|
||||||
content: Column(
|
content: Column(
|
||||||
@ -467,9 +489,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
row(
|
||||||
alignment: Alignment.centerLeft,
|
lable: Row(
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'*',
|
'*',
|
||||||
@ -481,36 +502,51 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).marginOnly(bottom: marginBottom),
|
input: TextField(
|
||||||
TextField(
|
|
||||||
controller: idController,
|
controller: idController,
|
||||||
inputFormatters: [IDTextInputFormatter()],
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
decoration:
|
decoration: InputDecoration(
|
||||||
InputDecoration(errorText: errorMsg, errorMaxLines: 5),
|
labelText: !isMobile ? null : translate('ID'),
|
||||||
),
|
errorText: errorMsg,
|
||||||
Align(
|
errorMaxLines: 5),
|
||||||
alignment: Alignment.centerLeft,
|
)),
|
||||||
child: Text(
|
row(
|
||||||
|
lable: Text(
|
||||||
translate('Alias'),
|
translate('Alias'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
).marginOnly(top: 8, bottom: marginBottom),
|
input: TextField(
|
||||||
TextField(
|
|
||||||
controller: aliasController,
|
controller: aliasController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: !isMobile ? null : translate('Alias'),
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
if (isCurrentAbShared)
|
if (isCurrentAbShared)
|
||||||
Align(
|
row(
|
||||||
alignment: Alignment.centerLeft,
|
lable: Text(
|
||||||
child: Text(
|
|
||||||
translate('Password'),
|
translate('Password'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
).marginOnly(top: 8, bottom: marginBottom),
|
input: TextField(
|
||||||
if (isCurrentAbShared)
|
|
||||||
TextField(
|
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
obscureText: true,
|
obscureText: !passwordVisible,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: !isMobile ? null : translate('Password'),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
passwordVisible
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
color: MyTheme.lightTheme.primaryColor),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
passwordVisible = !passwordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -518,6 +554,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
).marginOnly(top: 8, bottom: marginBottom),
|
).marginOnly(top: 8, bottom: marginBottom),
|
||||||
|
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
@ -1918,11 +1918,9 @@ void addPeersToAbDialog(
|
|||||||
Future<bool> addTo(String abname) async {
|
Future<bool> addTo(String abname) async {
|
||||||
final mapList = peers.map((e) {
|
final mapList = peers.map((e) {
|
||||||
var json = e.toJson();
|
var json = e.toJson();
|
||||||
// remove shared password when add to other address book
|
// remove password when add to another address book to avoid re-share
|
||||||
json.remove('password');
|
json.remove('password');
|
||||||
if (gFFI.abModel.addressbooks[abname]?.isPersonal() != true) {
|
|
||||||
json.remove('hash');
|
json.remove('hash');
|
||||||
}
|
|
||||||
return json;
|
return json;
|
||||||
}).toList();
|
}).toList();
|
||||||
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
|
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
|
||||||
@ -1986,6 +1984,7 @@ void addPeersToAbDialog(
|
|||||||
content: Obx(() => Column(
|
content: Obx(() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
// https://github.com/flutter/flutter/issues/145081
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
initialSelection: currentName.value,
|
initialSelection: currentName.value,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
@ -2026,18 +2025,23 @@ void addPeersToAbDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setSharedAbPasswordDialog(String abName, Peer peer) {
|
void setSharedAbPasswordDialog(String abName, Peer peer) {
|
||||||
TextEditingController controller = TextEditingController(text: peer.password);
|
TextEditingController controller = TextEditingController(text: '');
|
||||||
RxBool isInProgress = false.obs;
|
RxBool isInProgress = false.obs;
|
||||||
|
RxBool isInputEmpty = true.obs;
|
||||||
|
bool passwordVisible = false;
|
||||||
|
controller.addListener(() {
|
||||||
|
isInputEmpty.value = controller.text.isEmpty;
|
||||||
|
});
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() async {
|
change(String password) async {
|
||||||
isInProgress.value = true;
|
isInProgress.value = true;
|
||||||
bool res = await gFFI.abModel
|
bool res =
|
||||||
.changeSharedPassword(abName, peer.id, controller.text);
|
await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
|
||||||
close();
|
|
||||||
isInProgress.value = false;
|
isInProgress.value = false;
|
||||||
if (res) {
|
if (res) {
|
||||||
showToast(translate('Successful'));
|
showToast(translate('Successful'));
|
||||||
}
|
}
|
||||||
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
@ -2049,15 +2053,31 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.key, color: MyTheme.accent),
|
Icon(Icons.key, color: MyTheme.accent),
|
||||||
Text(translate('Set shared password')).paddingOnly(left: 10),
|
Text(translate(peer.password.isEmpty
|
||||||
|
? 'Set shared password'
|
||||||
|
: 'Change Password'))
|
||||||
|
.paddingOnly(left: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Obx(() => Column(children: [
|
content: Obx(() => Column(children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: true,
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
obscureText: !passwordVisible,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
passwordVisible ? Icons.visibility : Icons.visibility_off,
|
||||||
|
color: MyTheme.lightTheme.primaryColor),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
passwordVisible = !passwordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!gFFI.abModel.current.isPersonal())
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
||||||
Text(
|
Text(
|
||||||
@ -2075,13 +2095,22 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
onPressed: cancel,
|
onPressed: cancel,
|
||||||
isOutline: true,
|
isOutline: true,
|
||||||
),
|
),
|
||||||
|
if (peer.password.isNotEmpty)
|
||||||
dialogButton(
|
dialogButton(
|
||||||
|
"Remove",
|
||||||
|
icon: Icon(Icons.delete_outline_rounded),
|
||||||
|
onPressed: () => change(''),
|
||||||
|
buttonStyle: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
|
),
|
||||||
|
Obx(() => dialogButton(
|
||||||
"OK",
|
"OK",
|
||||||
icon: Icon(Icons.done_rounded),
|
icon: Icon(Icons.done_rounded),
|
||||||
onPressed: submit,
|
onPressed:
|
||||||
),
|
isInputEmpty.value ? null : () => change(controller.text),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
onSubmit: submit,
|
onSubmit: isInputEmpty.value ? null : () => change(controller.text),
|
||||||
onCancel: cancel,
|
onCancel: cancel,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -151,9 +151,18 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
width: isMobile ? 50 : 42,
|
width: isMobile ? 50 : 42,
|
||||||
height: isMobile ? 50 : null,
|
height: isMobile ? 50 : null,
|
||||||
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||||
.paddingAll(6),
|
.paddingAll(6),
|
||||||
|
if (_shouldBuildPasswordIcon(peer))
|
||||||
|
Positioned(
|
||||||
|
top: 1,
|
||||||
|
left: 1,
|
||||||
|
child: Icon(Icons.key, size: 6, color: Colors.white),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -216,12 +225,6 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_shouldBuildPasswordIcon(peer))
|
|
||||||
Positioned(
|
|
||||||
top: 2,
|
|
||||||
left: isMobile ? 60 : 50,
|
|
||||||
child: Icon(Icons.key, size: 12),
|
|
||||||
),
|
|
||||||
if (colors.isNotEmpty)
|
if (colors.isNotEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
@ -329,7 +332,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: 4,
|
top: 4,
|
||||||
left: 12,
|
left: 12,
|
||||||
child: Icon(Icons.key, size: 12),
|
child: Icon(Icons.key, size: 12, color: Colors.white),
|
||||||
),
|
),
|
||||||
if (colors.isNotEmpty)
|
if (colors.isNotEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -1102,7 +1105,8 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
MenuEntryBase<String> _changeSharedAbPassword() {
|
MenuEntryBase<String> _changeSharedAbPassword() {
|
||||||
return MenuEntryButton<String>(
|
return MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Text(
|
childBuilder: (TextStyle? style) => Text(
|
||||||
translate('Set shared password'),
|
translate(
|
||||||
|
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
proc: () {
|
proc: () {
|
||||||
|
@ -320,7 +320,7 @@ class AbModel {
|
|||||||
peer['password'] = password;
|
peer['password'] = password;
|
||||||
}
|
}
|
||||||
final ret = await addPeersTo([peer], _currentName.value);
|
final ret = await addPeersTo([peer], _currentName.value);
|
||||||
_timerCounter = 0;
|
_syncAllFromRecent = true;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +364,7 @@ class AbModel {
|
|||||||
final personalAb = addressbooks[_personalAddressBookName];
|
final personalAb = addressbooks[_personalAddressBookName];
|
||||||
if (personalAb != null) {
|
if (personalAb != null) {
|
||||||
ret = await personalAb.changePersonalHashPassword(id, hash);
|
ret = await personalAb.changePersonalHashPassword(id, hash);
|
||||||
await pullNonLegacyAfterChange();
|
await personalAb.pullAb(quiet: true);
|
||||||
} else {
|
} else {
|
||||||
final legacyAb = addressbooks[_legacyAddressBookName];
|
final legacyAb = addressbooks[_legacyAddressBookName];
|
||||||
if (legacyAb != null) {
|
if (legacyAb != null) {
|
||||||
@ -377,9 +377,10 @@ class AbModel {
|
|||||||
|
|
||||||
Future<bool> changeSharedPassword(
|
Future<bool> changeSharedPassword(
|
||||||
String abName, String id, String password) async {
|
String abName, String id, String password) async {
|
||||||
final ret =
|
final ab = addressbooks[abName];
|
||||||
await addressbooks[abName]?.changeSharedPassword(id, password) ?? false;
|
if (ab == null) return false;
|
||||||
await pullNonLegacyAfterChange();
|
final ret = await ab.changeSharedPassword(id, password);
|
||||||
|
await ab.pullAb(quiet: true);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,9 +539,7 @@ class AbModel {
|
|||||||
"name": key,
|
"name": key,
|
||||||
"tags": value.tags,
|
"tags": value.tags,
|
||||||
"peers": value.peers
|
"peers": value.peers
|
||||||
.map((e) => value.isPersonal()
|
.map((e) => e.toCustomJson(includingHash: value.isPersonal()))
|
||||||
? e.toPersonalAbUploadJson(true)
|
|
||||||
: e.toSharedAbCacheJson())
|
|
||||||
.toList(),
|
.toList(),
|
||||||
"tag_colors": jsonEncode(value.tagColors)
|
"tag_colors": jsonEncode(value.tagColors)
|
||||||
});
|
});
|
||||||
@ -745,6 +744,10 @@ abstract class BaseAb {
|
|||||||
name() == _legacyAddressBookName;
|
name() == _legacyAddressBookName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isLegacy() {
|
||||||
|
return name() == _legacyAddressBookName;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> pullAb({quiet = false}) async {
|
Future<void> pullAb({quiet = false}) async {
|
||||||
debugPrint("pull ab \"${name()}\"");
|
debugPrint("pull ab \"${name()}\"");
|
||||||
if (abLoading.value) return;
|
if (abLoading.value) return;
|
||||||
@ -1049,9 +1052,6 @@ class LegacyAb extends BaseAb {
|
|||||||
p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname;
|
p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname;
|
||||||
p.platform = r.platform.isEmpty ? p.platform : r.platform;
|
p.platform = r.platform.isEmpty ? p.platform : r.platform;
|
||||||
p.alias = p.alias.isEmpty ? r.alias : p.alias;
|
p.alias = p.alias.isEmpty ? r.alias : p.alias;
|
||||||
p.forceAlwaysRelay = r.forceAlwaysRelay;
|
|
||||||
p.rdpPort = r.rdpPort;
|
|
||||||
p.rdpUsername = r.rdpUsername;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1151,7 +1151,7 @@ class LegacyAb extends BaseAb {
|
|||||||
|
|
||||||
Map<String, dynamic> _serialize() {
|
Map<String, dynamic> _serialize() {
|
||||||
final peersJsonData =
|
final peersJsonData =
|
||||||
peers.map((e) => e.toPersonalAbUploadJson(true)).toList();
|
peers.map((e) => e.toCustomJson(includingHash: true)).toList();
|
||||||
for (var e in tags) {
|
for (var e in tags) {
|
||||||
if (tagColors[e] == null) {
|
if (tagColors[e] == null) {
|
||||||
tagColors[e] = str2color2(e, existing: tagColors.values.toList()).value;
|
tagColors[e] = str2color2(e, existing: tagColors.values.toList()).value;
|
||||||
@ -1491,38 +1491,55 @@ class Ab extends BaseAb {
|
|||||||
Future<bool> changePersonalHashPassword(String id, String hash) async {
|
Future<bool> changePersonalHashPassword(String id, String hash) async {
|
||||||
if (!personal) return false;
|
if (!personal) return false;
|
||||||
if (!peers.any((e) => e.id == id)) return false;
|
if (!peers.any((e) => e.id == id)) return false;
|
||||||
return _setPassword({"id": id, "hash": hash});
|
return await _setPassword({"id": id, "hash": hash});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> changeSharedPassword(String id, String password) async {
|
Future<bool> changeSharedPassword(String id, String password) async {
|
||||||
if (personal) return false;
|
if (personal) return false;
|
||||||
return _setPassword({"id": id, "password": password});
|
return await _setPassword({"id": id, "password": password});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> syncFromRecent(List<Peer> recents) async {
|
Future<void> syncFromRecent(List<Peer> recents) async {
|
||||||
bool uiUpdate = false;
|
bool uiUpdate = false;
|
||||||
bool peerSyncEqual(Peer a, Peer b) {
|
bool saveCache = false;
|
||||||
return a.username == b.username &&
|
|
||||||
a.platform == b.platform &&
|
|
||||||
a.hostname == b.hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> syncOnePeer(Peer p, Peer r) async {
|
|
||||||
p.username = r.username;
|
|
||||||
p.hostname = r.hostname;
|
|
||||||
p.platform = r.platform;
|
|
||||||
final api =
|
final api =
|
||||||
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
|
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
|
||||||
var headers = getHttpHeaders();
|
var headers = getHttpHeaders();
|
||||||
headers['Content-Type'] = "application/json";
|
headers['Content-Type'] = "application/json";
|
||||||
final body = jsonEncode({
|
|
||||||
"id": p.id,
|
Future<bool> trySyncOnePeer(Peer p, Peer r) async {
|
||||||
"username": r.username,
|
var map = Map<String, String>.fromEntries([]);
|
||||||
"hostname": r.hostname,
|
if (p.sameServer != true &&
|
||||||
"platform": r.platform
|
r.username.isNotEmpty &&
|
||||||
});
|
p.username != r.username) {
|
||||||
|
p.username = r.username;
|
||||||
|
map['username'] = r.username;
|
||||||
|
}
|
||||||
|
if (p.sameServer != true &&
|
||||||
|
r.hostname.isNotEmpty &&
|
||||||
|
p.hostname != r.hostname) {
|
||||||
|
p.hostname = r.hostname;
|
||||||
|
map['hostname'] = r.hostname;
|
||||||
|
}
|
||||||
|
if (p.sameServer != true &&
|
||||||
|
r.platform.isNotEmpty &&
|
||||||
|
p.platform != r.platform) {
|
||||||
|
p.platform = r.platform;
|
||||||
|
map['platform'] = r.platform;
|
||||||
|
}
|
||||||
|
if (personal && r.hash.isNotEmpty && p.hash != r.hash) {
|
||||||
|
p.hash = r.hash;
|
||||||
|
map['hash'] = r.hash;
|
||||||
|
saveCache = true;
|
||||||
|
}
|
||||||
|
if (map.isEmpty) {
|
||||||
|
// no need to sync
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
map['id'] = p.id;
|
||||||
|
final body = jsonEncode(map);
|
||||||
final resp = await http.put(Uri.parse(api), headers: headers, body: body);
|
final resp = await http.put(Uri.parse(api), headers: headers, body: body);
|
||||||
final errMsg = _jsonDecodeActionResp(resp);
|
final errMsg = _jsonDecodeActionResp(resp);
|
||||||
if (errMsg.isNotEmpty) {
|
if (errMsg.isNotEmpty) {
|
||||||
@ -1534,35 +1551,20 @@ class Ab extends BaseAb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Remove this because IDs that are not on the server can't be synced, then sync will happen every startup.
|
// Not add new peers because IDs that are not on the server can't be synced, then sync will happen every startup.
|
||||||
// Try add new peers to personal ab
|
for (var p in peers) {
|
||||||
if (personal) {
|
|
||||||
for (var r in recents) {
|
|
||||||
if (peers.length < gFFI.abModel._maxPeerOneAb) {
|
|
||||||
if (!peers.any((e) => e.id == r.id)) {
|
|
||||||
var err = await addPeers([r.toPersonalAbUploadJson(true)]);
|
|
||||||
if (err == null) {
|
|
||||||
peers.add(r);
|
|
||||||
uiUpdate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
final syncPeers = peers.where((p0) => p0.sameServer != true);
|
|
||||||
for (var p in syncPeers) {
|
|
||||||
Peer? r = recents.firstWhereOrNull((e) => e.id == p.id);
|
Peer? r = recents.firstWhereOrNull((e) => e.id == p.id);
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
if (!peerSyncEqual(p, r)) {
|
await trySyncOnePeer(p, r);
|
||||||
await syncOnePeer(p, r);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pull cannot be used for sync to avoid cyclic sync.
|
// Pull cannot be used for sync to avoid cyclic sync.
|
||||||
if (uiUpdate && gFFI.abModel.currentName.value == profile.name) {
|
if (uiUpdate && gFFI.abModel.currentName.value == profile.name) {
|
||||||
peers.refresh();
|
peers.refresh();
|
||||||
}
|
}
|
||||||
|
if (saveCache) {
|
||||||
|
gFFI.abModel._saveCache();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrint('syncFromRecent err: ${err.toString()}');
|
debugPrint('syncFromRecent err: ${err.toString()}');
|
||||||
}
|
}
|
||||||
|
@ -352,13 +352,13 @@ class FfiModel with ChangeNotifier {
|
|||||||
handleReloading(evt);
|
handleReloading(evt);
|
||||||
} else if (name == 'plugin_option') {
|
} else if (name == 'plugin_option') {
|
||||||
handleOption(evt);
|
handleOption(evt);
|
||||||
} else if (name == "sync_peer_password_to_ab") {
|
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||||
if (desktopType == DesktopType.main) {
|
if (desktopType == DesktopType.main) {
|
||||||
final id = evt['id'];
|
final id = evt['id'];
|
||||||
final password = evt['password'];
|
final hash = evt['hash'];
|
||||||
if (id != null && password != null) {
|
if (id != null && hash != null) {
|
||||||
gFFI.abModel
|
gFFI.abModel
|
||||||
.changePersonalHashPassword(id.toString(), password.toString());
|
.changePersonalHashPassword(id.toString(), hash.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (name == "cm_file_transfer_log") {
|
} else if (name == "cm_file_transfer_log") {
|
||||||
|
@ -61,7 +61,7 @@ class Peer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toPersonalAbUploadJson(bool includingHash) {
|
Map<String, dynamic> toCustomJson({required bool includingHash}) {
|
||||||
var res = <String, dynamic>{
|
var res = <String, dynamic>{
|
||||||
"id": id,
|
"id": id,
|
||||||
"username": username,
|
"username": username,
|
||||||
@ -76,32 +76,6 @@ class Peer {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toSharedAbUploadJson(bool includingPassword) {
|
|
||||||
var res = <String, dynamic>{
|
|
||||||
"id": id,
|
|
||||||
"username": username,
|
|
||||||
"hostname": hostname,
|
|
||||||
"platform": platform,
|
|
||||||
"alias": alias,
|
|
||||||
"tags": tags,
|
|
||||||
};
|
|
||||||
if (includingPassword) {
|
|
||||||
res['password'] = password;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toSharedAbCacheJson() {
|
|
||||||
return <String, dynamic>{
|
|
||||||
"id": id,
|
|
||||||
"username": username,
|
|
||||||
"hostname": hostname,
|
|
||||||
"platform": platform,
|
|
||||||
"alias": alias,
|
|
||||||
"tags": tags,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toGroupCacheJson() {
|
Map<String, dynamic> toGroupCacheJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
"id": id,
|
"id": id,
|
||||||
|
170
src/client.rs
170
src/client.rs
@ -1129,6 +1129,53 @@ impl VideoHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The source of sent password
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum PasswordSource {
|
||||||
|
PersonalAb(Vec<u8>),
|
||||||
|
SharedAb(String),
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PasswordSource {
|
||||||
|
fn default() -> Self {
|
||||||
|
PasswordSource::Undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordSource {
|
||||||
|
// Whether the password is personal ab password
|
||||||
|
pub fn is_personal_ab(&self, password: &[u8]) -> bool {
|
||||||
|
if password.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
PasswordSource::PersonalAb(p) => p == password,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the password is shared ab password
|
||||||
|
pub fn is_shared_ab(&self, password: &[u8], hash: &Hash) -> bool {
|
||||||
|
if password.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
PasswordSource::SharedAb(p) => Self::equal(p, password, hash),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the password equals to the connected password
|
||||||
|
fn equal(password: &str, connected_password: &[u8], hash: &Hash) -> bool {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(password);
|
||||||
|
hasher.update(&hash.salt);
|
||||||
|
let res = hasher.finalize();
|
||||||
|
connected_password[..] == res[..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Login config handler for [`Client`].
|
/// Login config handler for [`Client`].
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct LoginConfigHandler {
|
pub struct LoginConfigHandler {
|
||||||
@ -1155,7 +1202,8 @@ pub struct LoginConfigHandler {
|
|||||||
pub mark_unsupported: Vec<CodecFormat>,
|
pub mark_unsupported: Vec<CodecFormat>,
|
||||||
pub selected_windows_session_id: Option<u32>,
|
pub selected_windows_session_id: Option<u32>,
|
||||||
pub peer_info: Option<PeerInfo>,
|
pub peer_info: Option<PeerInfo>,
|
||||||
shared_password: Option<String>, // used to distinguish whether it is connected with a shared password
|
password_source: PasswordSource, // where the sent password comes from
|
||||||
|
shared_password: Option<String>, // Store the shared password
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for LoginConfigHandler {
|
impl Deref for LoginConfigHandler {
|
||||||
@ -1829,20 +1877,25 @@ impl LoginConfigHandler {
|
|||||||
platform: pi.platform.clone(),
|
platform: pi.platform.clone(),
|
||||||
};
|
};
|
||||||
let mut config = self.load_config();
|
let mut config = self.load_config();
|
||||||
let connected_with_shared_password = self.is_connected_with_shared_password();
|
|
||||||
let old_config_password = config.password.clone();
|
|
||||||
config.info = serde;
|
config.info = serde;
|
||||||
let password = self.password.clone();
|
let password = self.password.clone();
|
||||||
let password0 = config.password.clone();
|
let password0 = config.password.clone();
|
||||||
let remember = self.remember;
|
let remember = self.remember;
|
||||||
|
let hash = self.hash.clone();
|
||||||
if remember {
|
if remember {
|
||||||
if !password.is_empty() && password != password0 {
|
// remember is true: use PeerConfig password or ui login
|
||||||
config.password = password;
|
// not sync shared password to recent
|
||||||
|
if !password.is_empty()
|
||||||
|
&& password != password0
|
||||||
|
&& !self.password_source.is_shared_ab(&password, &hash)
|
||||||
|
{
|
||||||
|
config.password = password.clone();
|
||||||
log::debug!("remember password of {}", self.id);
|
log::debug!("remember password of {}", self.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.save_ab_password_to_recent {
|
if self.password_source.is_personal_ab(&password) {
|
||||||
config.password = password;
|
// sync personal ab password to recent automatically
|
||||||
|
config.password = password.clone();
|
||||||
log::debug!("save ab password of {} to recent", self.id);
|
log::debug!("save ab password of {} to recent", self.id);
|
||||||
} else if !password0.is_empty() {
|
} else if !password0.is_empty() {
|
||||||
config.password = Default::default();
|
config.password = Default::default();
|
||||||
@ -1863,13 +1916,16 @@ impl LoginConfigHandler {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
{
|
{
|
||||||
if !connected_with_shared_password && remember && !config.password.is_empty() {
|
// sync connected password to personal ab automatically if it is not shared password
|
||||||
// sync ab password with PeerConfig password
|
if !config.password.is_empty()
|
||||||
let password = base64::encode(config.password.clone(), base64::Variant::Original);
|
&& !self.password_source.is_shared_ab(&password, &hash)
|
||||||
|
&& !self.password_source.is_personal_ab(&password)
|
||||||
|
{
|
||||||
|
let hash = base64::encode(config.password.clone(), base64::Variant::Original);
|
||||||
let evt: HashMap<&str, String> = HashMap::from([
|
let evt: HashMap<&str, String> = HashMap::from([
|
||||||
("name", "sync_peer_password_to_ab".to_string()),
|
("name", "sync_peer_hash_password_to_personal_ab".to_string()),
|
||||||
("id", self.id.clone()),
|
("id", self.id.clone()),
|
||||||
("password", password),
|
("hash", hash),
|
||||||
]);
|
]);
|
||||||
let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned());
|
let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned());
|
||||||
crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt);
|
crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt);
|
||||||
@ -1893,27 +1949,12 @@ impl LoginConfigHandler {
|
|||||||
config.keyboard_mode = KeyboardMode::Legacy.to_string();
|
config.keyboard_mode = KeyboardMode::Legacy.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// keep hash password unchanged if connected with shared password
|
|
||||||
if connected_with_shared_password {
|
|
||||||
config.password = old_config_password;
|
|
||||||
}
|
|
||||||
// no matter if change, for update file time
|
// no matter if change, for update file time
|
||||||
self.save_config(config);
|
self.save_config(config);
|
||||||
self.supported_encoding = pi.encoding.clone().unwrap_or_default();
|
self.supported_encoding = pi.encoding.clone().unwrap_or_default();
|
||||||
log::info!("peer info supported_encoding:{:?}", self.supported_encoding);
|
log::info!("peer info supported_encoding:{:?}", self.supported_encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_connected_with_shared_password(&self) -> bool {
|
|
||||||
if let Some(shared_password) = self.shared_password.as_ref() {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(shared_password);
|
|
||||||
hasher.update(&self.hash.salt);
|
|
||||||
let res = hasher.finalize();
|
|
||||||
return self.password.clone()[..] == res[..];
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_remote_dir(&self) -> String {
|
pub fn get_remote_dir(&self) -> String {
|
||||||
serde_json::from_str::<HashMap<String, String>>(&self.get_option("remote_dir"))
|
serde_json::from_str::<HashMap<String, String>>(&self.get_option("remote_dir"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@ -2565,7 +2606,6 @@ pub fn handle_login_error(
|
|||||||
err: &str,
|
err: &str,
|
||||||
interface: &impl Interface,
|
interface: &impl Interface,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
lc.write().unwrap().save_ab_password_to_recent = false;
|
|
||||||
if err == LOGIN_MSG_PASSWORD_EMPTY {
|
if err == LOGIN_MSG_PASSWORD_EMPTY {
|
||||||
lc.write().unwrap().password = Default::default();
|
lc.write().unwrap().password = Default::default();
|
||||||
interface.msgbox("input-password", "Password Required", "", "");
|
interface.msgbox("input-password", "Password Required", "", "");
|
||||||
@ -2617,14 +2657,20 @@ pub async fn handle_hash(
|
|||||||
peer: &mut Stream,
|
peer: &mut Stream,
|
||||||
) {
|
) {
|
||||||
lc.write().unwrap().hash = hash.clone();
|
lc.write().unwrap().hash = hash.clone();
|
||||||
|
// Take care of password application order
|
||||||
|
|
||||||
|
// switch_uuid
|
||||||
let uuid = lc.write().unwrap().switch_uuid.take();
|
let uuid = lc.write().unwrap().switch_uuid.take();
|
||||||
if let Some(uuid) = uuid {
|
if let Some(uuid) = uuid {
|
||||||
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
||||||
send_switch_login_request(lc.clone(), peer, uuid).await;
|
send_switch_login_request(lc.clone(), peer, uuid).await;
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// last password
|
||||||
let mut password = lc.read().unwrap().password.clone();
|
let mut password = lc.read().unwrap().password.clone();
|
||||||
|
// preset password
|
||||||
if password.is_empty() {
|
if password.is_empty() {
|
||||||
if !password_preset.is_empty() {
|
if !password_preset.is_empty() {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
@ -2632,31 +2678,32 @@ pub async fn handle_hash(
|
|||||||
hasher.update(&hash.salt);
|
hasher.update(&hash.salt);
|
||||||
let res = hasher.finalize();
|
let res = hasher.finalize();
|
||||||
password = res[..].into();
|
password = res[..].into();
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// shared password
|
||||||
|
// Currently it's used only when click shared ab peer card
|
||||||
|
let shared_password = lc.write().unwrap().shared_password.take();
|
||||||
|
if let Some(shared_password) = shared_password {
|
||||||
|
if !shared_password.is_empty() {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(shared_password.clone());
|
||||||
|
hasher.update(&hash.salt);
|
||||||
|
let res = hasher.finalize();
|
||||||
|
password = res[..].into();
|
||||||
|
lc.write().unwrap().password_source = PasswordSource::SharedAb(shared_password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// peer config password
|
||||||
if password.is_empty() {
|
if password.is_empty() {
|
||||||
password = lc.read().unwrap().config.password.clone();
|
password = lc.read().unwrap().config.password.clone();
|
||||||
|
if !password.is_empty() {
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// personal ab password
|
||||||
if password.is_empty() {
|
if password.is_empty() {
|
||||||
let access_token = LocalConfig::get_option("access_token");
|
try_get_password_from_personal_ab(lc.clone(), &mut password);
|
||||||
let ab = hbb_common::config::Ab::load();
|
|
||||||
if !access_token.is_empty() && access_token == ab.access_token {
|
|
||||||
let id = lc.read().unwrap().id.clone();
|
|
||||||
if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) {
|
|
||||||
if let Some(p) = ab
|
|
||||||
.peers
|
|
||||||
.iter()
|
|
||||||
.find_map(|p| if p.id == id { Some(p) } else { None })
|
|
||||||
{
|
|
||||||
if let Ok(hash) = base64::decode(p.hash.clone(), base64::Variant::Original) {
|
|
||||||
if !hash.is_empty() {
|
|
||||||
password = hash;
|
|
||||||
lc.write().unwrap().save_ab_password_to_recent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lc.write().unwrap().password = password.clone();
|
lc.write().unwrap().password = password.clone();
|
||||||
let password = if password.is_empty() {
|
let password = if password.is_empty() {
|
||||||
@ -2677,6 +2724,31 @@ pub async fn handle_hash(
|
|||||||
lc.write().unwrap().hash = hash;
|
lc.write().unwrap().hash = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_get_password_from_personal_ab(lc: Arc<RwLock<LoginConfigHandler>>, password: &mut Vec<u8>) {
|
||||||
|
let access_token = LocalConfig::get_option("access_token");
|
||||||
|
let ab = hbb_common::config::Ab::load();
|
||||||
|
if !access_token.is_empty() && access_token == ab.access_token {
|
||||||
|
let id = lc.read().unwrap().id.clone();
|
||||||
|
if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) {
|
||||||
|
if let Some(p) = ab
|
||||||
|
.peers
|
||||||
|
.iter()
|
||||||
|
.find_map(|p| if p.id == id { Some(p) } else { None })
|
||||||
|
{
|
||||||
|
if let Ok(hash_password) = base64::decode(p.hash.clone(), base64::Variant::Original)
|
||||||
|
{
|
||||||
|
if !hash_password.is_empty() {
|
||||||
|
*password = hash_password.clone();
|
||||||
|
lc.write().unwrap().password_source =
|
||||||
|
PasswordSource::PersonalAb(hash_password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send login message to peer.
|
/// Send login message to peer.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@ -2722,9 +2794,13 @@ pub async fn handle_login_from_ui(
|
|||||||
let mut password2 = lc.read().unwrap().password.clone();
|
let mut password2 = lc.read().unwrap().password.clone();
|
||||||
if password2.is_empty() {
|
if password2.is_empty() {
|
||||||
password2 = lc.read().unwrap().config.password.clone();
|
password2 = lc.read().unwrap().config.password.clone();
|
||||||
|
if !password2.is_empty() {
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
password2
|
password2
|
||||||
} else {
|
} else {
|
||||||
|
lc.write().unwrap().password_source = Default::default();
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(password);
|
hasher.update(password);
|
||||||
hasher.update(&lc.read().unwrap().hash.salt);
|
hasher.update(&lc.read().unwrap().hash.salt);
|
||||||
|
@ -1050,8 +1050,17 @@ pub fn session_add(
|
|||||||
|
|
||||||
LocalConfig::set_remote_id(&id);
|
LocalConfig::set_remote_id(&id);
|
||||||
|
|
||||||
|
let mut preset_password = password.clone();
|
||||||
|
let shared_password = if is_shared_password {
|
||||||
|
// To achieve a flexible password application order, we dont' treat shared password as a preset password.
|
||||||
|
preset_password = Default::default();
|
||||||
|
Some(password)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let session: Session<FlutterHandler> = Session {
|
let session: Session<FlutterHandler> = Session {
|
||||||
password: password.clone(),
|
password: preset_password,
|
||||||
server_keyboard_enabled: Arc::new(RwLock::new(true)),
|
server_keyboard_enabled: Arc::new(RwLock::new(true)),
|
||||||
server_file_transfer_enabled: Arc::new(RwLock::new(true)),
|
server_file_transfer_enabled: Arc::new(RwLock::new(true)),
|
||||||
server_clipboard_enabled: Arc::new(RwLock::new(true)),
|
server_clipboard_enabled: Arc::new(RwLock::new(true)),
|
||||||
@ -1069,11 +1078,6 @@ pub fn session_add(
|
|||||||
#[cfg(not(feature = "gpucodec"))]
|
#[cfg(not(feature = "gpucodec"))]
|
||||||
let adapter_luid = None;
|
let adapter_luid = None;
|
||||||
|
|
||||||
let shared_password = if is_shared_password {
|
|
||||||
Some(password)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
session.lc.write().unwrap().initialize(
|
session.lc.write().unwrap().initialize(
|
||||||
id.to_owned(),
|
id.to_owned(),
|
||||||
conn_type,
|
conn_type,
|
||||||
|
Loading…
Reference in New Issue
Block a user