diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6bed2c2e0..794815b86 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1078,25 +1078,32 @@ Color str2color(String str, [alpha = 0xFF]) { } Color str2color2(String str, [alpha = 0xFF]) { - List colorList = [ - Colors.red, - Colors.green, - Colors.blue, - Colors.orange, - Colors.yellow, - Colors.purple, - Colors.grey, - Colors.cyan, - Colors.lime, - Colors.teal, - Colors.pink, - Colors.indigo, - Colors.brown, - ]; + Map colorMap = { + "red": Colors.red, + "green": Colors.green, + "blue": Colors.blue, + "orange": Colors.orange, + "purple": Colors.purple, + "grey": Colors.grey, + "cyan": Colors.cyan, + "lime": Colors.lime, + "teal": Colors.teal, + "pink": Colors.pink[200]!, + "indigo": Colors.indigo, + "brown": Colors.brown, + }; + final color = colorMap[str.toLowerCase()]; + if (color != null) { + return color.withAlpha(alpha); + } + if (str.toLowerCase() == 'yellow') { + return Colors.yellow.withAlpha(alpha); + } var hash = 0; for (var i = 0; i < str.length; i++) { hash += str.codeUnitAt(i); } + List colorList = colorMap.values.toList(); hash = hash % colorList.length; return colorList[hash].withAlpha(alpha); } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index bf86de0ff..9530e851d 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -45,12 +45,17 @@ class _AddressBookState extends State { child: CircularProgressIndicator(), ); } - if (gFFI.abModel.abError.isNotEmpty) { - return _buildShowError(gFFI.abModel.abError.value); - } return Column( children: [ - _buildLoadingHavingPeers(), + _buildNotEmptyLoading(), + _buildErrorBanner( + err: gFFI.abModel.pullError, + retry: null, + close: () => gFFI.abModel.pullError.value = ''), + _buildErrorBanner( + err: gFFI.abModel.pushError, + retry: () => gFFI.abModel.pushAb(), + close: () => gFFI.abModel.pushError.value = ''), Expanded( child: isDesktop ? _buildAddressBookDesktop() @@ -60,22 +65,62 @@ class _AddressBookState extends State { } }); - Widget _buildShowError(String error) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate(error)), - TextButton( - onPressed: () { - gFFI.abModel.pullAb(); - }, - child: Text(translate("Retry"))) - ], - )); + 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: Color.fromARGB(255, 253, 238, 235), + 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 _buildLoadingHavingPeers() { + Widget _buildNotEmptyLoading() { double size = 15; return Obx(() => Offstage( offstage: !(gFFI.abModel.abLoading.value && !gFFI.abModel.emtpy), @@ -301,6 +346,7 @@ class _AddressBookState extends State { close(); } + double marginBottom = 4; return CustomAlertDialog( title: Text(translate("Add ID")), content: Column( @@ -322,7 +368,7 @@ class _AddressBookState extends State { ), ], ), - ), + ).marginOnly(bottom: marginBottom), TextField( controller: idController, inputFormatters: [IDTextInputFormatter()], @@ -334,7 +380,7 @@ class _AddressBookState extends State { translate('Alias'), style: style, ), - ).marginOnly(top: 8, bottom: 2), + ).marginOnly(top: 8, bottom: marginBottom), TextField( controller: aliasController, ), @@ -344,8 +390,9 @@ class _AddressBookState extends State { translate('Tags'), style: style, ), - ).marginOnly(top: 8), - Container( + ).marginOnly(top: 8, bottom: marginBottom), + Align( + alignment: Alignment.centerLeft, child: Wrap( children: tags .map((e) => AddressBookTag( diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index b046cd504..88d44cc7e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -159,7 +159,6 @@ class _PeerCardState extends State<_PeerCard> final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); - final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); final child = Obx( () => Container( foregroundDecoration: deco.value, @@ -196,7 +195,9 @@ class _PeerCardState extends State<_PeerCard> getOnline(8, peer.online), Expanded( child: Text( - alias.isEmpty ? formatID(peer.id) : alias, + peer.alias.isEmpty + ? formatID(peer.id) + : peer.alias, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, )), @@ -229,13 +230,14 @@ class _PeerCardState extends State<_PeerCard> : '', child: Stack(children: [ child, - Positioned( - top: 2, - right: 10, - child: CustomPaint( - painter: TagPainter(radius: 3, colors: colors), - ), - ) + if (colors.isNotEmpty) + Positioned( + top: 2, + right: 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + ) ]), ); } @@ -329,13 +331,14 @@ class _PeerCardState extends State<_PeerCard> : '', child: Stack(children: [ child, - Positioned( - top: 4, - right: 12, - child: CustomPaint( - painter: TagPainter(radius: 4, colors: colors), - ), - ) + if (colors.isNotEmpty) + Positioned( + top: 4, + right: 12, + child: CustomPaint( + painter: TagPainter(radius: 4, colors: colors), + ), + ) ]), ); } @@ -649,8 +652,13 @@ abstract class BasePeerCard extends StatelessWidget { oldName: oldName, onSubmit: (String newName) async { if (newName != oldName) { - await bind.mainSetPeerAlias(id: id, alias: newName); - _update(); + if (tab == PeerTabIndex.ab) { + gFFI.abModel.changeAlias(id: id, alias: newName); + gFFI.abModel.pushAb(); + } else { + await bind.mainSetPeerAlias(id: id, alias: newName); + _update(); + } } }); }, @@ -1048,6 +1056,11 @@ class AddressBookPeerCard extends BasePeerCard { dismissOnClicked: true, ); } + + @protected + @override + Future _getAlias(String id) async => + gFFI.abModel.find(id)?.alias ?? ''; } class MyGroupPeerCard extends BasePeerCard { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index ef3862aac..dbf563de2 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -125,6 +125,7 @@ void runMainApp(bool startService) async { bind.pluginSyncUi(syncTo: kAppTypeMain); bind.pluginListReload(); } + gFFI.abModel.loadCache(); gFFI.userModel.refreshCurrentUser(); runApp(App()); // Set window option. @@ -152,6 +153,7 @@ void runMobileApp() async { await initEnv(kAppTypeMain); if (isAndroid) androidChannelInit(); platformFFI.syncAndroidServiceAppDirConfigPath(); + gFFI.abModel.loadCache(); gFFI.userModel.refreshCurrentUser(); runApp(App()); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 9a7a42d31..7fdf940d3 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -24,7 +25,8 @@ bool shouldSortTags() { class AbModel { final abLoading = false.obs; - final abError = "".obs; + final pullError = "".obs; + final pushError = "".obs; final tags = [].obs; final peers = List.empty(growable: true).obs; final sortTags = shouldSortTags().obs; @@ -33,8 +35,10 @@ class AbModel { final selectedTags = List.empty(growable: true).obs; var initialized = false; var licensedDevices = 0; - var sync_all_from_recent = true; + var _syncAllFromRecent = true; + var _syncFromRecentLock = false; var _timerCounter = 0; + var _cacheLoadOnceFlag = false; WeakReference parent; @@ -51,12 +55,15 @@ class AbModel { if (gFFI.userModel.userName.isEmpty) return; if (abLoading.value) return; if (!force && initialized) return; - if (!initialized) { - await _loadCache(); + if (pushError.isNotEmpty) { + try { + // push to retry + pushAb(toast: false); + } catch (_) {} } if (!quiet) { abLoading.value = true; - abError.value = ""; + pullError.value = ""; } final api = "${await bind.mainGetApiServer()}/api/ab"; int? statusCode; @@ -66,19 +73,22 @@ class AbModel { authHeaders['Accept-Encoding'] = "gzip"; final resp = await http.get(Uri.parse(api), headers: authHeaders); statusCode = resp.statusCode; - if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + if (resp.body.toLowerCase() == "null") { + // normal reply, emtpy ab return null + tags.clear(); + peers.clear(); + } else if (resp.body.isNotEmpty) { Map json; try { json = jsonDecode(utf8.decode(resp.bodyBytes)); } catch (e) { if (resp.statusCode != 200) { - BotToast.showText( - contentColor: Colors.red, text: 'HTTP ${resp.statusCode}'); + throw 'HTTP ${resp.statusCode}, $e'; } rethrow; } if (json.containsKey('error')) { - abError.value = json['error']; + throw json['error']; } else if (json.containsKey('data')) { try { gFFI.abModel.licensedDevices = json['licensed_devices']; @@ -101,7 +111,13 @@ class AbModel { } } } catch (err) { - abError.value = err.toString(); + if (!quiet) { + pullError.value = + '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; + if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) { + BotToast.showText(contentColor: Colors.red, text: pullError.value); + } + } } finally { if (initialized) { // make loading effect obvious @@ -112,26 +128,16 @@ class AbModel { abLoading.value = false; } initialized = true; - sync_all_from_recent = true; + _syncAllFromRecent = true; _timerCounter = 0; - if (abError.isNotEmpty) { + if (pullError.isNotEmpty) { if (statusCode == 401) { gFFI.userModel.reset(clearAbCache: true); - } else { - reset(clearCache: false); } } } } - Future reset({bool clearCache = false}) async { - await bind.mainSetLocalOption(key: "selected-tags", value: ''); - tags.clear(); - peers.clear(); - initialized = false; - if (clearCache) await bind.mainClearAb(); - } - void addId(String id, String alias, List tags) { if (idContainBy(id)) { return; @@ -154,8 +160,12 @@ class AbModel { } void addPeer(Peer peer) { - peers.removeWhere((e) => e.id == peer.id); - peers.add(peer); + final index = peers.indexWhere((e) => e.id == peer.id); + if (index >= 0) { + peers[index] = merge(peer, peers[index]); + } else { + peers.add(peer); + } } void addPeers(List ps) { @@ -187,33 +197,66 @@ class AbModel { }).toList(); } - Future pushAb() async { - debugPrint("pushAb"); - final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = getHttpHeaders(); - authHeaders['Content-Type'] = "application/json"; - final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); - final body = jsonEncode({ - "data": jsonEncode({"tags": tags, "peers": peersJsonData}) - }); - var request = http.Request('POST', Uri.parse(api)); - // support compression - if (licensedDevices > 0 && body.length > 1024) { - authHeaders['Content-Encoding'] = "gzip"; - request.bodyBytes = GZipCodec().encode(utf8.encode(body)); - } else { - request.body = body; + void changeAlias({required String id, required String alias}) { + final it = peers.where((element) => element.id == id); + if (it.isEmpty) { + return; } - request.headers.addAll(authHeaders); + it.first.alias = alias; + } + + Future pushAb({bool toast = true}) async { + debugPrint("pushAb"); + pushError.value = ''; try { - await http.Client().send(request); - // await pullAb(quiet: true); - _saveCache(); // save on success + // avoid double pushes in a row + _syncAllFromRecent = true; + syncFromRecent(push: false); + final api = "${await bind.mainGetApiServer()}/api/ab"; + var authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; + final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); + final body = jsonEncode({ + "data": jsonEncode({"tags": tags, "peers": peersJsonData}) + }); + http.Response resp; + // support compression + if (licensedDevices > 0 && body.length > 1024) { + authHeaders['Content-Encoding'] = "gzip"; + resp = await http.post(Uri.parse(api), + headers: authHeaders, body: GZipCodec().encode(utf8.encode(body))); + } else { + resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); + } + try { + if (resp.statusCode == 200 && + (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) { + _saveCache(); + } else { + final json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else if (resp.statusCode == 200) { + _saveCache(); + } else { + throw 'HTTP ${resp.statusCode}'; + } + } + } catch (e) { + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}, $e'; + } + rethrow; + } } catch (e) { - BotToast.showText(contentColor: Colors.red, text: e.toString()); + 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 { - sync_all_from_recent = true; - _timerCounter = 0; + _syncAllFromRecent = true; } } @@ -290,34 +333,43 @@ class AbModel { } } - void syncFromRecent() async { - Peer merge(Peer r, Peer p) { - return Peer( - id: p.id, - hash: r.hash.isEmpty ? p.hash : r.hash, - username: r.username.isEmpty ? p.username : r.username, - hostname: r.hostname.isEmpty ? p.hostname : r.hostname, - platform: r.platform.isEmpty ? p.platform : r.platform, - alias: r.alias, - tags: p.tags, - forceAlwaysRelay: r.forceAlwaysRelay, - rdpPort: r.rdpPort, - rdpUsername: r.rdpUsername); - } + Peer merge(Peer r, Peer p) { + return Peer( + id: p.id, + hash: r.hash.isEmpty ? p.hash : r.hash, + username: r.username.isEmpty ? p.username : r.username, + hostname: r.hostname.isEmpty ? p.hostname : r.hostname, + platform: r.platform.isEmpty ? p.platform : r.platform, + alias: p.alias.isEmpty ? r.alias : p.alias, + tags: p.tags, + forceAlwaysRelay: r.forceAlwaysRelay, + rdpPort: r.rdpPort, + rdpUsername: r.rdpUsername); + } + void syncFromRecent({bool push = true}) async { + if (!_syncFromRecentLock) { + _syncFromRecentLock = true; + _syncFromRecentWithoutLock(push: push); + _syncFromRecentLock = false; + } + } + + void _syncFromRecentWithoutLock({bool push = true}) async { bool shouldSync(Peer a, Peer b) { return a.hash != b.hash || a.username != b.username || a.platform != b.platform || - a.hostname != b.hostname; + a.hostname != b.hostname || + a.alias != b.alias; } Future> getRecentPeers() async { try { if (peers.isEmpty) []; List filteredPeerIDs; - if (sync_all_from_recent) { - sync_all_from_recent = false; + if (_syncAllFromRecent) { + _syncAllFromRecent = false; filteredPeerIDs = peers.map((e) => e.id).toList(); } else { final new_stored_str = await bind.mainGetNewStoredPeers(); @@ -372,7 +424,7 @@ class AbModel { } } // Be careful with loop calls - if (changed) { + if (changed && push) { pushAb(); } } catch (e) { @@ -394,8 +446,10 @@ class AbModel { } } - _loadCache() async { + loadCache() async { try { + if (_cacheLoadOnceFlag || abLoading.value) return; + _cacheLoadOnceFlag = true; final access_token = bind.mainGetLocalOption(key: 'access_token'); if (access_token.isEmpty) return; final cache = await bind.mainLoadAb(); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 95a4cad0c..35c0d020d 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -556,8 +556,10 @@ class FileController { mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.warning_rounded, color: Colors.red), - Text(title).paddingOnly( - left: 10, + Expanded( + child: Text(title).paddingOnly( + left: 10, + ), ), ], ), diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 7a61c3dcf..98810b09e 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -56,6 +56,7 @@ class Peer { "username": username, "hostname": hostname, "platform": platform, + "alias": alias, "tags": tags, }; } diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 896d7c564..2d8e5c61a 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -87,7 +87,7 @@ class UserModel { Future reset({bool clearAbCache = false}) async { await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: ''); - await gFFI.abModel.reset(clearCache: clearAbCache); + if (clearAbCache) await bind.mainClearAb(); await gFFI.groupModel.reset(); userName.value = ''; } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 16c02d758..b0ec5602f 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1499,6 +1499,12 @@ pub struct AbPeer { skip_serializing_if = "String::is_empty" )] pub platform: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub alias: String, #[serde(default, deserialize_with = "deserialize_vec_string")] pub tags: Vec, } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 364ed885f..ef0cd3b86 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 6b03397fa..88638e0d6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", "未成功获取地址簿"), + ("push_ab_failed_tip", "未成功上传地址簿"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 23d83578e..69df1b70f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index a05ad2001..cf1ad5748 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index cc1b321e0..43640bd6a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", "Listenansicht"), ("Select", "Auswählen"), ("Toggle Tags", "Tags umschalten"), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index a0d1fa85c..2795a0e90 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 2a9b40568..695ff923d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -74,5 +74,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clipboard_wait_response_timeout_tip", "Timed out waiting for copy response."), ("logout_tip", "Are you sure you want to log out?"), ("exceed_max_devices", "You have reached the maximum number of managed devices."), + ("pull_ab_failed_tip", "Failed to refresh address book"), + ("push_ab_failed_tip", "Failed to sync address book to server"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 451b93a55..3ac8ea79b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 475a4d72d..c10f9de16 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", "Ver en Lista"), ("Select", "Seleccionar"), ("Toggle Tags", "Alternar Etiquetas"), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b507231da..389676bac 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c995c8d41..b1aa90f76 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1e516e947..4e5d34c08 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index aa61d5420..76fad9ad2 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 185fb3e43..80ef1f389 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", "Vista elenco"), ("Select", "Seleziona"), ("Toggle Tags", "Attiva/disattiva tag"), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a247aff07..9e2bc66df 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a1371ae90..18ee01abd 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b1506ac1b..252880ebb 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index c47aac327..6ad41ee7a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index a2cacb59a..ef04ed01a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d95615ed8..dc45c4ea9 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 00956b972..a3fc256ad 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 2a82f6208..201ae1138 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 3d97a7ecd..54bc2db4c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f3e4db6d1..877a7936a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", "Список"), ("Select", "Выбор"), ("Toggle Tags", "Переключить метки"), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8b2e2eaa0..2d8db2678 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2913ad3fd..e16bcfc18 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ebe9e6275..ea7a7da5c 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 999bca477..388618bfa 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index cd32b975f..a61f4ded9 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index c3b2cb76a..4605779c3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c1191aed4..4b7262021 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 45f44a5f2..df39aa99b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d07d19f71..608a01b4e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", "未成功獲取地址簿"), + ("push_ab_failed_tip", "未成功上傳地址簿"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 6b1fbc437..edd98640c 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 657fbcf0d..140585e31 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -535,5 +535,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List View", ""), ("Select", ""), ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), ].iter().cloned().collect(); }