Cw 514 add sort functionality for addressbook mywallets and contacts (#1309)
* add sort function to contact list * fix UI * prevent duplicate contact names * dispose contact source subscription * fix custom order issue * update the address book UI * fix saving custom order * fix merge conflict issue * review fixes [skip ci] * revert to single scroll for entire page * tabBarView address book --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Serhii committed
Nov 7, 2024 at 03:26 UTC
109d9b458eac30534dd93492173145a3fc48b948
38 files changed
+575
-223
lib/entities/contact.dart
+6
-3
@@ -7,7 +7,8 @@ part 'contact.g.dart';
7
8
@HiveType(typeId: Contact.typeId)
9
class Contact extends HiveObject with Keyable {
10
- Contact({required this.name, required this.address, CryptoCurrency? type}) {
10
+ Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange})
11
+ : lastChange = lastChange ?? DateTime.now() {
12
if (type != null) {
13
raw = type.raw;
14
}
@@ -25,6 +26,9 @@ class Contact extends HiveObject with Keyable {
26
@HiveField(2, defaultValue: 0)
27
late int raw;
28
29
+ @HiveField(3)
30
+ DateTime lastChange;
31
+
32
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
33
34
@override
@@ -36,6 +40,5 @@ class Contact extends HiveObject with Keyable {
40
@override
41
int get hashCode => key.hashCode;
42
39
- void updateCryptoCurrency({required CryptoCurrency currency}) =>
40
- raw = currency.raw;
43
+ void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw;
44
}
lib/entities/contact_record.dart
+11
-12
@@ -1,22 +1,21 @@
1
-import 'package:hive/hive.dart';
2
-import 'package:mobx/mobx.dart';
1
import 'package:cake_wallet/entities/contact.dart';
4
-import 'package:cw_core/crypto_currency.dart';
5
-import 'package:cake_wallet/entities/record.dart';
2
import 'package:cake_wallet/entities/contact_base.dart';
3
+import 'package:cake_wallet/entities/record.dart';
4
+import 'package:cw_core/crypto_currency.dart';
5
+import 'package:hive/hive.dart';
6
+import 'package:mobx/mobx.dart';
7
8
part 'contact_record.g.dart';
9
10
class ContactRecord = ContactRecordBase with _$ContactRecord;
11
12
-abstract class ContactRecordBase extends Record<Contact>
13
- with Store
14
- implements ContactBase {
12
+abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
13
ContactRecordBase(Box<Contact> source, Contact original)
14
: name = original.name,
15
address = original.address,
16
type = original.type,
19
- super(source, original);
17
+ lastChange = original.lastChange,
18
+ super(source, original);
19
20
@override
21
@observable
@@ -30,14 +29,14 @@ abstract class ContactRecordBase extends Record<Contact>
29
@observable
30
CryptoCurrency type;
31
32
+ DateTime? lastChange;
33
+
34
@override
35
void toBind(Contact original) {
36
reaction((_) => name, (String name) => original.name = name);
37
reaction((_) => address, (String address) => original.address = address);
37
- reaction(
38
- (_) => type,
39
- (CryptoCurrency currency) =>
40
- original.updateCryptoCurrency(currency: currency));
38
+ reaction((_) => type,
39
+ (CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency));
40
}
41
42
@override
lib/entities/preferences_key.dart
+2
@@ -25,7 +25,9 @@ class PreferencesKey {
25
static const disableBulletinKey = 'disable_bulletin';
26
static const defaultBuyProvider = 'default_buy_provider';
27
static const walletListOrder = 'wallet_list_order';
28
+ static const contactListOrder = 'contact_list_order';
29
static const walletListAscending = 'wallet_list_ascending';
30
+ static const contactListAscending = 'contact_list_ascending';
31
static const currentFiatApiModeKey = 'current_fiat_api_mode';
32
static const failedTotpTokenTrials = 'failed_token_trials';
33
static const disableExchangeKey = 'disable_exchange';
lib/entities/wallet_list_order_types.dart
+5
-5
@@ -1,6 +1,6 @@
1
import 'package:cake_wallet/generated/i18n.dart';
2
3
-enum WalletListOrderType {
3
+enum FilterListOrderType {
4
CreationDate,
5
Alphabetical,
6
GroupByType,
@@ -9,13 +9,13 @@ enum WalletListOrderType {
9
@override
10
String toString() {
11
switch (this) {
12
- case WalletListOrderType.CreationDate:
12
+ case FilterListOrderType.CreationDate:
13
return S.current.creation_date;
14
- case WalletListOrderType.Alphabetical:
14
+ case FilterListOrderType.Alphabetical:
15
return S.current.alphabetical;
16
- case WalletListOrderType.GroupByType:
16
+ case FilterListOrderType.GroupByType:
17
return S.current.group_by_type;
18
- case WalletListOrderType.Custom:
18
+ case FilterListOrderType.Custom:
19
return S.current.custom_drag;
20
}
21
}
lib/src/screens/contact/contact_list_page.dart
+297
-65
@@ -1,21 +1,24 @@
1
import 'package:cake_wallet/core/auth_service.dart';
2
import 'package:cake_wallet/entities/contact_base.dart';
3
import 'package:cake_wallet/entities/contact_record.dart';
4
+import 'package:cake_wallet/entities/wallet_list_order_types.dart';
5
+import 'package:cake_wallet/generated/i18n.dart';
6
+import 'package:cake_wallet/routes.dart';
7
+import 'package:cake_wallet/src/screens/base_page.dart';
8
+import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
9
+import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
10
+import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
11
+import 'package:cake_wallet/src/widgets/standard_list.dart';
12
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
13
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
14
import 'package:cake_wallet/utils/show_bar.dart';
15
import 'package:cake_wallet/utils/show_pop_up.dart';
8
-import 'package:flutter/material.dart';
16
+import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
17
import 'package:flutter/cupertino.dart';
18
+import 'package:flutter/material.dart';
19
import 'package:flutter/services.dart';
11
-import 'package:flutter_mobx/flutter_mobx.dart';
20
+import 'package:flutter/widgets.dart';
21
import 'package:flutter_slidable/flutter_slidable.dart';
13
-import 'package:cake_wallet/routes.dart';
14
-import 'package:cake_wallet/generated/i18n.dart';
15
-import 'package:cake_wallet/src/screens/base_page.dart';
16
-import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
17
-import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
18
-import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart';
22
23
class ContactListPage extends BasePage {
24
ContactListPage(this.contactListViewModel, this.authService);
@@ -74,45 +77,101 @@ class ContactListPage extends BasePage {
77
}
78
79
@override
77
- Widget body(BuildContext context) {
78
- return Container(
79
- padding: EdgeInsets.all(20.0),
80
- child: Observer(builder: (_) {
81
- final contacts = contactListViewModel.contactsToShow;
82
- final walletContacts = contactListViewModel.walletContactsToShow;
83
- return CollapsibleSectionList(
84
- sectionCount: 2,
85
- sectionTitleBuilder: (int sectionIndex) {
86
- var title = S.current.contact_list_contacts;
87
-
88
- if (sectionIndex == 0) {
89
- title = S.current.contact_list_wallets;
90
- }
80
+ Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel);
81
+}
82
92
- return Container(
93
- padding: EdgeInsets.only(bottom: 10),
94
- child: Text(title, style: TextStyle(fontSize: 36)));
95
- },
96
- itemCounter: (int sectionIndex) =>
97
- sectionIndex == 0 ? walletContacts.length : contacts.length,
98
- itemBuilder: (int sectionIndex, index) {
99
- if (sectionIndex == 0) {
100
- final walletInfo = walletContacts[index];
101
- return generateRaw(context, walletInfo);
102
- }
83
+class ContactPageBody extends StatefulWidget {
84
+ const ContactPageBody({required this.contactListViewModel});
85
104
- final contact = contacts[index];
105
- final content = generateRaw(context, contact);
106
- return contactListViewModel.isEditable
107
- ? Slidable(
108
- key: Key('${contact.key}'),
109
- endActionPane: _actionPane(context, contact),
110
- child: content,
111
- )
112
- : content;
113
- },
114
- );
115
- }));
86
+ final ContactListViewModel contactListViewModel;
87
+
88
+ @override
89
+ State<ContactPageBody> createState() => _ContactPageBodyState();
90
+}
91
+
92
+class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProviderStateMixin {
93
+ late TabController _tabController;
94
+
95
+ @override
96
+ void initState() {
97
+ super.initState();
98
+ _tabController = TabController(length: 2, vsync: this);
99
+ }
100
+
101
+ @override
102
+ void dispose() {
103
+ _tabController.dispose();
104
+ super.dispose();
105
+ }
106
+
107
+ @override
108
+ Widget build(BuildContext context) {
109
+ return Padding(
110
+ padding: const EdgeInsets.only(left: 24),
111
+ child: Column(
112
+ children: [
113
+ Align(
114
+ alignment: Alignment.centerLeft,
115
+ child: TabBar(
116
+ controller: _tabController,
117
+ splashFactory: NoSplash.splashFactory,
118
+ indicatorSize: TabBarIndicatorSize.label,
119
+ isScrollable: true,
120
+ labelStyle: TextStyle(
121
+ fontSize: 18,
122
+ fontFamily: 'Lato',
123
+ fontWeight: FontWeight.w600,
124
+ color: Theme.of(context).appBarTheme.titleTextStyle!.color,
125
+ ),
126
+ unselectedLabelStyle: TextStyle(
127
+ fontSize: 18,
128
+ fontFamily: 'Lato',
129
+ fontWeight: FontWeight.w600,
130
+ color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)),
131
+ labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
132
+ indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
133
+ indicatorPadding: EdgeInsets.zero,
134
+ labelPadding: EdgeInsets.only(right: 24),
135
+ tabAlignment: TabAlignment.center,
136
+ dividerColor: Colors.transparent,
137
+ padding: EdgeInsets.zero,
138
+ tabs: [
139
+ Tab(text: S.of(context).wallets),
140
+ Tab(text: S.of(context).contact_list_contacts),
141
+ ],
142
+ ),
143
+ ),
144
+ Expanded(
145
+ child: TabBarView(
146
+ controller: _tabController,
147
+ children: [
148
+ _buildWalletContacts(context),
149
+ ContactListBody(
150
+ contactListViewModel: widget.contactListViewModel,
151
+ tabController: _tabController),
152
+ ],
153
+ ),
154
+ ),
155
+ ],
156
+ ),
157
+ );
158
+ }
159
+
160
+ Widget _buildWalletContacts(BuildContext context) {
161
+ final walletContacts = widget.contactListViewModel.walletContactsToShow;
162
+
163
+ return ListView.builder(
164
+ shrinkWrap: true,
165
+ itemCount: walletContacts.length * 2,
166
+ itemBuilder: (context, index) {
167
+ if (index.isOdd) {
168
+ return StandardListSeparator();
169
+ } else {
170
+ final walletInfo = walletContacts[index ~/ 2];
171
+ return generateRaw(context, walletInfo);
172
+ }
173
+ },
174
+ );
175
}
176
177
Widget generateRaw(BuildContext context, ContactBase contact) {
@@ -123,7 +182,7 @@ class ContactListPage extends BasePage {
182
183
return GestureDetector(
184
onTap: () async {
126
- if (!contactListViewModel.isEditable) {
185
+ if (!widget.contactListViewModel.isEditable) {
186
Navigator.of(context).pop(contact);
187
return;
188
}
@@ -143,8 +202,7 @@ class ContactListPage extends BasePage {
202
mainAxisAlignment: MainAxisAlignment.start,
203
children: <Widget>[
204
currencyIcon,
146
- Expanded(
147
- child: Padding(
205
+ Padding(
206
padding: EdgeInsets.only(left: 12),
207
child: Text(
208
contact.name,
@@ -154,28 +212,13 @@ class ContactListPage extends BasePage {
212
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
213
),
214
),
157
- ))
215
+ ),
216
],
217
),
218
),
219
);
220
}
221
164
- Future<bool> showAlertDialog(BuildContext context) async {
165
- return await showPopUp<bool>(
166
- context: context,
167
- builder: (BuildContext context) {
168
- return AlertWithTwoActions(
169
- alertTitle: S.of(context).address_remove_contact,
170
- alertContent: S.of(context).address_remove_content,
171
- rightButtonText: S.of(context).remove,
172
- leftButtonText: S.of(context).cancel,
173
- actionRightButton: () => Navigator.of(context).pop(true),
174
- actionLeftButton: () => Navigator.of(context).pop(false));
175
- }) ??
176
- false;
177
- }
178
-
222
Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
223
return await showPopUp<bool>(
224
context: context,
@@ -190,6 +233,119 @@ class ContactListPage extends BasePage {
233
}) ??
234
false;
235
}
236
+}
237
+
238
+class ContactListBody extends StatefulWidget {
239
+ ContactListBody({required this.contactListViewModel, required this.tabController});
240
+
241
+ final ContactListViewModel contactListViewModel;
242
+ final TabController tabController;
243
+
244
+ @override
245
+ State<ContactListBody> createState() => _ContactListBodyState();
246
+}
247
+
248
+class _ContactListBodyState extends State<ContactListBody> {
249
+ bool _isContactsTabActive = false;
250
+
251
+ @override
252
+ void initState() {
253
+ super.initState();
254
+ widget.tabController.addListener(_handleTabChange);
255
+ }
256
+
257
+ void _handleTabChange() {
258
+ setState(() {
259
+ _isContactsTabActive = widget.tabController.index == 1;
260
+ });
261
+ }
262
+
263
+ @override
264
+ void dispose() {
265
+ widget.tabController.removeListener(_handleTabChange);
266
+ super.dispose();
267
+ }
268
+
269
+ @override
270
+ Widget build(BuildContext context) {
271
+ final contacts = widget.contactListViewModel.contacts;
272
+ return Scaffold(
273
+ body: Container(
274
+ child: FilteredList(
275
+ list: contacts,
276
+ updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
277
+ canReorder: widget.contactListViewModel.isEditable,
278
+ shrinkWrap: true,
279
+ itemBuilder: (context, index) {
280
+ final contact = contacts[index];
281
+ final contactContent =
282
+ generateContactRaw(context, contact, contacts.length == index + 1);
283
+ return GestureDetector(
284
+ key: Key('${contact.name}'),
285
+ onTap: () async {
286
+ if (!widget.contactListViewModel.isEditable) {
287
+ Navigator.of(context).pop(contact);
288
+ return;
289
+ }
290
+
291
+ final isCopied =
292
+ await showNameAndAddressDialog(context, contact.name, contact.address);
293
+
294
+ if (isCopied) {
295
+ await Clipboard.setData(ClipboardData(text: contact.address));
296
+ await showBar<void>(context, S.of(context).copied_to_clipboard);
297
+ }
298
+ },
299
+ behavior: HitTestBehavior.opaque,
300
+ child: widget.contactListViewModel.isEditable
301
+ ? Slidable(
302
+ key: Key('${contact.key}'),
303
+ endActionPane: _actionPane(context, contact),
304
+ child: contactContent)
305
+ : contactContent,
306
+ );
307
+ },
308
+ ),
309
+ ),
310
+ floatingActionButton:
311
+ _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null,
312
+ );
313
+ }
314
+
315
+ Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
316
+ final image = contact.type.iconPath;
317
+ final currencyIcon = image != null
318
+ ? Image.asset(image, height: 24, width: 24)
319
+ : const SizedBox(height: 24, width: 24);
320
+ return Column(
321
+ children: [
322
+ Container(
323
+ key: Key('${contact.name}'),
324
+ padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24),
325
+ child: Row(
326
+ mainAxisSize: MainAxisSize.min,
327
+ mainAxisAlignment: MainAxisAlignment.start,
328
+ children: <Widget>[
329
+ currencyIcon,
330
+ Expanded(
331
+ child: Padding(
332
+ padding: EdgeInsets.only(left: 12),
333
+ child: Text(
334
+ contact.name,
335
+ style: TextStyle(
336
+ fontSize: 14,
337
+ fontWeight: FontWeight.normal,
338
+ color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
339
+ ),
340
+ ),
341
+ ))
342
+ ],
343
+ ),
344
+ ),
345
+ StandardListSeparator()
346
+ ],
347
+ );
348
+ }
349
350
ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
351
motion: const ScrollMotion(),
@@ -208,7 +364,7 @@ class ContactListPage extends BasePage {
364
final isDelete = await showAlertDialog(context);
365
366
if (isDelete) {
211
- await contactListViewModel.delete(contact);
367
+ await widget.contactListViewModel.delete(contact);
368
}
369
},
370
backgroundColor: Colors.red,
@@ -218,4 +374,80 @@ class ContactListPage extends BasePage {
374
),
375
],
376
);
377
+
378
+ Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) {
379
+ final filterIcon = Image.asset('assets/images/filter_icon.png',
380
+ color: Theme.of(context).appBarTheme.titleTextStyle!.color);
381
+ return MergeSemantics(
382
+ child: SizedBox(
383
+ height: 58,
384
+ width: 58,
385
+ child: ButtonTheme(
386
+ minWidth: double.minPositive,
387
+ child: Semantics(
388
+ container: true,
389
+ child: GestureDetector(
390
+ onTap: () async {
391
+ await showPopUp<void>(
392
+ context: context,
393
+ builder: (context) => FilterListWidget(
394
+ initalType: contactListViewModel.orderType,
395
+ initalAscending: contactListViewModel.ascending,
396
+ onClose: (bool ascending, FilterListOrderType type) async {
397
+ contactListViewModel.setAscending(ascending);
398
+ await contactListViewModel.setOrderType(type);
399
+ },
400
+ ),
401
+ );
402
+ },
403
+ child: Semantics(
404
+ label: 'Transaction Filter',
405
+ button: true,
406
+ enabled: true,
407
+ child: Container(
408
+ height: 36,
409
+ width: 36,
410
+ decoration: BoxDecoration(
411
+ shape: BoxShape.circle,
412
+ color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor,
413
+ ),
414
+ child: filterIcon,
415
+ ),
416
+ ),
417
+ ),
418
+ ),
419
+ ),
420
+ ),
421
+ );
422
+ }
423
+
424
+ Future<bool> showAlertDialog(BuildContext context) async {
425
+ return await showPopUp<bool>(
426
+ context: context,
427
+ builder: (BuildContext context) {
428
+ return AlertWithTwoActions(
429
+ alertTitle: S.of(context).address_remove_contact,
430
+ alertContent: S.of(context).address_remove_content,
431
+ rightButtonText: S.of(context).remove,
432
+ leftButtonText: S.of(context).cancel,
433
+ actionRightButton: () => Navigator.of(context).pop(true),
434
+ actionLeftButton: () => Navigator.of(context).pop(false));
435
+ }) ??
436
+ false;
437
+ }
438
+
439
+ Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
440
+ return await showPopUp<bool>(
441
+ context: context,
442
+ builder: (BuildContext context) {
443
+ return AlertWithTwoActions(
444
+ alertTitle: name,
445
+ alertContent: address,
446
+ rightButtonText: S.of(context).copy,
447
+ leftButtonText: S.of(context).cancel,
448
+ actionRightButton: () => Navigator.of(context).pop(true),
449
+ actionLeftButton: () => Navigator.of(context).pop(false));
450
+ }) ??
451
+ false;
452
+ }
453
}
lib/src/screens/dashboard/widgets/filter_list_widget.dart
+13
-13
@@ -18,9 +18,9 @@ class FilterListWidget extends StatefulWidget {
18
required this.onClose,
19
});
20
21
- final WalletListOrderType? initalType;
21
+ final FilterListOrderType? initalType;
22
final bool initalAscending;
23
- final Function(bool, WalletListOrderType) onClose;
23
+ final Function(bool, FilterListOrderType) onClose;
24
25
@override
26
FilterListWidgetState createState() => FilterListWidgetState();
@@ -28,7 +28,7 @@ class FilterListWidget extends StatefulWidget {
28
29
class FilterListWidgetState extends State<FilterListWidget> {
30
late bool ascending;
31
- late WalletListOrderType? type;
31
+ late FilterListOrderType? type;
32
33
@override
34
void initState() {
@@ -37,7 +37,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
37
type = widget.initalType;
38
}
39
40
- void setSelectedOrderType(WalletListOrderType? orderType) {
40
+ void setSelectedOrderType(FilterListOrderType? orderType) {
41
setState(() {
42
type = orderType;
43
});
@@ -72,7 +72,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
72
),
73
),
74
),
75
- if (type != WalletListOrderType.Custom) ...[
75
+ if (type != FilterListOrderType.Custom) ...[
76
sectionDivider,
77
SettingsChoicesCell(
78
ChoicesListItem<ListOrderMode>(
@@ -89,10 +89,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
89
],
90
sectionDivider,
91
RadioListTile(
92
- value: WalletListOrderType.CreationDate,
92
+ value: FilterListOrderType.CreationDate,
93
groupValue: type,
94
title: Text(
95
- WalletListOrderType.CreationDate.toString(),
95
+ FilterListOrderType.CreationDate.toString(),
96
style: TextStyle(
97
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
98
fontSize: 16,
@@ -104,10 +104,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
104
activeColor: Theme.of(context).primaryColor,
105
),
106
RadioListTile(
107
- value: WalletListOrderType.Alphabetical,
107
+ value: FilterListOrderType.Alphabetical,
108
groupValue: type,
109
title: Text(
110
- WalletListOrderType.Alphabetical.toString(),
110
+ FilterListOrderType.Alphabetical.toString(),
111
style: TextStyle(
112
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
113
fontSize: 16,
@@ -119,10 +119,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
119
activeColor: Theme.of(context).primaryColor,
120
),
121
RadioListTile(
122
- value: WalletListOrderType.GroupByType,
122
+ value: FilterListOrderType.GroupByType,
123
groupValue: type,
124
title: Text(
125
- WalletListOrderType.GroupByType.toString(),
125
+ FilterListOrderType.GroupByType.toString(),
126
style: TextStyle(
127
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
128
fontSize: 16,
@@ -134,10 +134,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
134
activeColor: Theme.of(context).primaryColor,
135
),
136
RadioListTile(
137
- value: WalletListOrderType.Custom,
137
+ value: FilterListOrderType.Custom,
138
groupValue: type,
139
title: Text(
140
- WalletListOrderType.Custom.toString(),
140
+ FilterListOrderType.Custom.toString(),
141
style: TextStyle(
142
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
143
fontSize: 16,
lib/src/screens/wallet_list/filtered_list.dart
+30
-16
@@ -7,13 +7,17 @@ class FilteredList extends StatefulWidget {
7
required this.list,
8
required this.itemBuilder,
9
required this.updateFunction,
10
+ this.canReorder = true,
11
this.shrinkWrap = false,
12
+ this.physics,
13
});
14
15
final ObservableList<dynamic> list;
16
final Widget Function(BuildContext, int) itemBuilder;
17
final Function updateFunction;
18
+ final bool canReorder;
19
final bool shrinkWrap;
20
+ final ScrollPhysics? physics;
21
22
@override
23
FilteredListState createState() => FilteredListState();
@@ -22,21 +26,31 @@ class FilteredList extends StatefulWidget {
26
class FilteredListState extends State<FilteredList> {
27
@override
28
Widget build(BuildContext context) {
25
- return Observer(
26
- builder: (_) => ReorderableListView.builder(
27
- shrinkWrap: widget.shrinkWrap,
28
- physics: const BouncingScrollPhysics(),
29
- itemBuilder: widget.itemBuilder,
30
- itemCount: widget.list.length,
31
- onReorder: (int oldIndex, int newIndex) {
32
- if (oldIndex < newIndex) {
33
- newIndex -= 1;
34
- }
35
- final dynamic item = widget.list.removeAt(oldIndex);
36
- widget.list.insert(newIndex, item);
37
- widget.updateFunction();
38
- },
39
- ),
40
- );
29
+ if (widget.canReorder) {
30
+ return Observer(
31
+ builder: (_) => ReorderableListView.builder(
32
+ shrinkWrap: widget.shrinkWrap,
33
+ physics: widget.physics ?? const BouncingScrollPhysics(),
34
+ itemBuilder: widget.itemBuilder,
35
+ itemCount: widget.list.length,
36
+ onReorder: (int oldIndex, int newIndex) {
37
+ if (oldIndex < newIndex) {
38
+ newIndex -= 1;
39
+ }
40
+ final dynamic item = widget.list.removeAt(oldIndex);
41
+ widget.list.insert(newIndex, item);
42
+ widget.updateFunction();
43
+ },
44
+ ),
45
+ );
46
+ } else {
47
+ return Observer(
48
+ builder: (_) => ListView.builder(
49
+ physics: widget.physics ?? const BouncingScrollPhysics(),
50
+ itemBuilder: widget.itemBuilder,
51
+ itemCount: widget.list.length,
52
+ ),
53
+ );
54
+ }
55
}
56
}
lib/src/screens/wallet_list/wallet_list_page.dart
+1
-1
@@ -59,7 +59,7 @@ class WalletListPage extends BasePage {
59
builder: (context) => FilterListWidget(
60
initalType: walletListViewModel.orderType,
61
initalAscending: walletListViewModel.ascending,
62
- onClose: (bool ascending, WalletListOrderType type) async {
62
+ onClose: (bool ascending, FilterListOrderType type) async {
63
walletListViewModel.setAscending(ascending);
64
await walletListViewModel.setOrderType(type);
65
},
lib/src/widgets/collapsible_standart_list.dart
deleted
-39
@@ -1,39 +0,0 @@
1
-import 'package:cake_wallet/src/widgets/standard_list.dart';
2
-import 'package:flutter/material.dart';
3
-
4
-class CollapsibleSectionList extends SectionStandardList {
5
- CollapsibleSectionList(
6
- {required int sectionCount,
7
- required int Function(int sectionIndex) itemCounter,
8
- required Widget Function(int sectionIndex, int itemIndex) itemBuilder,
9
- Widget Function(int sectionIndex)? sectionTitleBuilder,
10
- bool hasTopSeparator = false})
11
- : super(
12
- hasTopSeparator: hasTopSeparator,
13
- sectionCount: sectionCount,
14
- itemCounter: itemCounter,
15
- itemBuilder: itemBuilder,
16
- sectionTitleBuilder: sectionTitleBuilder);
17
-
18
- @override
19
- Widget buildTitle(List<Widget> items, int sectionIndex) {
20
- if (sectionTitleBuilder == null) {
21
- throw Exception('Cannot to build title. sectionTitleBuilder is null');
22
- }
23
- return sectionTitleBuilder!.call(sectionIndex);
24
- }
25
-
26
- @override
27
- List<Widget> buildSection(int itemCount, List<Widget> items, int sectionIndex) {
28
- final List<Widget> section = [];
29
-
30
- for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
31
- final item = itemBuilder(sectionIndex, itemIndex);
32
-
33
- section.add(StandardListSeparator());
34
-
35
- section.add(item);
36
- }
37
- return section;
38
- }
39
-}
lib/store/settings_store.dart
+34
-6
@@ -62,9 +62,11 @@ abstract class SettingsStoreBase with Store {
62
required bool initialAppSecure,
63
required bool initialDisableBuy,
64
required bool initialDisableSell,
65
+ required FilterListOrderType initialWalletListOrder,
66
+ required FilterListOrderType initialContactListOrder,
67
required bool initialDisableBulletin,
66
- required WalletListOrderType initialWalletListOrder,
68
required bool initialWalletListAscending,
69
+ required bool initialContactListAscending,
70
required FiatApiMode initialFiatMode,
71
required bool initialAllowBiometricalAuthentication,
72
required String initialTotpSecretKey,
@@ -149,7 +151,9 @@ abstract class SettingsStoreBase with Store {
151
disableSell = initialDisableSell,
152
disableBulletin = initialDisableBulletin,
153
walletListOrder = initialWalletListOrder,
154
+ contactListOrder = initialContactListOrder,
155
walletListAscending = initialWalletListAscending,
156
+ contactListAscending = initialContactListAscending,
157
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
158
exchangeStatus = initialExchangeStatus,
159
currentTheme = initialTheme,
@@ -324,14 +328,24 @@ abstract class SettingsStoreBase with Store {
328
329
reaction(
330
(_) => walletListOrder,
327
- (WalletListOrderType walletListOrder) =>
331
+ (FilterListOrderType walletListOrder) =>
332
sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index));
333
334
+ reaction(
335
+ (_) => contactListOrder,
336
+ (FilterListOrderType contactListOrder) =>
337
+ sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index));
338
+
339
reaction(
340
(_) => walletListAscending,
341
(bool walletListAscending) =>
342
sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending));
343
344
+ reaction(
345
+ (_) => contactListAscending,
346
+ (bool contactListAscending) =>
347
+ sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending));
348
+
349
reaction(
350
(_) => autoGenerateSubaddressStatus,
351
(AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
@@ -645,15 +659,21 @@ abstract class SettingsStoreBase with Store {
659
@observable
660
bool disableSell;
661
662
+ @observable
663
+ FilterListOrderType contactListOrder;
664
+
665
@observable
666
bool disableBulletin;
667
668
@observable
652
- WalletListOrderType walletListOrder;
669
+ FilterListOrderType walletListOrder;
670
671
@observable
672
bool walletListAscending;
673
674
+ @observable
675
+ bool contactListAscending;
676
+
677
@observable
678
bool allowBiometricalAuthentication;
679
@@ -907,9 +927,13 @@ abstract class SettingsStoreBase with Store {
927
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
928
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
929
final walletListOrder =
910
- WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
930
+ FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
931
+ final contactListOrder =
932
+ FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
933
final walletListAscending =
934
sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
935
+ final contactListAscending =
936
+ sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
937
final currentFiatApiMode = FiatApiMode.deserialize(
938
raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ??
939
FiatApiMode.enabled.raw);
@@ -1200,6 +1224,8 @@ abstract class SettingsStoreBase with Store {
1224
initialDisableBulletin: disableBulletin,
1225
initialWalletListOrder: walletListOrder,
1226
initialWalletListAscending: walletListAscending,
1227
+ initialContactListOrder: contactListOrder,
1228
+ initialContactListAscending: contactListAscending,
1229
initialFiatMode: currentFiatApiMode,
1230
initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
1231
initialCake2FAPresetOptions: selectedCake2FAPreset,
@@ -1348,9 +1374,11 @@ abstract class SettingsStoreBase with Store {
1374
disableBulletin =
1375
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
1376
walletListOrder =
1351
- WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
1377
+ FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
1378
+ contactListOrder =
1379
+ FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
1380
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
1353
-
1381
+ contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
1382
shouldShowMarketPlaceInDashboard =
1383
sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
1384
shouldShowMarketPlaceInDashboard;
lib/view_model/contact_list/contact_list_view_model.dart
+79
-5
@@ -1,18 +1,20 @@
1
import 'dart:async';
2
+
3
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
4
+import 'package:cake_wallet/entities/contact.dart';
5
import 'package:cake_wallet/entities/contact_base.dart';
6
+import 'package:cake_wallet/entities/contact_record.dart';
7
import 'package:cake_wallet/entities/wallet_contact.dart';
8
+import 'package:cake_wallet/entities/wallet_list_order_types.dart';
9
import 'package:cake_wallet/generated/i18n.dart';
10
import 'package:cake_wallet/store/settings_store.dart';
11
+import 'package:cake_wallet/utils/mobx.dart';
12
+import 'package:collection/collection.dart';
13
+import 'package:cw_core/crypto_currency.dart';
14
import 'package:cw_core/wallet_info.dart';
15
import 'package:cw_core/wallet_type.dart';
16
import 'package:hive/hive.dart';
17
import 'package:mobx/mobx.dart';
11
-import 'package:cake_wallet/entities/contact_record.dart';
12
-import 'package:cake_wallet/entities/contact.dart';
13
-import 'package:cake_wallet/utils/mobx.dart';
14
-import 'package:cw_core/crypto_currency.dart';
15
-import 'package:collection/collection.dart';
18
19
part 'contact_list_view_model.g.dart';
20
@@ -75,6 +77,8 @@ abstract class ContactListViewModelBase with Store {
77
_subscription = contactSource.bindToListWithTransform(
78
contacts, (Contact contact) => ContactRecord(contactSource, contact),
79
initialFire: true);
80
+
81
+ setOrderType(settingsStore.contactListOrder);
82
}
83
84
String _createName(String walletName, String label, {int? key = null}) {
@@ -93,6 +97,10 @@ abstract class ContactListViewModelBase with Store {
97
98
bool get isEditable => _currency == null;
99
100
+ FilterListOrderType? get orderType => settingsStore.contactListOrder;
101
+
102
+ bool get ascending => settingsStore.contactListAscending;
103
+
104
@computed
105
bool get shouldRequireTOTP2FAForAddingContacts =>
106
settingsStore.shouldRequireTOTP2FAForAddingContacts;
@@ -118,4 +126,70 @@ abstract class ContactListViewModelBase with Store {
126
_currency?.toString() == element.type.tag ||
127
_currency?.tag == element.type.toString();
128
}
129
+
130
+ void dispose() async {
131
+ _subscription?.cancel();
132
+ final List<Contact> contactsSourceCopy = contacts.map((e) => e.original).toList();
133
+ await reorderContacts(contactsSourceCopy);
134
+ }
135
+
136
+ void reorderAccordingToContactList() =>
137
+ settingsStore.contactListOrder = FilterListOrderType.Custom;
138
+
139
+ Future<void> reorderContacts(List<Contact> contactCopy) async {
140
+ await contactSource.deleteAll(contactCopy.map((e) => e.key).toList());
141
+ await contactSource.addAll(contactCopy);
142
+ }
143
+
144
+ Future<void> sortGroupByType() async {
145
+ List<Contact> contactsSourceCopy = contactSource.values.toList();
146
+
147
+ contactsSourceCopy.sort((a, b) => ascending
148
+ ? a.type.toString().compareTo(b.type.toString())
149
+ : b.type.toString().compareTo(a.type.toString()));
150
+
151
+ await reorderContacts(contactsSourceCopy);
152
+ }
153
+
154
+ Future<void> sortAlphabetically() async {
155
+ List<Contact> contactsSourceCopy = contactSource.values.toList();
156
+
157
+ contactsSourceCopy
158
+ .sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name));
159
+
160
+ await reorderContacts(contactsSourceCopy);
161
+ }
162
+
163
+ Future<void> sortByCreationDate() async {
164
+ List<Contact> contactsSourceCopy = contactSource.values.toList();
165
+
166
+ contactsSourceCopy.sort((a, b) =>
167
+ ascending ? a.lastChange.compareTo(b.lastChange) : b.lastChange.compareTo(a.lastChange));
168
+
169
+ await reorderContacts(contactsSourceCopy);
170
+ }
171
+
172
+ void setAscending(bool ascending) => settingsStore.contactListAscending = ascending;
173
+
174
+ Future<void> setOrderType(FilterListOrderType? type) async {
175
+ if (type == null) return;
176
+
177
+ settingsStore.contactListOrder = type;
178
+
179
+ switch (type) {
180
+ case FilterListOrderType.CreationDate:
181
+ await sortByCreationDate();
182
+ break;
183
+ case FilterListOrderType.Alphabetical:
184
+ await sortAlphabetically();
185
+ break;
186
+ case FilterListOrderType.GroupByType:
187
+ await sortGroupByType();
188
+ break;
189
+ case FilterListOrderType.Custom:
190
+ default:
191
+ reorderAccordingToContactList();
192
+ break;
193
+ }
194
+ }
195
}
lib/view_model/contact_list/contact_view_model.dart
+20
-4
@@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
2
import 'package:hive/hive.dart';
3
import 'package:mobx/mobx.dart';
4
import 'package:cake_wallet/core/execution_state.dart';
5
-import 'package:cw_core/wallet_base.dart';
5
+import 'package:cake_wallet/generated/i18n.dart';
6
import 'package:cake_wallet/entities/contact.dart';
7
import 'package:cw_core/crypto_currency.dart';
8
@@ -17,7 +17,9 @@ abstract class ContactViewModelBase with Store {
17
_contact = contact,
18
name = contact?.name ?? '',
19
address = contact?.address ?? '',
20
- currency = contact?.type;
20
+ currency = contact?.type,
21
+ lastChange = contact?.lastChange;
22
+
23
24
@observable
25
ExecutionState state;
@@ -31,6 +33,8 @@ abstract class ContactViewModelBase with Store {
33
@observable
34
CryptoCurrency? currency;
35
36
+ DateTime? lastChange;
37
+
38
@computed
39
bool get isReady =>
40
name.isNotEmpty &&
@@ -51,20 +55,32 @@ abstract class ContactViewModelBase with Store {
55
Future<void> save() async {
56
try {
57
state = IsExecutingState();
58
+ final now = DateTime.now();
59
+
60
+ if (doesContactNameExist(name)) {
61
+ state = FailureState(S.current.contact_name_exists);
62
+ return;
63
+ }
64
65
if (_contact != null && _contact!.original.isInBox) {
66
_contact?.name = name;
67
_contact?.address = address;
68
_contact?.type = currency!;
69
+ _contact?.lastChange = now;
70
await _contact?.save();
71
} else {
72
await _contacts
62
- .add(Contact(name: name, address: address, type: currency!));
73
+ .add(Contact(name: name, address: address, type: currency!, lastChange: now));
74
}
75
76
+ lastChange = now;
77
state = ExecutedSuccessfullyState();
78
} catch (e) {
79
state = FailureState(e.toString());
80
}
81
}
70
-}
82
+
83
+ bool doesContactNameExist(String name) {
84
+ return _contacts.values.any((contact) => contact.name == name);
85
+ }
86
+}
\ No newline at end of file
lib/view_model/wallet_list/wallet_list_view_model.dart
+7
-7
@@ -76,7 +76,7 @@ abstract class WalletListViewModelBase with Store {
76
await _appStore.changeCurrentWallet(wallet);
77
}
78
79
- WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
79
+ FilterListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
80
81
bool get ascending => _appStore.settingsStore.walletListAscending;
82
@@ -108,7 +108,7 @@ abstract class WalletListViewModelBase with Store {
108
return;
109
}
110
111
- _appStore.settingsStore.walletListOrder = WalletListOrderType.Custom;
111
+ _appStore.settingsStore.walletListOrder = FilterListOrderType.Custom;
112
113
// make a copy of the walletInfoSource:
114
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
@@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store {
186
_appStore.settingsStore.walletListAscending = ascending;
187
}
188
189
- Future<void> setOrderType(WalletListOrderType? type) async {
189
+ Future<void> setOrderType(FilterListOrderType? type) async {
190
if (type == null) return;
191
192
_appStore.settingsStore.walletListOrder = type;
193
194
switch (type) {
195
- case WalletListOrderType.CreationDate:
195
+ case FilterListOrderType.CreationDate:
196
await sortByCreationDate();
197
break;
198
- case WalletListOrderType.Alphabetical:
198
+ case FilterListOrderType.Alphabetical:
199
await sortAlphabetically();
200
break;
201
- case WalletListOrderType.GroupByType:
201
+ case FilterListOrderType.GroupByType:
202
await sortGroupByType();
203
break;
204
- case WalletListOrderType.Custom:
204
+ case FilterListOrderType.Custom:
205
default:
206
await reorderAccordingToWalletList();
207
break;
res/values/strings_ar.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "انت تدفع",
938
"you_will_get": "حول الى",
939
"you_will_send": "تحويل من",
940
- "yy": "YY"
941
-}
\ No newline at end of file
940
+ "yy": "YY",
941
+ "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ"
942
+}
res/values/strings_bg.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Вие плащате",
938
"you_will_get": "Обръщане в",
939
"you_will_send": "Обръщане от",
940
- "yy": "гг"
941
-}
\ No newline at end of file
940
+ "yy": "гг",
941
+ "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име."
942
+}
res/values/strings_cs.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Zaplatíte",
938
"you_will_get": "Směnit na",
939
"you_will_send": "Směnit z",
940
- "yy": "YY"
941
-}
\ No newline at end of file
940
+ "yy": "YY",
941
+ "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název."
942
+}
res/values/strings_en.arb
+3
-2
@@ -940,5 +940,6 @@
940
"you_pay": "You Pay",
941
"you_will_get": "Convert to",
942
"you_will_send": "Convert from",
943
- "yy": "YY"
944
-}
\ No newline at end of file
943
+ "yy": "YY",
944
+ "contact_name_exists": "A contact with that name already exists. Please choose a different name."
945
+}
res/values/strings_es.arb
+2
-1
@@ -938,5 +938,6 @@
938
"you_pay": "Tú pagas",
939
"you_will_get": "Convertir a",
940
"you_will_send": "Convertir de",
941
- "yy": "YY"
941
+ "yy": "YY",
942
+ "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente."
943
}
res/values/strings_fr.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Vous payez",
938
"you_will_get": "Convertir vers",
939
"you_will_send": "Convertir depuis",
940
- "yy": "AA"
941
-}
\ No newline at end of file
940
+ "yy": "AA",
941
+ "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom."
942
+}
res/values/strings_ha.arb
+3
-2
@@ -939,5 +939,6 @@
939
"you_pay": "Ka Bayar",
940
"you_will_get": "Maida zuwa",
941
"you_will_send": "Maida daga",
942
- "yy": "YY"
943
-}
\ No newline at end of file
942
+ "yy": "YY",
943
+ "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban."
944
+}
res/values/strings_hi.arb
+3
-2
@@ -939,5 +939,6 @@
939
"you_pay": "आप भुगतान करते हैं",
940
"you_will_get": "में बदलें",
941
"you_will_send": "से रूपांतरित करें",
942
- "yy": "वाईवाई"
943
-}
\ No newline at end of file
942
+ "yy": "वाईवाई",
943
+ "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें."
944
+}
res/values/strings_hr.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Vi plaćate",
938
"you_will_get": "Razmijeni u",
939
"you_will_send": "Razmijeni iz",
940
- "yy": "GG"
941
-}
\ No newline at end of file
940
+ "yy": "GG",
941
+ "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime."
942
+}
res/values/strings_id.arb
+3
-2
@@ -940,5 +940,6 @@
940
"you_pay": "Anda Membayar",
941
"you_will_get": "Konversi ke",
942
"you_will_send": "Konversi dari",
943
- "yy": "YY"
944
-}
\ No newline at end of file
943
+ "yy": "YY",
944
+ "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain."
945
+}
res/values/strings_it.arb
+3
-2
@@ -940,5 +940,6 @@
940
"you_pay": "Tu paghi",
941
"you_will_get": "Converti a",
942
"you_will_send": "Conveti da",
943
- "yy": "YY"
944
-}
\ No newline at end of file
943
+ "yy": "YY",
944
+ "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso."
945
+}
res/values/strings_ja.arb
+3
-2
@@ -938,5 +938,6 @@
938
"you_pay": "あなたが支払う",
939
"you_will_get": "に変換",
940
"you_will_send": "から変換",
941
- "yy": "YY"
942
-}
\ No newline at end of file
941
+ "yy": "YY",
942
+ "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。"
943
+}
res/values/strings_ko.arb
+3
-2
@@ -939,5 +939,6 @@
939
"you_will_get": "로 변환하다",
940
"you_will_send": "다음에서 변환",
941
"YY": "YY",
942
- "yy": "YY"
943
-}
\ No newline at end of file
942
+ "yy": "YY",
943
+ "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요."
944
+}
res/values/strings_my.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "သင်ပေးချေပါ။",
938
"you_will_get": "သို့ပြောင်းပါ။",
939
"you_will_send": "မှပြောင်းပါ။",
940
- "yy": "YY"
941
-}
\ No newline at end of file
940
+ "yy": "YY",
941
+ "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။"
942
+}
res/values/strings_nl.arb
+3
-2
@@ -938,5 +938,6 @@
938
"you_pay": "U betaalt",
939
"you_will_get": "Converteren naar",
940
"you_will_send": "Converteren van",
941
- "yy": "JJ"
942
-}
\ No newline at end of file
941
+ "yy": "JJ",
942
+ "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam."
943
+}
res/values/strings_pl.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Płacisz",
938
"you_will_get": "Konwertuj na",
939
"you_will_send": "Konwertuj z",
940
- "yy": "RR"
941
-}
\ No newline at end of file
940
+ "yy": "RR",
941
+ "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę."
942
+}
res/values/strings_pt.arb
+1
-1
@@ -941,4 +941,4 @@
941
"you_will_get": "Converter para",
942
"you_will_send": "Converter de",
943
"yy": "aa"
944
-}
\ No newline at end of file
944
+}
res/values/strings_ru.arb
+3
-2
@@ -938,5 +938,6 @@
938
"you_pay": "Вы платите",
939
"you_will_get": "Конвертировать в",
940
"you_will_send": "Конвертировать из",
941
- "yy": "ГГ"
942
-}
\ No newline at end of file
941
+ "yy": "ГГ",
942
+ "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя."
943
+}
res/values/strings_th.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "คุณจ่าย",
938
"you_will_get": "แปลงเป็น",
939
"you_will_send": "แปลงจาก",
940
- "yy": "ปี"
941
-}
\ No newline at end of file
940
+ "yy": "ปี",
941
+ "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น"
942
+}
res/values/strings_tl.arb
+1
-1
@@ -938,4 +938,4 @@
938
"you_will_get": "I-convert sa",
939
"you_will_send": "I-convert mula sa",
940
"yy": "YY"
941
-}
\ No newline at end of file
941
+}
res/values/strings_tr.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "Şu kadar ödeyeceksin: ",
938
"you_will_get": "Biçimine dönüştür:",
939
"you_will_send": "Biçiminden dönüştür:",
940
- "yy": "YY"
941
-}
\ No newline at end of file
940
+ "yy": "YY",
941
+ "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin."
942
+}
res/values/strings_uk.arb
+3
-2
@@ -938,5 +938,6 @@
938
"you_pay": "Ви платите",
939
"you_will_get": "Конвертувати в",
940
"you_will_send": "Конвертувати з",
941
- "yy": "YY"
942
-}
\ No newline at end of file
941
+ "yy": "YY",
942
+ "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я."
943
+}
res/values/strings_ur.arb
+3
-2
@@ -939,5 +939,6 @@
939
"you_pay": "تم ادا کرو",
940
"you_will_get": "میں تبدیل کریں۔",
941
"you_will_send": "سے تبدیل کریں۔",
942
- "yy": "YY"
943
-}
\ No newline at end of file
942
+ "yy": "YY",
943
+ "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ"
944
+}
res/values/strings_yo.arb
+3
-2
@@ -938,5 +938,6 @@
938
"you_pay": "Ẹ sàn",
939
"you_will_get": "Ṣe pàṣípààrọ̀ sí",
940
"you_will_send": "Ṣe pàṣípààrọ̀ láti",
941
- "yy": "Ọd"
942
-}
\ No newline at end of file
941
+ "yy": "Ọd",
942
+ "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ."
943
+}
res/values/strings_zh.arb
+3
-2
@@ -937,5 +937,6 @@
937
"you_pay": "你付钱",
938
"you_will_get": "转换到",
939
"you_will_send": "转换自",
940
- "yy": "YY"
941
-}
\ No newline at end of file
940
+ "yy": "YY",
941
+ "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。"
942
+}