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>
This commit is contained in:
Serhii 2024-11-07 03:26:14 +02:00 committed by GitHub
parent 459f0d352d
commit 109d9b458e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 588 additions and 236 deletions

View file

@ -7,7 +7,8 @@ part 'contact.g.dart';
@HiveType(typeId: Contact.typeId) @HiveType(typeId: Contact.typeId)
class Contact extends HiveObject with Keyable { class Contact extends HiveObject with Keyable {
Contact({required this.name, required this.address, CryptoCurrency? type}) { Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange})
: lastChange = lastChange ?? DateTime.now() {
if (type != null) { if (type != null) {
raw = type.raw; raw = type.raw;
} }
@ -25,6 +26,9 @@ class Contact extends HiveObject with Keyable {
@HiveField(2, defaultValue: 0) @HiveField(2, defaultValue: 0)
late int raw; late int raw;
@HiveField(3)
DateTime lastChange;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
@override @override
@ -36,6 +40,5 @@ class Contact extends HiveObject with Keyable {
@override @override
int get hashCode => key.hashCode; int get hashCode => key.hashCode;
void updateCryptoCurrency({required CryptoCurrency currency}) => void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw;
raw = currency.raw;
} }

View file

@ -1,22 +1,21 @@
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/record.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/record.dart';
import 'package:cake_wallet/entities/contact_base.dart';
part 'contact_record.g.dart'; part 'contact_record.g.dart';
class ContactRecord = ContactRecordBase with _$ContactRecord; class ContactRecord = ContactRecordBase with _$ContactRecord;
abstract class ContactRecordBase extends Record<Contact> abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
with Store
implements ContactBase {
ContactRecordBase(Box<Contact> source, Contact original) ContactRecordBase(Box<Contact> source, Contact original)
: name = original.name, : name = original.name,
address = original.address, address = original.address,
type = original.type, type = original.type,
super(source, original); lastChange = original.lastChange,
super(source, original);
@override @override
@observable @observable
@ -30,14 +29,14 @@ abstract class ContactRecordBase extends Record<Contact>
@observable @observable
CryptoCurrency type; CryptoCurrency type;
DateTime? lastChange;
@override @override
void toBind(Contact original) { void toBind(Contact original) {
reaction((_) => name, (String name) => original.name = name); reaction((_) => name, (String name) => original.name = name);
reaction((_) => address, (String address) => original.address = address); reaction((_) => address, (String address) => original.address = address);
reaction( reaction((_) => type,
(_) => type, (CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency));
(CryptoCurrency currency) =>
original.updateCryptoCurrency(currency: currency));
} }
@override @override

View file

@ -25,7 +25,9 @@ class PreferencesKey {
static const disableBulletinKey = 'disable_bulletin'; static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider'; static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order'; static const walletListOrder = 'wallet_list_order';
static const contactListOrder = 'contact_list_order';
static const walletListAscending = 'wallet_list_ascending'; static const walletListAscending = 'wallet_list_ascending';
static const contactListAscending = 'contact_list_ascending';
static const currentFiatApiModeKey = 'current_fiat_api_mode'; static const currentFiatApiModeKey = 'current_fiat_api_mode';
static const failedTotpTokenTrials = 'failed_token_trials'; static const failedTotpTokenTrials = 'failed_token_trials';
static const disableExchangeKey = 'disable_exchange'; static const disableExchangeKey = 'disable_exchange';

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
enum WalletListOrderType { enum FilterListOrderType {
CreationDate, CreationDate,
Alphabetical, Alphabetical,
GroupByType, GroupByType,
@ -9,13 +9,13 @@ enum WalletListOrderType {
@override @override
String toString() { String toString() {
switch (this) { switch (this) {
case WalletListOrderType.CreationDate: case FilterListOrderType.CreationDate:
return S.current.creation_date; return S.current.creation_date;
case WalletListOrderType.Alphabetical: case FilterListOrderType.Alphabetical:
return S.current.alphabetical; return S.current.alphabetical;
case WalletListOrderType.GroupByType: case FilterListOrderType.GroupByType:
return S.current.group_by_type; return S.current.group_by_type;
case WalletListOrderType.Custom: case FilterListOrderType.Custom:
return S.current.custom_drag; return S.current.custom_drag;
} }
} }

View file

@ -1,21 +1,24 @@
import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class ContactListPage extends BasePage { class ContactListPage extends BasePage {
ContactListPage(this.contactListViewModel, this.authService); ContactListPage(this.contactListViewModel, this.authService);
@ -74,45 +77,101 @@ class ContactListPage extends BasePage {
} }
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel);
return Container( }
padding: EdgeInsets.all(20.0),
child: Observer(builder: (_) {
final contacts = contactListViewModel.contactsToShow;
final walletContacts = contactListViewModel.walletContactsToShow;
return CollapsibleSectionList(
sectionCount: 2,
sectionTitleBuilder: (int sectionIndex) {
var title = S.current.contact_list_contacts;
if (sectionIndex == 0) { class ContactPageBody extends StatefulWidget {
title = S.current.contact_list_wallets; const ContactPageBody({required this.contactListViewModel});
}
return Container( final ContactListViewModel contactListViewModel;
padding: EdgeInsets.only(bottom: 10),
child: Text(title, style: TextStyle(fontSize: 36)));
},
itemCounter: (int sectionIndex) =>
sectionIndex == 0 ? walletContacts.length : contacts.length,
itemBuilder: (int sectionIndex, index) {
if (sectionIndex == 0) {
final walletInfo = walletContacts[index];
return generateRaw(context, walletInfo);
}
final contact = contacts[index]; @override
final content = generateRaw(context, contact); State<ContactPageBody> createState() => _ContactPageBodyState();
return contactListViewModel.isEditable }
? Slidable(
key: Key('${contact.key}'), class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProviderStateMixin {
endActionPane: _actionPane(context, contact), late TabController _tabController;
child: content,
) @override
: content; void initState() {
}, super.initState();
); _tabController = TabController(length: 2, vsync: this);
})); }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 24),
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: TabBar(
controller: _tabController,
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
labelStyle: TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.titleTextStyle!.color,
),
unselectedLabelStyle: TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)),
labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
indicatorPadding: EdgeInsets.zero,
labelPadding: EdgeInsets.only(right: 24),
tabAlignment: TabAlignment.center,
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: [
Tab(text: S.of(context).wallets),
Tab(text: S.of(context).contact_list_contacts),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildWalletContacts(context),
ContactListBody(
contactListViewModel: widget.contactListViewModel,
tabController: _tabController),
],
),
),
],
),
);
}
Widget _buildWalletContacts(BuildContext context) {
final walletContacts = widget.contactListViewModel.walletContactsToShow;
return ListView.builder(
shrinkWrap: true,
itemCount: walletContacts.length * 2,
itemBuilder: (context, index) {
if (index.isOdd) {
return StandardListSeparator();
} else {
final walletInfo = walletContacts[index ~/ 2];
return generateRaw(context, walletInfo);
}
},
);
} }
Widget generateRaw(BuildContext context, ContactBase contact) { Widget generateRaw(BuildContext context, ContactBase contact) {
@ -123,7 +182,7 @@ class ContactListPage extends BasePage {
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
if (!contactListViewModel.isEditable) { if (!widget.contactListViewModel.isEditable) {
Navigator.of(context).pop(contact); Navigator.of(context).pop(contact);
return; return;
} }
@ -143,8 +202,7 @@ class ContactListPage extends BasePage {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
currencyIcon, currencyIcon,
Expanded( Padding(
child: Padding(
padding: EdgeInsets.only(left: 12), padding: EdgeInsets.only(left: 12),
child: Text( child: Text(
contact.name, contact.name,
@ -154,13 +212,215 @@ class ContactListPage extends BasePage {
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
), ),
), ),
)) ),
], ],
), ),
), ),
); );
} }
Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
return await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: name,
alertContent: address,
rightButtonText: S.of(context).copy,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ??
false;
}
}
class ContactListBody extends StatefulWidget {
ContactListBody({required this.contactListViewModel, required this.tabController});
final ContactListViewModel contactListViewModel;
final TabController tabController;
@override
State<ContactListBody> createState() => _ContactListBodyState();
}
class _ContactListBodyState extends State<ContactListBody> {
bool _isContactsTabActive = false;
@override
void initState() {
super.initState();
widget.tabController.addListener(_handleTabChange);
}
void _handleTabChange() {
setState(() {
_isContactsTabActive = widget.tabController.index == 1;
});
}
@override
void dispose() {
widget.tabController.removeListener(_handleTabChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
final contacts = widget.contactListViewModel.contacts;
return Scaffold(
body: Container(
child: FilteredList(
list: contacts,
updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
canReorder: widget.contactListViewModel.isEditable,
shrinkWrap: true,
itemBuilder: (context, index) {
final contact = contacts[index];
final contactContent =
generateContactRaw(context, contact, contacts.length == index + 1);
return GestureDetector(
key: Key('${contact.name}'),
onTap: () async {
if (!widget.contactListViewModel.isEditable) {
Navigator.of(context).pop(contact);
return;
}
final isCopied =
await showNameAndAddressDialog(context, contact.name, contact.address);
if (isCopied) {
await Clipboard.setData(ClipboardData(text: contact.address));
await showBar<void>(context, S.of(context).copied_to_clipboard);
}
},
behavior: HitTestBehavior.opaque,
child: widget.contactListViewModel.isEditable
? Slidable(
key: Key('${contact.key}'),
endActionPane: _actionPane(context, contact),
child: contactContent)
: contactContent,
);
},
),
),
floatingActionButton:
_isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null,
);
}
Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
final image = contact.type.iconPath;
final currencyIcon = image != null
? Image.asset(image, height: 24, width: 24)
: const SizedBox(height: 24, width: 24);
return Column(
children: [
Container(
key: Key('${contact.name}'),
padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
currencyIcon,
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 12),
child: Text(
contact.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
))
],
),
),
StandardListSeparator()
],
);
}
ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.4,
children: [
SlidableAction(
onPressed: (_) async => await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact, arguments: contact),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: S.of(context).edit,
),
SlidableAction(
onPressed: (_) async {
final isDelete = await showAlertDialog(context);
if (isDelete) {
await widget.contactListViewModel.delete(contact);
}
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
],
);
Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) {
final filterIcon = Image.asset('assets/images/filter_icon.png',
color: Theme.of(context).appBarTheme.titleTextStyle!.color);
return MergeSemantics(
child: SizedBox(
height: 58,
width: 58,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
container: true,
child: GestureDetector(
onTap: () async {
await showPopUp<void>(
context: context,
builder: (context) => FilterListWidget(
initalType: contactListViewModel.orderType,
initalAscending: contactListViewModel.ascending,
onClose: (bool ascending, FilterListOrderType type) async {
contactListViewModel.setAscending(ascending);
await contactListViewModel.setOrderType(type);
},
),
);
},
child: Semantics(
label: 'Transaction Filter',
button: true,
enabled: true,
child: Container(
height: 36,
width: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor,
),
child: filterIcon,
),
),
),
),
),
),
);
}
Future<bool> showAlertDialog(BuildContext context) async { Future<bool> showAlertDialog(BuildContext context) async {
return await showPopUp<bool>( return await showPopUp<bool>(
context: context, context: context,
@ -190,32 +450,4 @@ class ContactListPage extends BasePage {
}) ?? }) ??
false; false;
} }
ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.4,
children: [
SlidableAction(
onPressed: (_) async => await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact, arguments: contact),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: S.of(context).edit,
),
SlidableAction(
onPressed: (_) async {
final isDelete = await showAlertDialog(context);
if (isDelete) {
await contactListViewModel.delete(contact);
}
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
],
);
} }

View file

@ -18,9 +18,9 @@ class FilterListWidget extends StatefulWidget {
required this.onClose, required this.onClose,
}); });
final WalletListOrderType? initalType; final FilterListOrderType? initalType;
final bool initalAscending; final bool initalAscending;
final Function(bool, WalletListOrderType) onClose; final Function(bool, FilterListOrderType) onClose;
@override @override
FilterListWidgetState createState() => FilterListWidgetState(); FilterListWidgetState createState() => FilterListWidgetState();
@ -28,7 +28,7 @@ class FilterListWidget extends StatefulWidget {
class FilterListWidgetState extends State<FilterListWidget> { class FilterListWidgetState extends State<FilterListWidget> {
late bool ascending; late bool ascending;
late WalletListOrderType? type; late FilterListOrderType? type;
@override @override
void initState() { void initState() {
@ -37,7 +37,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
type = widget.initalType; type = widget.initalType;
} }
void setSelectedOrderType(WalletListOrderType? orderType) { void setSelectedOrderType(FilterListOrderType? orderType) {
setState(() { setState(() {
type = orderType; type = orderType;
}); });
@ -72,7 +72,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
), ),
), ),
), ),
if (type != WalletListOrderType.Custom) ...[ if (type != FilterListOrderType.Custom) ...[
sectionDivider, sectionDivider,
SettingsChoicesCell( SettingsChoicesCell(
ChoicesListItem<ListOrderMode>( ChoicesListItem<ListOrderMode>(
@ -89,10 +89,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
], ],
sectionDivider, sectionDivider,
RadioListTile( RadioListTile(
value: WalletListOrderType.CreationDate, value: FilterListOrderType.CreationDate,
groupValue: type, groupValue: type,
title: Text( title: Text(
WalletListOrderType.CreationDate.toString(), FilterListOrderType.CreationDate.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16, fontSize: 16,
@ -104,10 +104,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
), ),
RadioListTile( RadioListTile(
value: WalletListOrderType.Alphabetical, value: FilterListOrderType.Alphabetical,
groupValue: type, groupValue: type,
title: Text( title: Text(
WalletListOrderType.Alphabetical.toString(), FilterListOrderType.Alphabetical.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16, fontSize: 16,
@ -119,10 +119,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
), ),
RadioListTile( RadioListTile(
value: WalletListOrderType.GroupByType, value: FilterListOrderType.GroupByType,
groupValue: type, groupValue: type,
title: Text( title: Text(
WalletListOrderType.GroupByType.toString(), FilterListOrderType.GroupByType.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16, fontSize: 16,
@ -134,10 +134,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
), ),
RadioListTile( RadioListTile(
value: WalletListOrderType.Custom, value: FilterListOrderType.Custom,
groupValue: type, groupValue: type,
title: Text( title: Text(
WalletListOrderType.Custom.toString(), FilterListOrderType.Custom.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 16, fontSize: 16,

View file

@ -7,13 +7,17 @@ class FilteredList extends StatefulWidget {
required this.list, required this.list,
required this.itemBuilder, required this.itemBuilder,
required this.updateFunction, required this.updateFunction,
this.canReorder = true,
this.shrinkWrap = false, this.shrinkWrap = false,
this.physics,
}); });
final ObservableList<dynamic> list; final ObservableList<dynamic> list;
final Widget Function(BuildContext, int) itemBuilder; final Widget Function(BuildContext, int) itemBuilder;
final Function updateFunction; final Function updateFunction;
final bool canReorder;
final bool shrinkWrap; final bool shrinkWrap;
final ScrollPhysics? physics;
@override @override
FilteredListState createState() => FilteredListState(); FilteredListState createState() => FilteredListState();
@ -22,21 +26,31 @@ class FilteredList extends StatefulWidget {
class FilteredListState extends State<FilteredList> { class FilteredListState extends State<FilteredList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Observer( if (widget.canReorder) {
builder: (_) => ReorderableListView.builder( return Observer(
shrinkWrap: widget.shrinkWrap, builder: (_) => ReorderableListView.builder(
physics: const BouncingScrollPhysics(), shrinkWrap: widget.shrinkWrap,
itemBuilder: widget.itemBuilder, physics: widget.physics ?? const BouncingScrollPhysics(),
itemCount: widget.list.length, itemBuilder: widget.itemBuilder,
onReorder: (int oldIndex, int newIndex) { itemCount: widget.list.length,
if (oldIndex < newIndex) { onReorder: (int oldIndex, int newIndex) {
newIndex -= 1; if (oldIndex < newIndex) {
} newIndex -= 1;
final dynamic item = widget.list.removeAt(oldIndex); }
widget.list.insert(newIndex, item); final dynamic item = widget.list.removeAt(oldIndex);
widget.updateFunction(); widget.list.insert(newIndex, item);
}, widget.updateFunction();
), },
); ),
);
} else {
return Observer(
builder: (_) => ListView.builder(
physics: widget.physics ?? const BouncingScrollPhysics(),
itemBuilder: widget.itemBuilder,
itemCount: widget.list.length,
),
);
}
} }
} }

View file

@ -59,7 +59,7 @@ class WalletListPage extends BasePage {
builder: (context) => FilterListWidget( builder: (context) => FilterListWidget(
initalType: walletListViewModel.orderType, initalType: walletListViewModel.orderType,
initalAscending: walletListViewModel.ascending, initalAscending: walletListViewModel.ascending,
onClose: (bool ascending, WalletListOrderType type) async { onClose: (bool ascending, FilterListOrderType type) async {
walletListViewModel.setAscending(ascending); walletListViewModel.setAscending(ascending);
await walletListViewModel.setOrderType(type); await walletListViewModel.setOrderType(type);
}, },

View file

@ -1,39 +0,0 @@
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:flutter/material.dart';
class CollapsibleSectionList extends SectionStandardList {
CollapsibleSectionList(
{required int sectionCount,
required int Function(int sectionIndex) itemCounter,
required Widget Function(int sectionIndex, int itemIndex) itemBuilder,
Widget Function(int sectionIndex)? sectionTitleBuilder,
bool hasTopSeparator = false})
: super(
hasTopSeparator: hasTopSeparator,
sectionCount: sectionCount,
itemCounter: itemCounter,
itemBuilder: itemBuilder,
sectionTitleBuilder: sectionTitleBuilder);
@override
Widget buildTitle(List<Widget> items, int sectionIndex) {
if (sectionTitleBuilder == null) {
throw Exception('Cannot to build title. sectionTitleBuilder is null');
}
return sectionTitleBuilder!.call(sectionIndex);
}
@override
List<Widget> buildSection(int itemCount, List<Widget> items, int sectionIndex) {
final List<Widget> section = [];
for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
final item = itemBuilder(sectionIndex, itemIndex);
section.add(StandardListSeparator());
section.add(item);
}
return section;
}
}

View file

@ -62,9 +62,11 @@ abstract class SettingsStoreBase with Store {
required bool initialAppSecure, required bool initialAppSecure,
required bool initialDisableBuy, required bool initialDisableBuy,
required bool initialDisableSell, required bool initialDisableSell,
required FilterListOrderType initialWalletListOrder,
required FilterListOrderType initialContactListOrder,
required bool initialDisableBulletin, required bool initialDisableBulletin,
required WalletListOrderType initialWalletListOrder,
required bool initialWalletListAscending, required bool initialWalletListAscending,
required bool initialContactListAscending,
required FiatApiMode initialFiatMode, required FiatApiMode initialFiatMode,
required bool initialAllowBiometricalAuthentication, required bool initialAllowBiometricalAuthentication,
required String initialTotpSecretKey, required String initialTotpSecretKey,
@ -149,7 +151,9 @@ abstract class SettingsStoreBase with Store {
disableSell = initialDisableSell, disableSell = initialDisableSell,
disableBulletin = initialDisableBulletin, disableBulletin = initialDisableBulletin,
walletListOrder = initialWalletListOrder, walletListOrder = initialWalletListOrder,
contactListOrder = initialContactListOrder,
walletListAscending = initialWalletListAscending, walletListAscending = initialWalletListAscending,
contactListAscending = initialContactListAscending,
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
exchangeStatus = initialExchangeStatus, exchangeStatus = initialExchangeStatus,
currentTheme = initialTheme, currentTheme = initialTheme,
@ -324,14 +328,24 @@ abstract class SettingsStoreBase with Store {
reaction( reaction(
(_) => walletListOrder, (_) => walletListOrder,
(WalletListOrderType walletListOrder) => (FilterListOrderType walletListOrder) =>
sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index)); sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index));
reaction(
(_) => contactListOrder,
(FilterListOrderType contactListOrder) =>
sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index));
reaction( reaction(
(_) => walletListAscending, (_) => walletListAscending,
(bool walletListAscending) => (bool walletListAscending) =>
sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending)); sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending));
reaction(
(_) => contactListAscending,
(bool contactListAscending) =>
sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending));
reaction( reaction(
(_) => autoGenerateSubaddressStatus, (_) => autoGenerateSubaddressStatus,
(AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
@ -645,15 +659,21 @@ abstract class SettingsStoreBase with Store {
@observable @observable
bool disableSell; bool disableSell;
@observable
FilterListOrderType contactListOrder;
@observable @observable
bool disableBulletin; bool disableBulletin;
@observable @observable
WalletListOrderType walletListOrder; FilterListOrderType walletListOrder;
@observable @observable
bool walletListAscending; bool walletListAscending;
@observable
bool contactListAscending;
@observable @observable
bool allowBiometricalAuthentication; bool allowBiometricalAuthentication;
@ -907,9 +927,13 @@ abstract class SettingsStoreBase with Store {
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
final walletListOrder = final walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
final contactListOrder =
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
final walletListAscending = final walletListAscending =
sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
final contactListAscending =
sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
final currentFiatApiMode = FiatApiMode.deserialize( final currentFiatApiMode = FiatApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ??
FiatApiMode.enabled.raw); FiatApiMode.enabled.raw);
@ -1200,6 +1224,8 @@ abstract class SettingsStoreBase with Store {
initialDisableBulletin: disableBulletin, initialDisableBulletin: disableBulletin,
initialWalletListOrder: walletListOrder, initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending, initialWalletListAscending: walletListAscending,
initialContactListOrder: contactListOrder,
initialContactListAscending: contactListAscending,
initialFiatMode: currentFiatApiMode, initialFiatMode: currentFiatApiMode,
initialAllowBiometricalAuthentication: allowBiometricalAuthentication, initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
initialCake2FAPresetOptions: selectedCake2FAPreset, initialCake2FAPresetOptions: selectedCake2FAPreset,
@ -1348,9 +1374,11 @@ abstract class SettingsStoreBase with Store {
disableBulletin = disableBulletin =
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder = walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
contactListOrder =
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
shouldShowMarketPlaceInDashboard = shouldShowMarketPlaceInDashboard =
sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
shouldShowMarketPlaceInDashboard; shouldShowMarketPlaceInDashboard;

View file

@ -1,18 +1,20 @@
import 'dart:async'; import 'dart:async';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/mobx.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/utils/mobx.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:collection/collection.dart';
part 'contact_list_view_model.g.dart'; part 'contact_list_view_model.g.dart';
@ -75,6 +77,8 @@ abstract class ContactListViewModelBase with Store {
_subscription = contactSource.bindToListWithTransform( _subscription = contactSource.bindToListWithTransform(
contacts, (Contact contact) => ContactRecord(contactSource, contact), contacts, (Contact contact) => ContactRecord(contactSource, contact),
initialFire: true); initialFire: true);
setOrderType(settingsStore.contactListOrder);
} }
String _createName(String walletName, String label, {int? key = null}) { String _createName(String walletName, String label, {int? key = null}) {
@ -93,6 +97,10 @@ abstract class ContactListViewModelBase with Store {
bool get isEditable => _currency == null; bool get isEditable => _currency == null;
FilterListOrderType? get orderType => settingsStore.contactListOrder;
bool get ascending => settingsStore.contactListAscending;
@computed @computed
bool get shouldRequireTOTP2FAForAddingContacts => bool get shouldRequireTOTP2FAForAddingContacts =>
settingsStore.shouldRequireTOTP2FAForAddingContacts; settingsStore.shouldRequireTOTP2FAForAddingContacts;
@ -118,4 +126,70 @@ abstract class ContactListViewModelBase with Store {
_currency?.toString() == element.type.tag || _currency?.toString() == element.type.tag ||
_currency?.tag == element.type.toString(); _currency?.tag == element.type.toString();
} }
void dispose() async {
_subscription?.cancel();
final List<Contact> contactsSourceCopy = contacts.map((e) => e.original).toList();
await reorderContacts(contactsSourceCopy);
}
void reorderAccordingToContactList() =>
settingsStore.contactListOrder = FilterListOrderType.Custom;
Future<void> reorderContacts(List<Contact> contactCopy) async {
await contactSource.deleteAll(contactCopy.map((e) => e.key).toList());
await contactSource.addAll(contactCopy);
}
Future<void> sortGroupByType() async {
List<Contact> contactsSourceCopy = contactSource.values.toList();
contactsSourceCopy.sort((a, b) => ascending
? a.type.toString().compareTo(b.type.toString())
: b.type.toString().compareTo(a.type.toString()));
await reorderContacts(contactsSourceCopy);
}
Future<void> sortAlphabetically() async {
List<Contact> contactsSourceCopy = contactSource.values.toList();
contactsSourceCopy
.sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name));
await reorderContacts(contactsSourceCopy);
}
Future<void> sortByCreationDate() async {
List<Contact> contactsSourceCopy = contactSource.values.toList();
contactsSourceCopy.sort((a, b) =>
ascending ? a.lastChange.compareTo(b.lastChange) : b.lastChange.compareTo(a.lastChange));
await reorderContacts(contactsSourceCopy);
}
void setAscending(bool ascending) => settingsStore.contactListAscending = ascending;
Future<void> setOrderType(FilterListOrderType? type) async {
if (type == null) return;
settingsStore.contactListOrder = type;
switch (type) {
case FilterListOrderType.CreationDate:
await sortByCreationDate();
break;
case FilterListOrderType.Alphabetical:
await sortAlphabetically();
break;
case FilterListOrderType.GroupByType:
await sortGroupByType();
break;
case FilterListOrderType.Custom:
default:
reorderAccordingToContactList();
break;
}
}
} }

View file

@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/execution_state.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
@ -17,7 +17,9 @@ abstract class ContactViewModelBase with Store {
_contact = contact, _contact = contact,
name = contact?.name ?? '', name = contact?.name ?? '',
address = contact?.address ?? '', address = contact?.address ?? '',
currency = contact?.type; currency = contact?.type,
lastChange = contact?.lastChange;
@observable @observable
ExecutionState state; ExecutionState state;
@ -31,6 +33,8 @@ abstract class ContactViewModelBase with Store {
@observable @observable
CryptoCurrency? currency; CryptoCurrency? currency;
DateTime? lastChange;
@computed @computed
bool get isReady => bool get isReady =>
name.isNotEmpty && name.isNotEmpty &&
@ -51,20 +55,32 @@ abstract class ContactViewModelBase with Store {
Future<void> save() async { Future<void> save() async {
try { try {
state = IsExecutingState(); state = IsExecutingState();
final now = DateTime.now();
if (doesContactNameExist(name)) {
state = FailureState(S.current.contact_name_exists);
return;
}
if (_contact != null && _contact!.original.isInBox) { if (_contact != null && _contact!.original.isInBox) {
_contact?.name = name; _contact?.name = name;
_contact?.address = address; _contact?.address = address;
_contact?.type = currency!; _contact?.type = currency!;
_contact?.lastChange = now;
await _contact?.save(); await _contact?.save();
} else { } else {
await _contacts await _contacts
.add(Contact(name: name, address: address, type: currency!)); .add(Contact(name: name, address: address, type: currency!, lastChange: now));
} }
lastChange = now;
state = ExecutedSuccessfullyState(); state = ExecutedSuccessfullyState();
} catch (e) { } catch (e) {
state = FailureState(e.toString()); state = FailureState(e.toString());
} }
} }
}
bool doesContactNameExist(String name) {
return _contacts.values.any((contact) => contact.name == name);
}
}

View file

@ -76,7 +76,7 @@ abstract class WalletListViewModelBase with Store {
await _appStore.changeCurrentWallet(wallet); await _appStore.changeCurrentWallet(wallet);
} }
WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder; FilterListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
bool get ascending => _appStore.settingsStore.walletListAscending; bool get ascending => _appStore.settingsStore.walletListAscending;
@ -108,7 +108,7 @@ abstract class WalletListViewModelBase with Store {
return; return;
} }
_appStore.settingsStore.walletListOrder = WalletListOrderType.Custom; _appStore.settingsStore.walletListOrder = FilterListOrderType.Custom;
// make a copy of the walletInfoSource: // make a copy of the walletInfoSource:
List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList(); List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store {
_appStore.settingsStore.walletListAscending = ascending; _appStore.settingsStore.walletListAscending = ascending;
} }
Future<void> setOrderType(WalletListOrderType? type) async { Future<void> setOrderType(FilterListOrderType? type) async {
if (type == null) return; if (type == null) return;
_appStore.settingsStore.walletListOrder = type; _appStore.settingsStore.walletListOrder = type;
switch (type) { switch (type) {
case WalletListOrderType.CreationDate: case FilterListOrderType.CreationDate:
await sortByCreationDate(); await sortByCreationDate();
break; break;
case WalletListOrderType.Alphabetical: case FilterListOrderType.Alphabetical:
await sortAlphabetically(); await sortAlphabetically();
break; break;
case WalletListOrderType.GroupByType: case FilterListOrderType.GroupByType:
await sortGroupByType(); await sortGroupByType();
break; break;
case WalletListOrderType.Custom: case FilterListOrderType.Custom:
default: default:
await reorderAccordingToWalletList(); await reorderAccordingToWalletList();
break; break;

View file

@ -937,5 +937,6 @@
"you_pay": "انت تدفع", "you_pay": "انت تدفع",
"you_will_get": "حول الى", "you_will_get": "حول الى",
"you_will_send": "تحويل من", "you_will_send": "تحويل من",
"yy": "YY" "yy": "YY",
} "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ"
}

View file

@ -937,5 +937,6 @@
"you_pay": "Вие плащате", "you_pay": "Вие плащате",
"you_will_get": "Обръщане в", "you_will_get": "Обръщане в",
"you_will_send": "Обръщане от", "you_will_send": "Обръщане от",
"yy": "гг" "yy": "гг",
} "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Zaplatíte", "you_pay": "Zaplatíte",
"you_will_get": "Směnit na", "you_will_get": "Směnit na",
"you_will_send": "Směnit z", "you_will_send": "Směnit z",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název."
}

View file

@ -940,5 +940,6 @@
"you_pay": "You Pay", "you_pay": "You Pay",
"you_will_get": "Convert to", "you_will_get": "Convert to",
"you_will_send": "Convert from", "you_will_send": "Convert from",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "A contact with that name already exists. Please choose a different name."
}

View file

@ -938,5 +938,6 @@
"you_pay": "Tú pagas", "you_pay": "Tú pagas",
"you_will_get": "Convertir a", "you_will_get": "Convertir a",
"you_will_send": "Convertir de", "you_will_send": "Convertir de",
"yy": "YY" "yy": "YY",
"contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente."
} }

View file

@ -937,5 +937,6 @@
"you_pay": "Vous payez", "you_pay": "Vous payez",
"you_will_get": "Convertir vers", "you_will_get": "Convertir vers",
"you_will_send": "Convertir depuis", "you_will_send": "Convertir depuis",
"yy": "AA" "yy": "AA",
} "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom."
}

View file

@ -939,5 +939,6 @@
"you_pay": "Ka Bayar", "you_pay": "Ka Bayar",
"you_will_get": "Maida zuwa", "you_will_get": "Maida zuwa",
"you_will_send": "Maida daga", "you_will_send": "Maida daga",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban."
}

View file

@ -939,5 +939,6 @@
"you_pay": "आप भुगतान करते हैं", "you_pay": "आप भुगतान करते हैं",
"you_will_get": "में बदलें", "you_will_get": "में बदलें",
"you_will_send": "से रूपांतरित करें", "you_will_send": "से रूपांतरित करें",
"yy": "वाईवाई" "yy": "वाईवाई",
} "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Vi plaćate", "you_pay": "Vi plaćate",
"you_will_get": "Razmijeni u", "you_will_get": "Razmijeni u",
"you_will_send": "Razmijeni iz", "you_will_send": "Razmijeni iz",
"yy": "GG" "yy": "GG",
} "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime."
}

View file

@ -940,5 +940,6 @@
"you_pay": "Anda Membayar", "you_pay": "Anda Membayar",
"you_will_get": "Konversi ke", "you_will_get": "Konversi ke",
"you_will_send": "Konversi dari", "you_will_send": "Konversi dari",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain."
}

View file

@ -940,5 +940,6 @@
"you_pay": "Tu paghi", "you_pay": "Tu paghi",
"you_will_get": "Converti a", "you_will_get": "Converti a",
"you_will_send": "Conveti da", "you_will_send": "Conveti da",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso."
}

View file

@ -938,5 +938,6 @@
"you_pay": "あなたが支払う", "you_pay": "あなたが支払う",
"you_will_get": "に変換", "you_will_get": "に変換",
"you_will_send": "から変換", "you_will_send": "から変換",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。"
}

View file

@ -939,5 +939,6 @@
"you_will_get": "로 변환하다", "you_will_get": "로 변환하다",
"you_will_send": "다음에서 변환", "you_will_send": "다음에서 변환",
"YY": "YY", "YY": "YY",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요."
}

View file

@ -937,5 +937,6 @@
"you_pay": "သင်ပေးချေပါ။", "you_pay": "သင်ပေးချေပါ။",
"you_will_get": "သို့ပြောင်းပါ။", "you_will_get": "သို့ပြောင်းပါ။",
"you_will_send": "မှပြောင်းပါ။", "you_will_send": "မှပြောင်းပါ။",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။"
}

View file

@ -938,5 +938,6 @@
"you_pay": "U betaalt", "you_pay": "U betaalt",
"you_will_get": "Converteren naar", "you_will_get": "Converteren naar",
"you_will_send": "Converteren van", "you_will_send": "Converteren van",
"yy": "JJ" "yy": "JJ",
} "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Płacisz", "you_pay": "Płacisz",
"you_will_get": "Konwertuj na", "you_will_get": "Konwertuj na",
"you_will_send": "Konwertuj z", "you_will_send": "Konwertuj z",
"yy": "RR" "yy": "RR",
} "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę."
}

View file

@ -941,4 +941,4 @@
"you_will_get": "Converter para", "you_will_get": "Converter para",
"you_will_send": "Converter de", "you_will_send": "Converter de",
"yy": "aa" "yy": "aa"
} }

View file

@ -938,5 +938,6 @@
"you_pay": "Вы платите", "you_pay": "Вы платите",
"you_will_get": "Конвертировать в", "you_will_get": "Конвертировать в",
"you_will_send": "Конвертировать из", "you_will_send": "Конвертировать из",
"yy": "ГГ" "yy": "ГГ",
} "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя."
}

View file

@ -937,5 +937,6 @@
"you_pay": "คุณจ่าย", "you_pay": "คุณจ่าย",
"you_will_get": "แปลงเป็น", "you_will_get": "แปลงเป็น",
"you_will_send": "แปลงจาก", "you_will_send": "แปลงจาก",
"yy": "ปี" "yy": "ปี",
} "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น"
}

View file

@ -938,4 +938,4 @@
"you_will_get": "I-convert sa", "you_will_get": "I-convert sa",
"you_will_send": "I-convert mula sa", "you_will_send": "I-convert mula sa",
"yy": "YY" "yy": "YY"
} }

View file

@ -937,5 +937,6 @@
"you_pay": "Şu kadar ödeyeceksin: ", "you_pay": "Şu kadar ödeyeceksin: ",
"you_will_get": "Biçimine dönüştür:", "you_will_get": "Biçimine dönüştür:",
"you_will_send": "Biçiminden dönüştür:", "you_will_send": "Biçiminden dönüştür:",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin."
}

View file

@ -938,5 +938,6 @@
"you_pay": "Ви платите", "you_pay": "Ви платите",
"you_will_get": "Конвертувати в", "you_will_get": "Конвертувати в",
"you_will_send": "Конвертувати з", "you_will_send": "Конвертувати з",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я."
}

View file

@ -939,5 +939,6 @@
"you_pay": "تم ادا کرو", "you_pay": "تم ادا کرو",
"you_will_get": "میں تبدیل کریں۔", "you_will_get": "میں تبدیل کریں۔",
"you_will_send": "سے تبدیل کریں۔", "you_will_send": "سے تبدیل کریں۔",
"yy": "YY" "yy": "YY",
} "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ"
}

View file

@ -938,5 +938,6 @@
"you_pay": "Ẹ sàn", "you_pay": "Ẹ sàn",
"you_will_get": "Ṣe pàṣípààrọ̀ sí", "you_will_get": "Ṣe pàṣípààrọ̀ sí",
"you_will_send": "Ṣe pàṣípààrọ̀ láti", "you_will_send": "Ṣe pàṣípààrọ̀ láti",
"yy": "Ọd" "yy": "Ọd",
} "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ."
}

View file

@ -937,5 +937,6 @@
"you_pay": "你付钱", "you_pay": "你付钱",
"you_will_get": "转换到", "you_will_get": "转换到",
"you_will_send": "转换自", "you_will_send": "转换自",
"yy": "YY" "yy": "YY",
} "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。"
}