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:
21pages
2024-04-02 22:08:47 +08:00
committed by GitHub
parent 74af7ef8b2
commit d7b47b49d2
8 changed files with 369 additions and 243 deletions

View File

@@ -87,7 +87,10 @@ class _AddressBookState extends State<AddressBook> {
child: Column(
children: [
_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(
child: Container(
width: double.infinity,
@@ -415,6 +418,7 @@ class _AddressBookState extends State<AddressBook> {
return;
}
var isInProgress = false;
var passwordVisible = false;
IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: '');
TextEditingController passwordController = TextEditingController(text: '');
@@ -460,6 +464,24 @@ class _AddressBookState extends State<AddressBook> {
}
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(
title: Text(translate("Add ID")),
content: Column(
@@ -467,75 +489,90 @@ class _AddressBookState extends State<AddressBook> {
children: [
Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
'*',
style: TextStyle(color: Colors.red, fontSize: 14),
),
Text(
'ID',
style: style,
),
],
),
).marginOnly(bottom: marginBottom),
TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration:
InputDecoration(errorText: errorMsg, errorMaxLines: 5),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
row(
lable: Row(
children: [
Text(
'*',
style: TextStyle(color: Colors.red, fontSize: 14),
),
Text(
'ID',
style: style,
),
],
),
input: TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'),
errorText: errorMsg,
errorMaxLines: 5),
)),
row(
lable: Text(
translate('Alias'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
TextField(
controller: aliasController,
input: TextField(
controller: aliasController,
decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'),
)),
),
if (isCurrentAbShared)
row(
lable: Text(
translate('Password'),
style: style,
),
input: TextField(
controller: passwordController,
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(
alignment: Alignment.centerLeft,
child: Text(
translate('Password'),
translate('Tags'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
if (isCurrentAbShared)
TextField(
controller: passwordController,
obscureText: true,
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Tags'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
),
],
),
const SizedBox(

View File

@@ -1918,11 +1918,9 @@ void addPeersToAbDialog(
Future<bool> addTo(String abname) async {
final mapList = peers.map((e) {
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');
if (gFFI.abModel.addressbooks[abname]?.isPersonal() != true) {
json.remove('hash');
}
json.remove('hash');
return json;
}).toList();
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
@@ -1986,6 +1984,7 @@ void addPeersToAbDialog(
content: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// https://github.com/flutter/flutter/issues/145081
DropdownMenu(
initialSelection: currentName.value,
onSelected: (value) {
@@ -2026,18 +2025,23 @@ void addPeersToAbDialog(
}
void setSharedAbPasswordDialog(String abName, Peer peer) {
TextEditingController controller = TextEditingController(text: peer.password);
TextEditingController controller = TextEditingController(text: '');
RxBool isInProgress = false.obs;
RxBool isInputEmpty = true.obs;
bool passwordVisible = false;
controller.addListener(() {
isInputEmpty.value = controller.text.isEmpty;
});
gFFI.dialogManager.show((setState, close, context) {
submit() async {
change(String password) async {
isInProgress.value = true;
bool res = await gFFI.abModel
.changeSharedPassword(abName, peer.id, controller.text);
close();
bool res =
await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
isInProgress.value = false;
if (res) {
showToast(translate('Successful'));
}
close();
}
cancel() {
@@ -2049,22 +2053,38 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
mainAxisAlignment: MainAxisAlignment.center,
children: [
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: [
TextField(
controller: controller,
obscureText: 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;
});
},
),
),
),
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
// NOT use Offstage to wrap LinearProgressIndicator
isInProgress.value ? const LinearProgressIndicator() : Offstage()
])),
@@ -2075,13 +2095,22 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
onPressed: cancel,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
if (peer.password.isNotEmpty)
dialogButton(
"Remove",
icon: Icon(Icons.delete_outline_rounded),
onPressed: () => change(''),
buttonStyle: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
),
Obx(() => dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed:
isInputEmpty.value ? null : () => change(controller.text),
)),
],
onSubmit: submit,
onSubmit: isInputEmpty.value ? null : () => change(controller.text),
onCancel: cancel,
);
});

View File

@@ -139,21 +139,30 @@ class _PeerCardState extends State<_PeerCard>
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: Stack(
children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 1,
left: 1,
child: Icon(Icons.key, size: 6, color: Colors.white),
),
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
),
],
)),
Expanded(
child: Container(
decoration: BoxDecoration(
@@ -216,12 +225,6 @@ class _PeerCardState extends State<_PeerCard>
child: child,
),
),
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 2,
left: isMobile ? 60 : 50,
child: Icon(Icons.key, size: 12),
),
if (colors.isNotEmpty)
Positioned(
top: 2,
@@ -329,7 +332,7 @@ class _PeerCardState extends State<_PeerCard>
Positioned(
top: 4,
left: 12,
child: Icon(Icons.key, size: 12),
child: Icon(Icons.key, size: 12, color: Colors.white),
),
if (colors.isNotEmpty)
Positioned(
@@ -1102,7 +1105,8 @@ class AddressBookPeerCard extends BasePeerCard {
MenuEntryBase<String> _changeSharedAbPassword() {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Set shared password'),
translate(
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
style: style,
),
proc: () {

View File

@@ -320,7 +320,7 @@ class AbModel {
peer['password'] = password;
}
final ret = await addPeersTo([peer], _currentName.value);
_timerCounter = 0;
_syncAllFromRecent = true;
return ret;
}
@@ -364,7 +364,7 @@ class AbModel {
final personalAb = addressbooks[_personalAddressBookName];
if (personalAb != null) {
ret = await personalAb.changePersonalHashPassword(id, hash);
await pullNonLegacyAfterChange();
await personalAb.pullAb(quiet: true);
} else {
final legacyAb = addressbooks[_legacyAddressBookName];
if (legacyAb != null) {
@@ -377,9 +377,10 @@ class AbModel {
Future<bool> changeSharedPassword(
String abName, String id, String password) async {
final ret =
await addressbooks[abName]?.changeSharedPassword(id, password) ?? false;
await pullNonLegacyAfterChange();
final ab = addressbooks[abName];
if (ab == null) return false;
final ret = await ab.changeSharedPassword(id, password);
await ab.pullAb(quiet: true);
return ret;
}
@@ -538,9 +539,7 @@ class AbModel {
"name": key,
"tags": value.tags,
"peers": value.peers
.map((e) => value.isPersonal()
? e.toPersonalAbUploadJson(true)
: e.toSharedAbCacheJson())
.map((e) => e.toCustomJson(includingHash: value.isPersonal()))
.toList(),
"tag_colors": jsonEncode(value.tagColors)
});
@@ -745,6 +744,10 @@ abstract class BaseAb {
name() == _legacyAddressBookName;
}
bool isLegacy() {
return name() == _legacyAddressBookName;
}
Future<void> pullAb({quiet = false}) async {
debugPrint("pull ab \"${name()}\"");
if (abLoading.value) return;
@@ -1049,9 +1052,6 @@ class LegacyAb extends BaseAb {
p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname;
p.platform = r.platform.isEmpty ? p.platform : r.platform;
p.alias = p.alias.isEmpty ? r.alias : p.alias;
p.forceAlwaysRelay = r.forceAlwaysRelay;
p.rdpPort = r.rdpPort;
p.rdpUsername = r.rdpUsername;
}
@override
@@ -1151,7 +1151,7 @@ class LegacyAb extends BaseAb {
Map<String, dynamic> _serialize() {
final peersJsonData =
peers.map((e) => e.toPersonalAbUploadJson(true)).toList();
peers.map((e) => e.toCustomJson(includingHash: true)).toList();
for (var e in tags) {
if (tagColors[e] == null) {
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 {
if (!personal) 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
Future<bool> changeSharedPassword(String id, String password) async {
if (personal) return false;
return _setPassword({"id": id, "password": password});
return await _setPassword({"id": id, "password": password});
}
@override
Future<void> syncFromRecent(List<Peer> recents) async {
bool uiUpdate = false;
bool peerSyncEqual(Peer a, Peer b) {
return a.username == b.username &&
a.platform == b.platform &&
a.hostname == b.hostname;
}
bool saveCache = false;
final api =
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
Future<bool> syncOnePeer(Peer p, Peer r) async {
p.username = r.username;
p.hostname = r.hostname;
p.platform = r.platform;
final api =
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final body = jsonEncode({
"id": p.id,
"username": r.username,
"hostname": r.hostname,
"platform": r.platform
});
Future<bool> trySyncOnePeer(Peer p, Peer r) async {
var map = Map<String, String>.fromEntries([]);
if (p.sameServer != true &&
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 errMsg = _jsonDecodeActionResp(resp);
if (errMsg.isNotEmpty) {
@@ -1534,35 +1551,20 @@ class Ab extends BaseAb {
}
try {
/* Remove this 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
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) {
// Not add new peers because IDs that are not on the server can't be synced, then sync will happen every startup.
for (var p in peers) {
Peer? r = recents.firstWhereOrNull((e) => e.id == p.id);
if (r != null) {
if (!peerSyncEqual(p, r)) {
await syncOnePeer(p, r);
}
await trySyncOnePeer(p, r);
}
}
// Pull cannot be used for sync to avoid cyclic sync.
if (uiUpdate && gFFI.abModel.currentName.value == profile.name) {
peers.refresh();
}
if (saveCache) {
gFFI.abModel._saveCache();
}
} catch (err) {
debugPrint('syncFromRecent err: ${err.toString()}');
}

View File

@@ -352,13 +352,13 @@ class FfiModel with ChangeNotifier {
handleReloading(evt);
} else if (name == 'plugin_option') {
handleOption(evt);
} else if (name == "sync_peer_password_to_ab") {
} else if (name == "sync_peer_hash_password_to_personal_ab") {
if (desktopType == DesktopType.main) {
final id = evt['id'];
final password = evt['password'];
if (id != null && password != null) {
final hash = evt['hash'];
if (id != null && hash != null) {
gFFI.abModel
.changePersonalHashPassword(id.toString(), password.toString());
.changePersonalHashPassword(id.toString(), hash.toString());
}
}
} else if (name == "cm_file_transfer_log") {

View File

@@ -61,7 +61,7 @@ class Peer {
};
}
Map<String, dynamic> toPersonalAbUploadJson(bool includingHash) {
Map<String, dynamic> toCustomJson({required bool includingHash}) {
var res = <String, dynamic>{
"id": id,
"username": username,
@@ -76,32 +76,6 @@ class Peer {
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() {
return <String, dynamic>{
"id": id,