diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 1a09d6f53..054a1666c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -54,9 +54,9 @@ class _AddressBookState extends State { const LinearProgressIndicator(), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, - err: gFFI.abModel.currentAbPullError, + err: gFFI.abModel.abPullError, retry: null, - close: () => gFFI.abModel.currentAbPullError.value = ''), + close: gFFI.abModel.clearPullErrors), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, err: gFFI.abModel.currentAbPushError, diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 81c4dc851..001887c0c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; @@ -53,7 +52,9 @@ class AbModel { RxBool get currentAbLoading => current.abLoading; bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty; - RxString get currentAbPullError => current.pullError; + final _listPullError = ''.obs; + RxString get abPullError => + _listPullError.value.isNotEmpty ? _listPullError : current.pullError; RxString get currentAbPushError => current.pushError; String? _personalAbGuid; RxBool legacyMode = false.obs; @@ -68,6 +69,7 @@ class AbModel { var _syncFromRecentLock = false; var _timerCounter = 0; var _cacheLoadOnceFlag = false; + var _pulledOnce = false; var listInitialized = false; var _maxPeerOneAb = 0; @@ -97,10 +99,17 @@ class AbModel { print("reset ab model"); addressbooks.clear(); _currentName.value = ''; + _listPullError.value = ''; + _pulledOnce = false; await bind.mainClearAb(); listInitialized = false; } + void clearPullErrors() { + _listPullError.value = ''; + current.pullError.value = ''; + } + // #region ab /// Pulls the address book data from the server. /// @@ -110,31 +119,41 @@ class AbModel { var _pulling = false; Future pullAb( {required ForcePullAb? force, required bool quiet}) async { + if (bind.isDisableAb()) return; + if (!gFFI.userModel.isLogin) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (_pulling) return; + if (force == null && _pulledOnce) { + return; + } _pulling = true; + if (!quiet) { + _listPullError.value = ''; + current.pullError.value = ''; + } try { await _pullAb(force: force, quiet: quiet); _refreshTab(); } catch (_) {} _pulling = false; + _pulledOnce = true; } Future _pullAb( {required ForcePullAb? force, required bool quiet}) async { - if (bind.isDisableAb()) return; - if (!gFFI.userModel.isLogin) return; - if (gFFI.userModel.networkError.isNotEmpty) return; if (force == null && listInitialized && current.initialized) return; debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { try { // Read personal guid every time to avoid upgrading the server without closing the main window _personalAbGuid = null; - await _getPersonalAbGuid(); - // Determine legacy mode based on whether _personalAbGuid is null + // `true`: continue init. `false`: stop, error already recorded. + if (!await _getPersonalAbGuid(quiet: quiet)) { + return; + } legacyMode.value = _personalAbGuid == null; if (!legacyMode.value && _maxPeerOneAb == 0) { - await _getAbSettings(); + await _getAbSettings(quiet: quiet); } if (_personalAbGuid != null) { debugPrint("pull ab list"); @@ -142,7 +161,7 @@ class AbModel { abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName, gFFI.userModel.userName.value, null, ShareRule.read.value, null)); // get all address book name - await _getSharedAbProfiles(abProfiles); + await _getSharedAbProfiles(abProfiles, quiet: quiet); addressbooks.removeWhere((key, value) => abProfiles.firstWhereOrNull((e) => e.name == key) == null); for (int i = 0; i < abProfiles.length; i++) { @@ -182,6 +201,7 @@ class AbModel { } } catch (e) { debugPrint("pull ab list error: $e"); + _setListPullError(e, quiet: quiet); } } else if (listInitialized && (!current.initialized || force == ForcePullAb.current)) { @@ -197,14 +217,26 @@ class AbModel { } } - Future _getAbSettings() async { + void _setListPullError(Object err, {required bool quiet, int? statusCode}) { + if (!quiet) { + _listPullError.value = + '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; + } + if (statusCode == 401) { + gFFI.userModel.reset(resetOther: true); + } + } + + Future _getAbSettings({required bool quiet}) async { + int? statusCode; try { final api = "${await bind.mainGetApiServer()}/api/ab/settings"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - if (resp.statusCode == 404) { + statusCode = resp.statusCode; + if (statusCode == 404) { debugPrint("HTTP 404, api server doesn't support shared address book"); return false; } @@ -213,46 +245,57 @@ class AbModel { if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } _maxPeerOneAb = json['max_peer_one_ab'] ?? 0; return true; } catch (err) { debugPrint('get ab settings err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } - Future _getPersonalAbGuid() async { + /// Loads `/api/ab/personal`. + /// Returns `true` to continue init, `false` to stop after a real error. + Future _getPersonalAbGuid({required bool quiet}) async { + int? statusCode; try { final api = "${await bind.mainGetApiServer()}/api/ab/personal"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - if (resp.statusCode == 404) { + statusCode = resp.statusCode; + if (statusCode == 404) { debugPrint("HTTP 404, current api server is legacy mode"); - return false; + // Old server: keep `_personalAbGuid` null and continue in legacy mode. + return true; } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } _personalAbGuid = json['guid']; + // New server: guid is available, continue in non-legacy mode. return true; } catch (err) { debugPrint('get personal ab err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } + // Real error: stop the current pull. return false; } - Future _getSharedAbProfiles(List profiles) async { + Future _getSharedAbProfiles(List profiles, + {required bool quiet}) async { final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles"; + int? statusCode; try { var uri0 = Uri.parse(api); final pageSize = 100; @@ -273,13 +316,19 @@ class AbModel { headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); + statusCode = resp.statusCode; + if (statusCode == 404) { + debugPrint( + "HTTP 404, api server doesn't support shared address book"); + return false; + } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } if (json.containsKey('total')) { if (total == 0) total = json['total']; @@ -302,6 +351,7 @@ class AbModel { return true; } catch (err) { debugPrint('_getSharedAbProfiles err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index c6ba992d2..d55cff453 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -343,6 +343,7 @@ class GroupModel { } reset() async { + initialized = false; groupLoadError.value = ''; deviceGroups.clear(); users.clear();