diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 9530e851d..3c192c166 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -48,13 +48,14 @@ class _AddressBookState extends State { return Column( children: [ _buildNotEmptyLoading(), + _buildRetryProgress(), _buildErrorBanner( err: gFFI.abModel.pullError, retry: null, close: () => gFFI.abModel.pullError.value = ''), _buildErrorBanner( err: gFFI.abModel.pushError, - retry: () => gFFI.abModel.pushAb(), + retry: () => gFFI.abModel.pushAb(isRetry: true), close: () => gFFI.abModel.pushError.value = ''), Expanded( child: isDesktop @@ -136,6 +137,13 @@ class _AddressBookState extends State { )); } + Widget _buildRetryProgress() { + return Obx(() => Offstage( + offstage: !gFFI.abModel.retrying.value, + child: LinearProgressIndicator(), + )); + } + Widget _buildAddressBookDesktop() { return Row( children: [ @@ -339,7 +347,7 @@ class _AddressBookState extends State { return; } gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); - await gFFI.abModel.pushAb(); + gFFI.abModel.pushAb(); this.setState(() {}); // final currentPeers } @@ -448,7 +456,7 @@ class _AddressBookState extends State { for (final tag in tags) { gFFI.abModel.addTag(tag); } - await gFFI.abModel.pushAb(); + gFFI.abModel.pushAb(); // final currentPeers } close(); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1ab7b1f0..27b485023 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -584,6 +584,7 @@ abstract class BasePeerCard extends StatelessWidget { ), proc: () { bind.mainCreateShortcut(id: id); + showToast(translate('Successful')); }, padding: menuPadding, dismissOnClicked: true, @@ -599,6 +600,7 @@ abstract class BasePeerCard extends StatelessWidget { setter: (bool v) async { await bind.mainSetPeerOption( id: id, key: key, value: bool2option(key, v)); + showToast(translate('Successful')); }, padding: menuPadding, dismissOnClicked: true, @@ -635,6 +637,7 @@ abstract class BasePeerCard extends StatelessWidget { id: id, key: kOptionForceAlwaysRelay, value: bool2option(kOptionForceAlwaysRelay, v)); + showToast(translate('Successful')); }, padding: menuPadding, dismissOnClicked: true, @@ -659,6 +662,7 @@ abstract class BasePeerCard extends StatelessWidget { gFFI.abModel.pushAb(); } else { await bind.mainSetPeerAlias(id: id, alias: newName); + showToast(translate('Successful')); _update(); } } @@ -708,16 +712,25 @@ abstract class BasePeerCard extends StatelessWidget { break; case PeerTabIndex.ab: gFFI.abModel.deletePeer(id); - await gFFI.abModel.pushAb(); + final future = gFFI.abModel.pushAb(); if (shouldSyncAb() && await bind.mainPeerExists(id: peer.id)) { - BotToast.showText( - contentColor: Colors.lightBlue, - text: translate('synced_peer_readded_tip')); + Future.delayed(Duration.zero, () async { + final succ = await future; + if (succ) { + await Future.delayed(Duration(seconds: 2)); // success msg + BotToast.showText( + contentColor: Colors.lightBlue, + text: translate('synced_peer_readded_tip')); + } + }); } break; case PeerTabIndex.group: break; } + if (tab != PeerTabIndex.ab) { + showToast(translate('Successful')); + } } deletePeerConfirmDialog(onSubmit, @@ -737,6 +750,7 @@ abstract class BasePeerCard extends StatelessWidget { ), proc: () { bind.mainForgetPassword(id: id); + showToast(translate('Successful')); }, padding: menuPadding, dismissOnClicked: true, @@ -769,6 +783,7 @@ abstract class BasePeerCard extends StatelessWidget { favs.add(id); await bind.mainStoreFav(favs: favs); } + showToast(translate('Successful')); }(); }, padding: menuPadding, @@ -803,6 +818,7 @@ abstract class BasePeerCard extends StatelessWidget { await bind.mainStoreFav(favs: favs); await reloadFunc(); } + showToast(translate('Successful')); }(); }, padding: menuPadding, @@ -824,7 +840,7 @@ abstract class BasePeerCard extends StatelessWidget { } if (!gFFI.abModel.idContainBy(peer.id)) { gFFI.abModel.addPeer(peer); - await gFFI.abModel.pushAb(); + gFFI.abModel.pushAb(); } }(); }, @@ -1056,7 +1072,7 @@ class AddressBookPeerCard extends BasePeerCard { proc: () { editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async { gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.pushAb(); + gFFI.abModel.pushAb(); }); }, padding: super.menuPadding, @@ -1128,6 +1144,7 @@ void _rdpDialog(String id) async { id: id, key: 'rdp_username', value: username); await bind.mainSetPeerOption( id: id, key: 'rdp_password', value: password); + showToast(translate('Successful')); close(); } diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 54853c568..0937f6f5f 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -343,12 +343,17 @@ class _PeerTabPageState extends State } } gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); - await gFFI.abModel.pushAb(); + final future = gFFI.abModel.pushAb(); if (hasSynced) { - Future.delayed(Duration(seconds: 2), () { - BotToast.showText( - contentColor: Colors.lightBlue, - text: translate('synced_peer_readded_tip')); + Future.delayed(Duration.zero, () async { + final succ = await future; + if (succ) { + await Future.delayed( + Duration(seconds: 2)); // success msg + BotToast.showText( + contentColor: Colors.lightBlue, + text: translate('synced_peer_readded_tip')); + } }); } } @@ -357,7 +362,7 @@ class _PeerTabPageState extends State break; } gFFI.peerTabModel.setMultiSelectionMode(false); - showToast(translate('Successful')); + if (model.currentTab != 3) showToast(translate('Successful')); } deletePeerConfirmDialog(onSubmit, translate('Delete')); @@ -404,7 +409,6 @@ class _PeerTabPageState extends State gFFI.abModel.addPeers(peers); gFFI.abModel.pushAb(); model.setMultiSelectionMode(false); - showToast(translate('Successful')); }, child: Tooltip( message: translate('Add to Address Book'), diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index cb4242a6a..9426b0571 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -30,6 +30,7 @@ class AbModel { final tags = [].obs; final peers = List.empty(growable: true).obs; final sortTags = shouldSortTags().obs; + final retrying = false.obs; bool get emtpy => peers.isEmpty && tags.isEmpty; final selectedTags = List.empty(growable: true).obs; @@ -55,10 +56,11 @@ class AbModel { if (gFFI.userModel.userName.isEmpty) return; if (abLoading.value) return; if (!force && initialized) return; + DateTime startTime = DateTime.now(); if (pushError.isNotEmpty) { try { // push to retry - pushAb(toast: false); + pushAb(toastIfFail: false, toastIfSucc: false); } catch (_) {} } if (!quiet) { @@ -112,14 +114,14 @@ class AbModel { } } } finally { - if (initialized) { - // make loading effect obvious - Future.delayed(Duration(milliseconds: 300), () { - abLoading.value = false; - }); - } else { + var ms = + (Duration(milliseconds: 300) - DateTime.now().difference(startTime)) + .inMilliseconds; + ms = ms > 0 ? ms : 0; + Future.delayed(Duration(milliseconds: ms), () { abLoading.value = false; - } + }); + initialized = true; _syncAllFromRecent = true; _timerCounter = 0; @@ -198,9 +200,16 @@ class AbModel { it.first.alias = alias; } - Future pushAb({bool toast = true}) async { - debugPrint("pushAb"); + Future pushAb( + {bool toastIfFail = true, + bool toastIfSucc = true, + bool isRetry = false}) async { + debugPrint( + "pushAb: toastIfFail:$toastIfFail, toastIfSucc:$toastIfSucc, isRetry:$isRetry"); pushError.value = ''; + if (isRetry) retrying.value = true; + DateTime startTime = DateTime.now(); + bool ret = false; try { // avoid double pushes in a row _syncAllFromRecent = true; @@ -226,12 +235,14 @@ class AbModel { } if (resp.statusCode == 200 && (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) { + ret = true; _saveCache(); } else { Map json = _jsonDecode(resp.body, resp.statusCode); if (json.containsKey('error')) { throw json['error']; } else if (resp.statusCode == 200) { + ret = true; _saveCache(); } else { throw 'HTTP ${resp.statusCode}'; @@ -240,12 +251,25 @@ class AbModel { } catch (e) { pushError.value = '${translate('push_ab_failed_tip')}: ${translate(e.toString())}'; - if (toast && gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) { - BotToast.showText(contentColor: Colors.red, text: pushError.value); - } - } finally { - _syncAllFromRecent = true; } + _syncAllFromRecent = true; + if (isRetry) { + var ms = + (Duration(milliseconds: 200) - DateTime.now().difference(startTime)) + .inMilliseconds; + ms = ms > 0 ? ms : 0; + Future.delayed(Duration(milliseconds: ms), () { + retrying.value = false; + }); + } + + if (!ret && toastIfFail) { + BotToast.showText(contentColor: Colors.red, text: pushError.value); + } + if (ret && toastIfSucc) { + showToast(translate('Successful')); + } + return ret; } Peer? find(String id) { @@ -410,7 +434,7 @@ class AbModel { } // Be careful with loop calls if (syncChanged && push) { - pushAb(); + pushAb(toastIfSucc: false); } else if (uiChanged) { peers.refresh(); }