fix: save hive issue

This commit is contained in:
Serhii 2025-06-22 21:01:00 +03:00
parent 66c49f795b
commit 8dc3bb649d
27 changed files with 1411 additions and 1295 deletions

View file

@ -30,7 +30,6 @@ class AuthService with Store {
Routes.modify2FAPage,
Routes.newWallet,
Routes.newWalletType,
Routes.addressBookAddContact,
Routes.restoreOptions,
];

View file

@ -32,12 +32,12 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/src/screens/address_book/contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_addresses_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
@ -977,48 +977,66 @@ Future<void> setup({
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, List<dynamic>?>(
(contact, initialContactParams) => ContactViewModel(
_contactSource,
contact: contact,
initialParams: initialContactParams ?? <dynamic>[],
),
getIt.registerFactoryParam<ContactViewModel, AddressEditRequest?, void>(
(req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!, request: req),
);
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) =>
ContactListViewModel(_contactSource, _walletInfoSource, getIt
.get<AppStore>()
.wallet!, cur, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactoryParam<EditNewContactGroupPage, ParsedAddress, void>(
(ParsedAddress parsedAddress, _) => EditNewContactGroupPage(selectedParsedAddress: parsedAddress,contacts: _contactSource));
getIt.registerFactoryParam<EditAddressesPage, ContactRecord, void>(
(ContactRecord contact, _) => EditAddressesPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactoryParam<EditAddressPage, List<dynamic>, void>(
(List<dynamic> args, _) {
final contact = args.first as ContactRecord?;
final currency = args[1] as CryptoCurrency?;
final label = args[2] as String?;
return EditAddressPage(
contactViewModel: getIt.get<ContactViewModel>(param1: contact, param2: [currency, label]),
(cur, _) => ContactListViewModel(
_contactSource,
_walletInfoSource,
getIt<AppStore>().wallet!,
cur,
getIt<SettingsStore>(),
),
);
});
getIt.registerFactoryParam<EditNewContactPage, ContactRecord, void>(
(ContactRecord contact, _) => EditNewContactPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
(cur, _) => ContactListPage(
getIt.get<ContactListViewModel>(param1: cur),
getIt<AuthService>(),
),
);
getIt.registerFactoryParam<EditContactPage, ContactRecord, void>(
(ContactRecord contact, _) => EditContactPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact)));
(contact, _) => EditContactPage(
contactViewModel: getIt.get<ContactViewModel>(
param1: AddressEditRequest.contact(contact),
),
),
);
getIt.registerFactoryParam<EditAddressPage, AddressEditRequest, void>(
(request, _) => EditAddressPage(
contactViewModel: getIt.get<ContactViewModel>(param1: request),
),
);
getIt.registerFactoryParam<EditContactGroupPage, ContactViewModel, void>(
(vm, _) => EditContactGroupPage(contactViewModel: vm),
);
getIt.registerFactoryParam<EditNewContactGroupPage, ParsedAddress, ContactRecord?>(
(parsedAddress, record) {
final vm = getIt<ContactViewModel>(
param1: AddressEditRequest.contact(record),
);
return EditNewContactGroupPage(
selectedParsedAddress: parsedAddress,
contactViewModel : vm,
);
},
);
getIt.registerFactoryParam<EditNewContactPage, ContactRecord?, void>(
(contact, _) => EditNewContactPage(
contactViewModel: getIt.get<ContactViewModel>(param1: AddressEditRequest.contact(contact)),
),
);
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart';
@ -10,21 +12,152 @@ part 'contact.g.dart';
class Contact extends HiveObject with Keyable {
Contact({
required this.name,
required this.parsedAddresses,
required this.manualAddresses,
this.address = '',
CryptoCurrency? type,
Map<String, Map<int, Map<String, String>>> parsedByHandle = const {},
Map<int, Map<String, String>> manualAddresses = const {},
Map<String, String> extraBlobs = const {},
AddressSource source = AddressSource.notParsed,
this.handle = '',
this.imagePath = '',
this.profileName = '',
this.description = '',
DateTime? lastChange,
}) : sourceRaw = source.raw,
}) : raw = type?.raw ?? 0,
_parsedJson = _encode(parsedByHandle),
_manualJson = _encode(manualAddresses),
extraJsonBlobs = extraBlobs,
sourceRaw = source.raw,
lastChange = lastChange ?? DateTime.now();
static const typeId = CONTACT_TYPE_ID;
static const boxName = 'Contacts';
@HiveField(0, defaultValue: '')
String name;
@HiveField(1, defaultValue: '')
String address;
@HiveField(2, defaultValue: 0)
int raw;
@HiveField(3)
DateTime lastChange;
@HiveField(4, defaultValue: '')
String _parsedJson;
@HiveField(5, defaultValue: '')
String _manualJson;
@HiveField(6, defaultValue: '')
String handle;
@HiveField(7, defaultValue: '')
String imagePath;
@HiveField(8, defaultValue: '')
String profileName;
@HiveField(9, defaultValue: '')
String description;
@HiveField(10, defaultValue: 0)
int sourceRaw;
@HiveField(11, defaultValue: {})
Map<String, String> extraJsonBlobs;
AddressSource get source => AddressSourceIndex.fromRaw(sourceRaw);
set source(AddressSource v) => sourceRaw = v.raw;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
set type(CryptoCurrency v) => raw = v.raw;
Map<String, Map<int, Map<String, String>>> get parsedByHandle => _decodeParsed(_parsedJson);
set parsedByHandle(Map<String, Map<int, Map<String, String>>> v) => _parsedJson = _encode(v);
Map<int, Map<String, String>> get manualAddresses => _decodeManual(_manualJson);
set manualAddresses(Map<int, Map<String, String>> v) => _manualJson = _encode(v);
Map<CryptoCurrency, Map<String, String>> get manualByCurrency => manualAddresses.map(
(k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v),
);
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
final out = <CryptoCurrency, Map<String, String>>{};
for (final block in parsedByHandle.values) {
block.forEach((curRaw, lblMap) {
final cur = CryptoCurrency.deserialize(raw: curRaw);
out.putIfAbsent(cur, () => {})..addAll(lblMap);
});
}
return out;
}
@override
dynamic get keyIndex => key;
@override
bool operator ==(Object o) => o is Contact && o.key == key;
@override
int get hashCode => key.hashCode;
static String _encode(Object value) => jsonEncode(_stringifyKeys(value));
static dynamic _stringifyKeys(dynamic obj) {
if (obj is Map) {
return obj.map(
(k, v) => MapEntry(k.toString(), _stringifyKeys(v)),
);
}
if (obj is Iterable) return obj.map(_stringifyKeys).toList();
return obj;
}
static Map<String, Map<int, Map<String, String>>> _decodeParsed(String s) {
if (s.isEmpty) return {};
final Map<String, dynamic> data = jsonDecode(s) as Map<String, dynamic>;
return data.map((handle, byCur) {
final inner = (byCur as Map<String, dynamic>).map((curRaw, lblMap) {
final int cur = int.parse(curRaw);
final labels = (lblMap as Map).cast<String, String>();
return MapEntry(cur, labels);
});
return MapEntry(handle, inner);
});
}
static Map<int, Map<String, String>> _decodeManual(String s) {
if (s.isEmpty) return {};
final Map<String, dynamic> data = jsonDecode(s) as Map<String, dynamic>;
return data.map((curRaw, lblMap) {
final int cur = int.parse(curRaw);
final labels = (lblMap as Map).cast<String, String>();
return MapEntry(cur, labels);
});
}
factory Contact.fromParsed(ParsedAddress p, {String? localImage}) {
final manual = <int, Map<String, String>>{};
p.manualAddressByCurrencyMap?.forEach(
(cur, addr) => manual[cur.raw] = {cur.title: addr},
);
final parsed = <String, Map<int, Map<String, String>>>{};
if (p.parsedAddressByCurrencyMap.isNotEmpty) {
final hKey = '${p.addressSource.label}-${p.handle}';
parsed[hKey] = {
for (final e in p.parsedAddressByCurrencyMap.entries) e.key.raw: {e.key.title: e.value}
};
}
return Contact(
name: p.profileName.isNotEmpty ? p.profileName : p.handle,
profileName: p.profileName,
@ -32,88 +165,8 @@ class Contact extends HiveObject with Keyable {
description: p.description,
source: p.addressSource,
imagePath: localImage ?? '',
parsedAddresses: {
if (p.parsedAddressByCurrencyMap.isNotEmpty)
for (final e in p.parsedAddressByCurrencyMap.entries) e.key.raw: {e.key.title: e.value}
},
manualAddresses: {
if (p.manualAddressByCurrencyMap != null && p.manualAddressByCurrencyMap!.isNotEmpty)
for (final e in p.manualAddressByCurrencyMap!.entries) e.key.raw: {e.key.title: e.value}
},
manualAddresses: manual,
parsedByHandle: parsed,
);
}
@HiveField(0, defaultValue: '')
String name;
@HiveField(1, defaultValue: {})
Map<int, Map<String, String>> parsedAddresses;
@HiveField(2, defaultValue: 0)
int sourceRaw;
@HiveField(3, defaultValue: '')
String handle;
@HiveField(4, defaultValue: '')
String imagePath;
@HiveField(5, defaultValue: '')
String profileName;
@HiveField(6, defaultValue: '')
String description;
@HiveField(7)
DateTime lastChange;
@HiveField(8, defaultValue: {})
Map<int, Map<String, String>> manualAddresses;
void setAddress(
{required CryptoCurrency currency,
required String label,
required String address,
bool isManual = false}) {
final target = isManual ? manualAddresses : parsedAddresses;
target.putIfAbsent(currency.raw, () => <String, String>{});
final inner = target[currency.raw]!;
final baseLabel = (label.trim().isEmpty ? currency.title : label).trim();
final uniqueLabel = _getUniqueLabel(baseLabel, inner);
inner[uniqueLabel] = address;
lastChange = DateTime.now();
save();
}
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency =>
parsedAddresses.map((k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v));
Map<CryptoCurrency, Map<String, String>> get manualByCurrency =>
manualAddresses.map((k, v) => MapEntry(CryptoCurrency.deserialize(raw: k), v));
AddressSource get source => AddressSourceIndex.fromRaw(sourceRaw);
set source(AddressSource source) => sourceRaw = source.raw;
@override
dynamic get keyIndex => key;
String _getUniqueLabel(String base, Map<String, String> byLabel) {
if (!byLabel.containsKey(base)) return base;
var i = 1;
while (byLabel.containsKey('$base $i')) i++;
return base + '_' + '$i';
}
@override
bool operator ==(Object other) => other is Contact && other.key == key;
@override
int get hashCode => key.hashCode;
}

View file

@ -14,117 +14,143 @@ part 'contact_record.g.dart';
class ContactRecord = ContactRecordBase with _$ContactRecord;
abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
ContactRecordBase(Box<Contact> source, Contact original)
ContactRecordBase(Box<Contact> box, Contact original)
: name = original.name,
handle = original.handle,
profileName = original.profileName,
description = original.description,
imagePath = original.imagePath,
sourceType = original.source,
parsedAddresses = ObservableMap.of(original.parsedByCurrency),
manualAddresses = ObservableMap.of(original.manualByCurrency),
super(source, original);
manual = ObservableMap.of(original.manualByCurrency),
parsedBlocks = ObservableMap.of({
for (final h in original.parsedByHandle.entries)
h.key: {
for (final cur in h.value.entries)
CryptoCurrency.deserialize(raw: cur.key): Map<String, String>.of(cur.value)
}
}),
super(box, original);
@observable
String name;
@observable
String handle;
@observable
String profileName;
@observable
String description;
@observable
String imagePath;
String name, handle, profileName, description, imagePath;
@observable
AddressSource sourceType;
String address = '';
CryptoCurrency type = CryptoCurrency.btc;
@observable
ObservableMap<CryptoCurrency, Map<String, String>> manual;
@observable
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddresses;
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks;
@observable
ObservableMap<CryptoCurrency, Map<String, String>> manualAddresses;
@override
void toBind(Contact original) {
reaction((_) => name, (v) => original.name = v);
reaction((_) => handle, (v) => original.handle = v);
reaction((_) => profileName, (v) => original.profileName = v);
reaction((_) => description, (v) => original.description = v);
reaction((_) => imagePath, (v) => original.imagePath = v);
reaction((_) => sourceType, (v) => original.source = v);
bool _different(Map<String, String>? inner, String lbl, String addr) =>
inner == null || inner[lbl] != addr;
reaction((_) => Map.of(parsedAddresses), (_) {
parsedAddresses.forEach((cur, byLabel) {
byLabel.forEach((lbl, addr) {
final inner = original.parsedAddresses[cur.raw];
if (_different(inner, lbl, addr)) {
original.setAddress(
currency: cur,
label: lbl,
address: addr,
isManual: false,
);
}
});
});
});
reaction((_) => Map.of(manualAddresses), (_) {
manualAddresses.forEach((cur, byLabel) {
byLabel.forEach((lbl, addr) {
final inner = original.manualAddresses[cur.raw];
if (_different(inner, lbl, addr)) {
original.setAddress(
currency: cur,
label: lbl,
address: addr,
isManual: true,
);
}
});
});
});
}
@override
void fromBind(Contact original) {
name = original.name;
handle = original.handle;
profileName = original.profileName;
description = original.description;
imagePath = original.imagePath;
sourceType = original.source;
parsedAddresses = ObservableMap.of({
for (final e in original.parsedByCurrency.entries) e.key: Map<String, String>.of(e.value)
});
manualAddresses = ObservableMap.of({
for (final e in original.manualByCurrency.entries) e.key: Map<String, String>.of(e.value)
@computed
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
final out = <CryptoCurrency, Map<String, String>>{};
parsedBlocks.forEach((_, byCur) {
byCur.forEach((cur, lbl) => out.putIfAbsent(cur, () => {})..addAll(lbl));
});
return out;
}
@computed
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
@computed
ImageProvider get avatarProvider {
final f = avatarFile;
return (f != null && f.existsSync())
? FileImage(f)
ImageProvider get avatarProvider => (avatarFile?.existsSync() ?? false)
? FileImage(avatarFile!)
: const AssetImage('assets/images/profile.png');
@override
void toBind(Contact c) {
reaction((_) => name, (v) => c.name = v);
reaction((_) => handle, (v) => c.handle = v);
reaction((_) => profileName, (v) => c.profileName = v);
reaction((_) => description, (v) => c.description = v);
reaction((_) => imagePath, (v) => c.imagePath = v);
reaction((_) => sourceType, (v) => c.source = v);
}
@override
void fromBind(Contact c) {
name = c.name;
handle = c.handle;
profileName = c.profileName;
description = c.description;
imagePath = c.imagePath;
sourceType = c.source;
}
@action
void setParsedAddress(CryptoCurrency cur, String label, String addr) {
final oldInner = parsedAddresses[cur] ?? {};
parsedAddresses[cur] = {...oldInner, label: addr};
void setManualAddress(CryptoCurrency cur, String label, String addr) {
manual.putIfAbsent(cur, () => {})[label] = addr;
_flushManual();
}
@action
void removeManualAddress(CryptoCurrency cur, String label) {
final map = manual[cur];
if (map == null) return;
map.remove(label);
if (map.isEmpty) manual.remove(cur);
_flushManual();
}
@action
void setParsedAddress(String blockKey, CryptoCurrency cur, String label, String addr) {
final block = parsedBlocks.putIfAbsent(blockKey, () => {});
block.putIfAbsent(cur, () => {})[label] = addr;
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
_flushParsed();
}
@action
void removeParsedAddress(String blockKey, CryptoCurrency? cur, String? label) {
final block = parsedBlocks[blockKey];
if (block == null) return;
if (cur == null) {
parsedBlocks.remove(blockKey);
_flushParsed();
return;
}
final map = block[cur];
if (map == null) return;
if (label == null) {
block.remove(cur);
} else {
map.remove(label);
if (map.isEmpty) block.remove(cur);
}
if (block.isEmpty) {
parsedBlocks.remove(blockKey);
} else {
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
}
_flushParsed();
}
void _flushManual() {
original
..manualAddresses = {
for (final e in manual.entries) e.key.raw: Map<String, String>.of(e.value)
}
..lastChange = DateTime.now();
}
void _flushParsed() {
original
..parsedByHandle = {
for (final h in parsedBlocks.entries)
h.key: {for (final cur in h.value.entries) cur.key.raw: Map<String, String>.of(cur.value)}
}
..lastChange = DateTime.now();
}
@override
String address = '';
@override
CryptoCurrency type = CryptoCurrency.btc;
}

View file

@ -432,7 +432,10 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
final address = _item["address"] as String;
final name = _item["name"] as String;
return Contact(parsedAddresses: {5 : {'label':address} //TODO fix this hardcoded value
return Contact(parsedByHandle: {'handle':
{
5: {'label': address} //TODO fix this hardcoded value
}
}, manualAddresses: {}, name: name);
});

View file

@ -86,6 +86,19 @@ extension AddressSourceIndex on AddressSource {
AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
}
extension AddressSourceNameParser on AddressSource {
static AddressSource fromLabel(String? text) {
if (text == null || text.trim().isEmpty) {
return AddressSource.notParsed;
}
final needle = text.trim().toLowerCase();
return AddressSource.values.firstWhere(
(src) => src.label.toLowerCase() == needle,
orElse: () => AddressSource.notParsed,
);
}
}
class ParsedAddress {
const ParsedAddress({
required this.parsedAddressByCurrencyMap,

View file

@ -13,7 +13,6 @@ import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/contact_page.dart';
import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
@ -579,11 +578,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.pickerWalletAddress:
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
case Routes.addressBookAddContact:
return handleRouteWithPlatformAwareness(
(context) => getIt.get<ContactPage>(param1: settings.arguments as ContactRecord?),
);
case Routes.showKeys:
return handleRouteWithPlatformAwareness(
(context) => getIt.get<WalletKeysPage>(),

View file

@ -34,10 +34,10 @@ class Routes {
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
static const pickerWalletAddress = '/picker_wallet_address';
static const addressBookAddContact = '/address_book_add_contact';
static const newContactWelcomePage = '/new_contact_welcome_page';
static const supportedHandlesPage = '/supported_handles_page';
static const editNewContactGroupPage = '/edit_new_contact_group_page';
static const editContactGroupPage = '/edit_contact_group_page';
static const editAddressesPage = '/edit_addresses_page';
static const editAddressPage = '/edit_address_page';
static const editNewContactPage = '/edit_new_contact_page';

View file

@ -1,192 +0,0 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/contact_name_validator.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
class ContactPage extends BasePage {
ContactPage(this.contactViewModel);
// : _formKey = GlobalKey<FormState>(),
// _nameController = TextEditingController(),
// _addressController = TextEditingController(),
// _currencyTypeController = TextEditingController() {
// _nameController.text = contactViewModel.name;
// _addressController.text = contactViewModel.addresses.values.first;
// _nameController.addListener(() => contactViewModel.name = _nameController.text);
// _addressController.addListener(() => contactViewModel.address = _addressController.text);
//
// autorun((_) => _currencyTypeController.text = contactViewModel.currency?.toString() ?? '');
// }
@override
String get title => S.current.contact;
final ContactViewModel contactViewModel;
// final GlobalKey<FormState> _formKey;
// final TextEditingController _nameController;
// final TextEditingController _currencyTypeController;
// final TextEditingController _addressController;
bool _isEffectsApplied = false;
@override
Widget body(BuildContext context) {
final downArrow = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Theme.of(context).colorScheme.primary,
height: 8,
);
_setEffects(context);
return Container();
// Observer(
// builder: (_) => ScrollableWithBottomSection(
// contentPadding: EdgeInsets.all(24),
// content: Form(
// key: _formKey,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// BaseTextFormField(
// controller: _nameController,
// hintText: S.of(context).contact_name,
// validator: ContactNameValidator(),
// ),
// Padding(
// padding: EdgeInsets.only(top: 20),
// child: Container(
// child: InkWell(
// onTap: () => _presentCurrencyPicker(context),
// child: IgnorePointer(
// child: BaseTextFormField(
// controller: _currencyTypeController,
// hintText: S.of(context).settings_currency,
// suffixIcon: Row(
// mainAxisSize: MainAxisSize.min,
// mainAxisAlignment: MainAxisAlignment.end,
// children: <Widget>[
// downArrow,
// SizedBox(width: 16),
// ],
// ),
// ),
// ),
// ),
// ),
// ),
// if (contactViewModel.currency != null)
// Padding(
// padding: EdgeInsets.only(top: 20),
// child: AddressTextField(
// controller: _addressController,
// options: [
// AddressTextFieldOption.paste,
// AddressTextFieldOption.qrCode,
// ],
// buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest,
// iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
// validator: AddressValidator(type: contactViewModel.currency!),
// ),
// )
// ],
// ),
// ),
// bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
// bottomSection: Row(
// children: <Widget>[
// Expanded(
// child: PrimaryButton(
// onPressed: () {
// contactViewModel.reset();
// _nameController.text = '';
// _addressController.text = '';
// },
// text: S.of(context).reset,
// color: Theme.of(context).colorScheme.surfaceContainer,
// textColor: Theme.of(context).colorScheme.onSecondaryContainer,
// ),
// ),
// SizedBox(width: 20),
// Expanded(
// child: Observer(
// builder: (_) => PrimaryButton(
// onPressed: () async {
// if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
// return;
// }
//
// await contactViewModel.save();
// },
// text: S.of(context).save,
// color: Theme.of(context).colorScheme.primary,
// textColor: Theme.of(context).colorScheme.onPrimary,
// isDisabled: !contactViewModel.isReady,
// ),
// ),
// )
// ],
// )),
// );
}
void _presentCurrencyPicker(BuildContext context) {
// showPopUp<void>(
// builder: (_) => CurrencyPicker(
// selectedAtIndex: contactViewModel.currency != null
// ? contactViewModel.currencies.indexOf(contactViewModel.currency!)
// : -1,
// items: contactViewModel.currencies,
// title: S.of(context).please_select,
// hintText: S.of(context).search_currency,
// onItemSelected: (Currency item) => contactViewModel.currency = item as CryptoCurrency),
// context: context,
// );
}
void _onContactSavingFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.contact,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
}
void _onContactSavedSuccessfully(BuildContext context) => Navigator.of(context).pop();
void _setEffects(BuildContext context) {
if (_isEffectsApplied) {
return;
}
_isEffectsApplied = true;
reaction((_) => contactViewModel.state, (ExecutionState state) {
if (state is FailureState) {
_onContactSavingFailure(context, state.error);
}
if (state is ExecutedSuccessfullyState) {
_onContactSavedSuccessfully(context);
}
});
}
}

View file

@ -1,11 +1,14 @@
import 'dart:async';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
@ -14,24 +17,36 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum _InputMode { handle, address }
class NewContactWelcomePage extends BasePage {
NewContactWelcomePage({required this.onSearch});
NewContactWelcomePage({required this.onSearch, this.handleOnly = false, this.existingContact});
final Future<List<ParsedAddress>> Function(String query) onSearch;
final bool handleOnly;
final ContactRecord? existingContact;
@override
Widget body(BuildContext context) =>
NewContactWelcomePageBody(currentTheme: currentTheme, onSearch: onSearch);
Widget body(BuildContext context) => NewContactWelcomePageBody(
currentTheme: currentTheme,
onSearch: onSearch,
handleOnly: handleOnly,
existingContact: existingContact);
}
class NewContactWelcomePageBody extends StatefulWidget {
const NewContactWelcomePageBody({required this.currentTheme, required this.onSearch});
const NewContactWelcomePageBody(
{required this.currentTheme,
required this.onSearch,
required this.handleOnly,
required this.existingContact});
final MaterialThemeBase currentTheme;
final Future<List<ParsedAddress>> Function(String query) onSearch;
final bool handleOnly;
final ContactRecord? existingContact;
@override
State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState();
@ -130,6 +145,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
}
Widget _segmentedSwitcher(BuildContext ctx) {
if (widget.handleOnly) return const SizedBox.shrink();
final txt = Theme.of(ctx).textTheme.bodyMedium;
final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>(
value: m,
@ -150,13 +166,8 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
return SizedBox(
width: double.infinity,
child: SegmentedButton<_InputMode>(
segments: [
seg(_InputMode.handle, 'handle'),
seg(_InputMode.address, 'address'),
],
selected: <_InputMode>{
_mode
},
segments: [seg(_InputMode.handle, 'handle'), seg(_InputMode.address, 'address')],
selected: <_InputMode>{_mode},
showSelectedIcon: false,
style: ButtonStyle(
shape: WidgetStatePropertyAll(
@ -191,51 +202,40 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
final hasDropdown = _results.isNotEmpty || _isSearching;
final border = OutlineInputBorder(
borderRadius: BorderRadius.vertical(
top: const Radius.circular(12),
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
);
final isHandleMode = widget.handleOnly ? true : _mode == _InputMode.handle;
final rounded = BorderRadius.vertical(
top: const Radius.circular(12),
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
);
final noStroke = OutlineInputBorder(borderRadius: rounded, borderSide: BorderSide.none);
final isHandleMode = _mode == _InputMode.handle;
return Theme(
data: Theme.of(context).copyWith(
inputDecorationTheme: InputDecorationTheme(
border: noStroke,
focusedBorder: noStroke,
enabledBorder: noStroke,
disabledBorder: border,
errorBorder: border,
focusedErrorBorder: border,
prefixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0),
),
),
child: BaseTextFormField(
key: ValueKey(_mode),
return StandardTextFormFieldWidget(
focusNode: _focusNode,
controller: isHandleMode ? _handleCtl : _addressCtl,
labelText: isHandleMode ? 'Enter handle' : 'Enter address',
fillColor: fillColor,
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()),
prefixIcon: isHandleMode
? null
: Padding(
padding: const EdgeInsets.all(8.0),
child: _currencyPrefix(context),
),
focusNode: _focusNode,
fillColor: fillColor,
hintText: isHandleMode ? 'Enter handle' : 'Enter address',
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()),
textStyle: Theme.of(context).textTheme.bodyMedium!,
suffixIcon: RoundedIconButton(
icon: Icons.paste_outlined,
onPressed: () async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text?.trim() ?? '';
if (text.isEmpty) return;
final isHandleMode = widget.handleOnly || _mode == _InputMode.handle;
if (isHandleMode) {
_handleCtl.text = text;
_handleChanged(text);
} else {
_addressCtl.text = text;
setState(() => _typedAddress = text);
}
},
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))),
),
addressValidator: AddressValidator(type: _selectedCurrency),
);
}
@ -311,7 +311,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
},
),
),
_mode == _InputMode.handle
widget.handleOnly || _mode == _InputMode.handle
? InkWell(
splashFactory: NoSplash.splashFactory,
onTap: () {
@ -346,13 +346,14 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
onPressed: () {
_focusNode.unfocus();
if (_mode == _InputMode.handle) {
Navigator.pushNamed(context, Routes.editNewContactGroupPage,
arguments: _selected);
Navigator.pushNamed(
context,
Routes.editNewContactGroupPage,
arguments: [_selected!, widget.existingContact],
);
_selected = null;
} else {
Navigator.pushNamed(context,
Routes.editNewContactGroupPage,
arguments: ParsedAddress(
final parsed = ParsedAddress(
parsedAddressByCurrencyMap: {},
manualAddressByCurrencyMap: {
_selectedCurrency: _typedAddress.trim(),
@ -362,7 +363,13 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
profileName: '',
profileImageUrl: 'assets/images/profile.png',
description: '',
));
);
Navigator.pushNamed(
context,
Routes.editNewContactGroupPage,
arguments: [parsed, null],
);
}
},
color: Theme.of(context).colorScheme.primary,
@ -370,11 +377,12 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
height: 40,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: _mode == _InputMode.handle
isDisabled: widget.handleOnly
? _selected == null || _isSearching
: _typedAddress.isEmpty,
),
)
: (_mode == _InputMode.handle
? _selected == null || _isSearching
: _typedAddress.isEmpty),
)),
],
),
),

View file

@ -1,75 +1,38 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class EditAddressPage extends BasePage {
EditAddressPage({required this.contactViewModel})
: _formKey = GlobalKey<FormState>(),
manualAddress = contactViewModel.manualAddressesByCurrency[contactViewModel.initialCurrency]
?[contactViewModel.manualLabel] ??
'',
_labelController = TextEditingController(),
_manualAddressController = TextEditingController() {
_labelController.text = contactViewModel.manualLabel;
_manualAddressController.text = contactViewModel.isNewAddress ? '' : manualAddress ?? '';
_labelController.addListener(() => contactViewModel.manualLabel = _labelController.text);
_manualAddressController
.addListener(() => contactViewModel.manualAddress = _manualAddressController.text);
_labelController = TextEditingController(text: contactViewModel.label),
_addressController = TextEditingController(text: contactViewModel.address) {
_labelController.addListener(() => contactViewModel.label = _labelController.text);
_addressController.addListener(() => contactViewModel.address = _addressController.text);
}
@override
String get title => contactViewModel.isNewAddress ? 'Add Address' : 'Edit Address';
String get title => contactViewModel.isAddressEdit ? 'Edit Address' : 'Add Address';
final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey;
final TextEditingController _labelController;
final TextEditingController _manualAddressController;
final String? manualAddress;
Widget _circleIcon(
{required BuildContext context,
required IconData icon,
required VoidCallback onPressed,
ShapeBorder? shape,
double? width,
double? height,
double? iconSize,
Color? fillColor}) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: BoxConstraints.tightFor(width: width ?? 24, height: height ?? 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface),
);
}
final TextEditingController _addressController;
@override
Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
: CustomThemeColors.backgroundGradientColorLight;
@ -90,7 +53,7 @@ class EditAddressPage extends BasePage {
title: Text(initialCurrency.fullName ?? initialCurrency.name,
style: Theme.of(context).textTheme.bodyMedium),
trailing: Icon(Icons.keyboard_arrow_down_outlined,
size: 24, color: theme.colorScheme.onSurface),
size: 24, color: Theme.of(context).colorScheme.onSurface),
tileColor: fillColor,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
@ -100,113 +63,57 @@ class EditAddressPage extends BasePage {
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
onTap: () => _presentCurrencyPicker(context, contactViewModel),
),
onTap: () => _presentCurrencyPicker(context, contactViewModel)),
const SizedBox(height: 8),
TextField(
StandardTextFormFieldWidget(
controller: _labelController,
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: 'Address label',
labelStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
hintStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
fillColor: fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: theme.colorScheme.outline)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Colors.transparent),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: _circleIcon(
context: context,
icon: Icons.copy_all_outlined,
onPressed: () {},
suffixIcon: RoundedIconButton(
icon: Icons.paste_outlined,
onPressed: () async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text ?? '';
if (text.trim().isEmpty) return;
_labelController.text = text.trim();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)))),
),
suffixIconConstraints: const BoxConstraints(
minWidth: 34,
maxWidth: 34,
minHeight: 24,
maxHeight: 24,
)),
style: theme.textTheme.bodyMedium,
onChanged: (value) {},
addressValidator: (value) {
if (value == null || value.trim().isEmpty) return 'Label cannot be empty';
return null;
},
),
const SizedBox(height: 8),
TextFormField(
controller: _manualAddressController,
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: 'Address',
labelStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
hintStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
StandardTextFormFieldWidget(
controller: _addressController,
labelText: S.of(context).address,
fillColor: fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: theme.colorScheme.outline)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Colors.transparent),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: _circleIcon(
context: context,
icon: Icons.copy_all_outlined,
onPressed: () {},
suffixIcon: RoundedIconButton(
icon: Icons.paste_outlined,
onPressed: () async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text ?? '';
if (text.trim().isEmpty) return;
_addressController.text = text.trim();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)))),
),
suffixIconConstraints: const BoxConstraints(
minWidth: 34,
maxWidth: 34,
minHeight: 24,
maxHeight: 24,
)),
style: theme.textTheme.bodyMedium,
onChanged: (value) {},
validator: AddressValidator(type: contactViewModel.currency),
),
addressValidator: AddressValidator(type: contactViewModel.currency)),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
if (!contactViewModel.isNewAddress)
if (contactViewModel.isAddressEdit)
Padding(
padding: const EdgeInsets.only(right: 8),
child: _circleIcon(
context: context,
child: RoundedIconButton(
icon: Icons.delete_outline_rounded,
onPressed: () async {
contactViewModel.deleteManualAddress(
initialCurrency, contactViewModel.manualLabel);
contactViewModel.updateManualAddress();
await contactViewModel.save();
contactViewModel.reset();
_manualAddressController.clear();
Navigator.of(context, rootNavigator: true).pop();
await contactViewModel.deleteCurrentAddress();
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
@ -219,10 +126,7 @@ class EditAddressPage extends BasePage {
Expanded(
child: ElevatedButton(
onPressed: () {
contactViewModel.reset();
_labelController.clear();
_manualAddressController.clear();
Navigator.of(context, rootNavigator: true).pop();
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: fillColor,
@ -245,12 +149,12 @@ class EditAddressPage extends BasePage {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
contactViewModel.updateManualAddress();
await contactViewModel.save();
if (context.mounted && contactViewModel.state is! FailureState) {
Navigator.of(context, rootNavigator: true).pop();
if (contactViewModel.mode == ContactEditMode.manualAddress) {
await contactViewModel.saveManualAddress();
} else {
await contactViewModel.saveParsedAddress();
}
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,

View file

@ -1,184 +0,0 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
class EditAddressesPage extends BasePage {
EditAddressesPage({required this.contactViewModel});
final ContactViewModel contactViewModel;
Widget _circleIcon(BuildContext context, IconData icon, VoidCallback onPressed) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
@override
Widget leading(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.favorite_border_outlined, () {}),
const SizedBox(width: 8),
_circleIcon(context, Icons.refresh_sharp, () {}),
],
),
);
}
@override
Widget middle(BuildContext context) {
return Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatarProvider,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Text(
contactViewModel.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
color: titleColor(context),
),
),
],
),
);
}
@override
Widget trailing(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.add, () {}),
const SizedBox(width: 8),
_circleIcon(context, Icons.edit, () {}),
],
),
);
}
@override
Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
: CustomThemeColors.backgroundGradientColorLight;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ContactAddressesExpansionTile(
// key: ValueKey(contactViewModel.name),
// manualByCurrency: contact.manualAddresses,
// fillColor: Theme.of(context).colorScheme.surfaceContainer,
// title: _buildContactTitle(
// context: context,
// contact: contact,
// contactListViewModel: widget.contactListViewModel),
// onEditPressed: (cur, lbl) async {
// await _showAddressBookBottomSheet(
// context: context,
// contactListViewModel: widget.contactListViewModel,
// initialRoute: Routes.editAddressPage,
// initialArgs: [contact, cur, lbl],
// );
// },
// onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
// ),
//
//
// ContactAddressesExpansionTile(
// key: ValueKey(contactViewModel.name),
// title: Text('Manual Addresses'),
// fillColor: fillColor,
// addresses: contactViewModel.manualAddressesByCurrency),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 1),
decoration: BoxDecoration(
color: fillColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageUtil.getImageFromPath(
imagePath: contactViewModel.sourceType.iconPath, height: 24, width: 24),
const SizedBox(width: 6),
Expanded(
child: Text(
contactViewModel.sourceType.label + ' - ' + contactViewModel.handle,
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 6),
_circleIcon(context, Icons.edit, () {}),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Addresses detected:', style: theme.textTheme.bodyMedium),
const SizedBox(width: 8),
Expanded(
child: Wrap(
spacing: 8,
children: contactViewModel.parsedAddressesByCurrency.keys
.map((currency) => currency.iconPath != null
? Image.asset(currency.iconPath!, height: 24, width: 24)
: const SizedBox.shrink())
.toList(),
),
),
],
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,264 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/standard_text_form_field_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class EditContactGroupPage extends BasePage {
EditContactGroupPage({
required this.contactViewModel,
}) : _formKey = GlobalKey<FormState>(),
_groupLabelCtl = TextEditingController(text: contactViewModel.name) {
_groupLabelCtl.addListener(() {
contactViewModel.name = _groupLabelCtl.text;
});
}
final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey;
final TextEditingController _groupLabelCtl;
@override
String? get title => 'Edit Contact';
@override
Widget body(BuildContext context) {
final theme = Theme.of(context);
final fillColor = currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
return LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
reverse: true,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
//edit avatar
},
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 44,
maxWidth: 44,
minHeight: 44,
maxHeight: 44,
),
child: Container(
decoration: BoxDecoration(
color: fillColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 1),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatar,
fit: BoxFit.cover,
),
),
const SizedBox(height: 1),
Text('Icon',
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 8,
color: theme.colorScheme.onSurfaceVariant,
)),
],
),
),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Form(
key: _formKey,
child: StandardTextFormFieldWidget(
controller: _groupLabelCtl,
labelText: 'Address group name',
fillColor: fillColor,
addressValidator: (value) {
// final text = value?.trim() ?? '';
// if (text.isEmpty) return 'Name cannot be empty';
//
// final clash = contactViewModel.box.values.any(
// (c) =>
// c.name.toLowerCase() == text.toLowerCase() &&
// c.key != contactViewModel.contactRecord?.original.key,
// );
// return clash ? 'Group with this name already exists' : null;
},
),
),
),
],
),
),
),
contactViewModel.userHandles.isEmpty
? Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'No alias services found',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
)
: Observer(builder: (_) {
final userHandlesList = contactViewModel.userHandles.toList();
return Column(
children: [
const SizedBox(height: 24),
Align(
alignment: Alignment.centerLeft,
child: Text(
'Alias Services',
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(height: 4),
ListView.separated(
shrinkWrap: true,
primary: false,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: userHandlesList.length,
separatorBuilder: (context, index) => const SizedBox(height: 6),
itemBuilder: (context, index) {
final item = userHandlesList[index];
return ListTile(
title: Text(item.src?.label ?? '',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
)),
subtitle: Text(item.label,
style: Theme.of(context).textTheme.bodySmall),
trailing: RoundedIconButton(
icon: Icons.delete_outline_rounded,
onPressed: () {
contactViewModel.deleteParsedBlock(item.handleKey);
},
iconSize: 20,
width: 28,
height: 28),
tileColor: fillColor,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
leading: ImageUtil.getImageFromPath(
imagePath: item.src?.iconPath ?? '',
height: 24,
width: 24),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
);
},
),
],
);
}),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: RoundedIconButton(
icon: Icons.delete_outline_rounded,
onPressed: () async {
await contactViewModel.deleteContact();
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
width: 40,
height: 40,
iconSize: 30,
fillColor: Theme.of(context).colorScheme.errorContainer),
),
Expanded(
child: ElevatedButton(
onPressed: () {
if (context.mounted) Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: fillColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
S.of(context).cancel,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () async {
if (_formKey.currentState != null &&
!_formKey.currentState!.validate()) {
return;
}
contactViewModel.name = _groupLabelCtl.text;
await contactViewModel.saveContactInfo();
if (context.mounted) Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
S.of(context).save,
maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
],
),
),
],
),
),
),
);
});
}
}

View file

@ -1,46 +1,28 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class EditContactPage extends BasePage {
EditContactPage({required this.contactViewModel});
final ContactViewModel contactViewModel;
Widget _circleIcon(BuildContext context, IconData icon, VoidCallback onPressed) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
@override
Widget leading(BuildContext context) {
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.favorite_border_outlined, () {}),
],
),
);
}
@override
Widget middle(BuildContext context) {
return Center(
return Observer(
builder: (_) => Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -49,10 +31,8 @@ class EditContactPage extends BasePage {
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatarProvider,
fit: BoxFit.cover,
),
),
image: contactViewModel.avatar,
fit: BoxFit.cover)),
const SizedBox(width: 12),
Text(
contactViewModel.name,
@ -64,18 +44,37 @@ class EditContactPage extends BasePage {
),
],
),
),
);
}
@override
Widget trailing(BuildContext context) {
final onHandlerSearch = (query) async {
final address = await getIt
.get<AddressResolverService>()
.resolve(query: query as String, wallet: contactViewModel.wallet);
return address;
};
return SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(context, Icons.add, () {}),
RoundedIconButton(icon: Icons.add, onPressed: () {
Navigator.pushNamed(
context,
Routes.newContactWelcomePage,
arguments: [onHandlerSearch, true, contactViewModel.record],
);
}),
const SizedBox(width: 8),
_circleIcon(context, Icons.edit, () {}),
RoundedIconButton(icon: Icons.edit, onPressed: () {
Navigator.pushNamed(
context,
Routes.editContactGroupPage,
arguments: contactViewModel,
);
}),
],
),
);
@ -98,32 +97,46 @@ class EditContactPage extends BasePage {
key: ValueKey(contactViewModel.name),
title: Text('Manual Addresses'),
fillColor: fillColor,
manualByCurrency: contactViewModel.manualAddressesByCurrency,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
manualByCurrency: contactViewModel.manual,
onCopyPressed: (address) async =>
await Clipboard.setData(ClipboardData(text: address)),
onEditPressed: (cur, lbl) {
Navigator.pushNamed(
context,
Routes.editAddressPage,
arguments: [
contactViewModel.contactRecord,
cur,
lbl,
],
arguments: AddressEditRequest.address(
contact: contactViewModel.record,
currency: cur,
label: lbl,
kindIsManual: true,
),
);
},
),
const SizedBox(height: 8),
ContactAddressesExpansionTile(
key: ValueKey(contactViewModel.name + '_Parsed'),
...contactViewModel.parsedBlocks.entries.map((entry) {
final String handle = entry.key;
final stringSrc = handle.split('-').first;
final Map<CryptoCurrency, Map<String, String>> byCurrency = entry.value;
final src = AddressSourceNameParser.fromLabel(stringSrc);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ContactAddressesExpansionTile(
key: ValueKey('${contactViewModel.name}'),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageUtil.getImageFromPath(
imagePath: contactViewModel.sourceType.iconPath, height: 24, width: 24),
imagePath: src.iconPath,
height: 24,
width: 24,
),
const SizedBox(width: 6),
Expanded(
child: Text(
contactViewModel.sourceType.label + ' - ' + contactViewModel.handle,
handle,
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
@ -133,20 +146,25 @@ class EditContactPage extends BasePage {
],
),
fillColor: fillColor,
manualByCurrency: contactViewModel.parsedAddressesByCurrency,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
manualByCurrency: byCurrency,
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
onEditPressed: (cur, lbl) {
Navigator.pushNamed(
context,
Routes.editAddressPage,
arguments: [
contactViewModel.contactRecord,
cur,
lbl,
],
arguments: AddressEditRequest.address(
contact: contactViewModel.record,
currency: cur,
label: lbl,
kindIsManual: false,
handle: handle,
handleKey: entry.key,
),
);
},
),
);
}).toList(),
],
),
),

View file

@ -6,20 +6,20 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
class EditNewContactGroupPage extends BasePage {
EditNewContactGroupPage({
required this.selectedParsedAddress,
required this.contacts,
required this.contactViewModel,
}) : _formKey = GlobalKey<FormState>(),
_groupLabelCtl = TextEditingController(
text: selectedParsedAddress.profileName ?? '',
);
final ParsedAddress selectedParsedAddress;
final Box<Contact> contacts;
final ContactViewModel contactViewModel;
final GlobalKey<FormState> _formKey;
final TextEditingController _groupLabelCtl;
@ -127,7 +127,7 @@ class EditNewContactGroupPage extends BasePage {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Name cannot be empty';
final clash = contacts.values.any(
final clash = contactViewModel.box.values.any(
(c) => c.name.toLowerCase() == text.toLowerCase(),
);
return clash ? 'Group with this name already exists' : null;
@ -149,22 +149,46 @@ class EditNewContactGroupPage extends BasePage {
height: 40,
onPressed: () async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final localImg =
await ImageUtil.saveAvatarLocally(selectedParsedAddress.profileImageUrl);
final contact = Contact.fromParsed(
selectedParsedAddress.copyWith(profileName: _groupLabelCtl.text.trim()),
localImage: localImg);
contacts.add(contact);
final contactRecord = ContactRecord(
contacts,
contact,
if (contactViewModel.record != null) {
final record = contactViewModel.record!;
final handleKey =
'${selectedParsedAddress.addressSource.label}-${selectedParsedAddress.handle}'
.trim();
selectedParsedAddress.parsedAddressByCurrencyMap.forEach((cur, addr) {
record.setParsedAddress(
handleKey,
cur,
cur.title,
addr.trim(),
);
});
if (context.mounted) {
Navigator.of(context).pushNamed(
Routes.editNewContactPage,
arguments: [contactRecord],
Navigator.of(context, rootNavigator: true).pop();
}
} else {
final localImg = await ImageUtil.saveAvatarLocally(
selectedParsedAddress.profileImageUrl);
final newContact = Contact.fromParsed(
selectedParsedAddress.copyWith(
profileName: _groupLabelCtl.text.trim(),
),
localImage: localImg,
);
contactViewModel.box.add(newContact);
final record = ContactRecord(contactViewModel.box, newContact);
if (context.mounted) {
Navigator.pushNamed(
context,
Routes.editNewContactPage,
arguments: record,
);
}
}
},
color: Theme.of(context).colorScheme.primary,

View file

@ -1,14 +1,11 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -56,7 +53,7 @@ class EditNewContactPage extends BasePage {
child: Image(
width: 24,
height: 24,
image: contactViewModel.avatarProvider,
image: contactViewModel.avatar,
fit: BoxFit.cover,
),
),
@ -141,9 +138,11 @@ class EditNewContactPage extends BasePage {
Expanded(
child: Wrap(
spacing: 8,
children: contactViewModel.parsedAddressesByCurrency.keys
.map((currency) => currency.iconPath != null
? Image.asset(currency.iconPath!, height: 24, width: 24)
children: contactViewModel.parsedBlocks.values
.expand((map) => map.keys)
.toSet()
.map((CryptoCurrency cur) => cur.iconPath != null
? Image.asset(cur.iconPath!, height: 24, width: 24)
: const SizedBox.shrink())
.toList(),
),
@ -159,17 +158,18 @@ class EditNewContactPage extends BasePage {
key: ValueKey(contactViewModel.name),
title: Text('Manual Addresses'),
fillColor: fillColor,
manualByCurrency: contactViewModel.manualAddressesByCurrency,
manualByCurrency: contactViewModel.manual,
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
onEditPressed: (cur, lbl) {
Navigator.pushNamed(
context,
Routes.editAddressPage,
arguments: [
contactViewModel.contactRecord,
cur,
lbl,
],
arguments: AddressEditRequest.address(
contact: contactViewModel.record,
currency: cur,
label: lbl,
kindIsManual: false,
)
);
},
),

View file

@ -0,0 +1,52 @@
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cw_core/crypto_currency.dart';
enum EditMode {
contactFields,
manualAddressAdd,
manualAddressEdit,
parsedAddressAdd,
parsedAddressEdit,
}
class AddressEditRequest {
factory AddressEditRequest.contact(ContactRecord? c) => AddressEditRequest._(
contact: c,
mode: EditMode.contactFields,
);
factory AddressEditRequest.address({
required ContactRecord? contact,
required CryptoCurrency currency,
String? label,
required bool kindIsManual,
final String? handle,
String? handleKey,
}) =>
AddressEditRequest._(
contact: contact,
currency: currency,
label: label,
kindIsManual: kindIsManual,
handleKey: handleKey,
mode: label == null
? (kindIsManual ? EditMode.manualAddressAdd : EditMode.parsedAddressAdd)
: (kindIsManual ? EditMode.manualAddressEdit : EditMode.parsedAddressEdit),
);
const AddressEditRequest._({
this.contact,
this.currency,
this.label,
this.kindIsManual = false,
this.handleKey,
required this.mode,
});
final ContactRecord? contact;
final CryptoCurrency? currency;
final String? label;
final bool kindIsManual;
final EditMode mode;
final String? handleKey;
}

View file

@ -0,0 +1,23 @@
import 'package:cake_wallet/entities/parsed_address.dart';
class UserHandles {
factory UserHandles({required String handleKey}) {
assert(handleKey.isNotEmpty, 'handleKey cannot be empty');
final dash = handleKey.indexOf('-');
final prefix = dash == -1 ? handleKey : handleKey.substring(0, dash);
final label =
dash == -1 || dash == handleKey.length - 1 ? handleKey : handleKey.substring(dash + 1);
final src = AddressSourceNameParser.fromLabel(prefix);
return UserHandles._(handleKey, label, src);
}
const UserHandles._(this.handleKey, this.label, this.src);
final String handleKey;
final String label;
final AddressSource? src;
}

View file

@ -1,7 +1,7 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/handles_list_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart';
class SupportedHandlesPage extends BasePage {
@ -20,44 +20,4 @@ class SupportedHandlesPage extends BasePage {
}
}
class HandlesListWidget extends StatelessWidget {
const HandlesListWidget({
super.key,
required this.items,
required this.fillColor,
});
final List<AddressSource> items;
final Color fillColor;
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
itemCount: items.length,
separatorBuilder: (context, index) => const SizedBox(height: 6),
itemBuilder: (context, index) {
final src = items[index];
return ListTile(
title: Text(src.label, style: Theme
.of(context)
.textTheme
.bodyMedium),
trailing: Text(src.alias, style: Theme
.of(context)
.textTheme
.bodyMedium),
tileColor: fillColor,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
leading: ImageUtil.getImageFromPath(imagePath: src.iconPath, height: 24, width: 24),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
onTap: () {},
);
},
);
}
}

View file

@ -0,0 +1,46 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart';
class HandlesListWidget extends StatelessWidget {
const HandlesListWidget({
super.key,
required this.items,
required this.fillColor,
});
final List<AddressSource> items;
final Color fillColor;
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
primary: false,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
itemCount: items.length,
separatorBuilder: (context, index) => const SizedBox(height: 6),
itemBuilder: (context, index) {
final src = items[index];
return ListTile(
title: Text(src.label, style: Theme
.of(context)
.textTheme
.bodyMedium),
trailing: Text(src.alias, style: Theme
.of(context)
.textTheme
.bodyMedium),
tileColor: fillColor,
dense: true,
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
leading: ImageUtil.getImageFromPath(imagePath: src.iconPath, height: 24, width: 24),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
);
},
);
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class RoundedIconButton extends StatelessWidget {
const RoundedIconButton(
{required this.icon,
required this.onPressed,
this.shape,
this.width,
this.height,
this.iconSize,
this.fillColor});
final IconData icon;
final VoidCallback onPressed;
final ShapeBorder? shape;
final double? width;
final double? height;
final double? iconSize;
final Color? fillColor;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: BoxConstraints.tightFor(width: width ?? 24, height: height ?? 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface),
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class StandardTextFormFieldWidget extends StatelessWidget {
const StandardTextFormFieldWidget({
super.key,
required this.controller,
required this.labelText,
required this.fillColor,
required this.addressValidator,
this.focusNode,
this.suffixIcon,
this.prefixIcon,
this.suffix,
this.onChanged,
});
final TextEditingController controller;
final String labelText;
final Color fillColor;
final String? Function(String?)? addressValidator;
final FocusNode? focusNode;
final Widget? suffixIcon;
final Widget? prefixIcon;
final Widget? suffix;
final void Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
labelText: labelText,
labelStyle:
Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).hintColor),
hintStyle:
Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).hintColor),
fillColor: fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
borderSide: BorderSide(color: Colors.transparent),
),
suffixIcon: Padding(padding: const EdgeInsets.only(right: 10), child: suffixIcon),
suffix: suffix,
prefixIcon: prefixIcon,
suffixIconConstraints: const BoxConstraints(
minWidth: 34,
maxWidth: 34,
minHeight: 24,
maxHeight: 24,
)),
style: Theme.of(context).textTheme.bodyMedium,
onChanged: onChanged,
validator: addressValidator,
);
}
}

View file

@ -6,7 +6,9 @@ import 'package:cake_wallet/entities/parse_address_from_domain.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/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/addresses_expansion_tile_widget.dart';
import 'package:cake_wallet/src/screens/address_book/widgets/rounded_icon_button.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';
@ -63,19 +65,8 @@ class ContactListPage extends BasePage {
// FIX-ME: Style
//shape: CircleBorder(),
onPressed: () async {
if (contactListViewModel.shouldRequireTOTP2FAForAddingContacts) {
authService.authenticateAction(
context,
route: Routes.addressBookAddContact,
conditionToDetermineIfToUse2FA:
contactListViewModel.shouldRequireTOTP2FAForAddingContacts,
);
} else {
//await Navigator.of(context).pushNamed(Routes.addressBookAddContact); //TODO remove old flow
await _showAddressBookBottomSheet(
context: context, contactListViewModel: contactListViewModel);
}
},
child: Offstage(),
),
@ -340,7 +331,7 @@ class _ContactListBodyState extends State<ContactListBody> {
child: ContactAddressesExpansionTile(
key: Key(contact.key.toString()),
contentPadding: EdgeInsets.symmetric(horizontal: 8),
manualByCurrency: contact.manualAddresses,
manualByCurrency: contact.manual,
fillColor: Theme.of(context).colorScheme.surfaceContainer,
title: _buildContactTitle(
context: context,
@ -351,7 +342,12 @@ class _ContactListBodyState extends State<ContactListBody> {
context: context,
contactListViewModel: widget.contactListViewModel,
initialRoute: Routes.editAddressPage,
initialArgs: [contact, cur, lbl],
initialArgs: AddressEditRequest.address(
contact: contact,
currency: cur,
label: lbl,
kindIsManual: true,
),
);
},
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
@ -394,17 +390,20 @@ class _ContactListBodyState extends State<ContactListBody> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
_circleIcon(
context: context,
RoundedIconButton(
icon: Icons.add,
onPressed: () async => await _showAddressBookBottomSheet(
context: context,
contactListViewModel: contactListViewModel,
initialRoute: Routes.editAddressPage,
initialArgs: [contact, null, null])),
initialArgs: AddressEditRequest.address(
contact: contact,
currency: walletTypeToCryptoCurrency(widget.contactListViewModel.wallet.type),
label: null,
kindIsManual: true,
))),
const SizedBox(width: 8),
_circleIcon(
context: context,
RoundedIconButton(
icon: Icons.edit,
onPressed: () async => await _showAddressBookBottomSheet(
context: context,
@ -464,24 +463,6 @@ class _ContactListBodyState extends State<ContactListBody> {
),
);
}
Widget _circleIcon(
{required BuildContext context,
required IconData icon,
required VoidCallback onPressed,
ShapeBorder? shape}) {
final colorScheme = Theme.of(context).colorScheme;
return RawMaterialButton(
onPressed: onPressed,
fillColor: colorScheme.surfaceContainerHighest,
elevation: 0,
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: shape ?? const CircleBorder(),
child: Icon(icon, size: 14, color: colorScheme.onSurface),
);
}
}
Future<void> _showAddressBookBottomSheet(

View file

@ -612,64 +612,13 @@ class SendPage extends BasePage {
// newContactAddress = null;
// }
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
// bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null
? InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.doubleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
actionPanel: Padding(
padding: const EdgeInsets.only(left: 34.0),
child: Row(
children: [
SimpleCheckbox(
onChanged: (value) =>
sendViewModel.setShowAddressBookPopup(!value)),
const SizedBox(width: 8),
Text(
'Dont ask me next time',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.titleLarge!.color,
decoration: TextDecoration.none,
),
),
],
),
),
doubleActionLeftButtonText: 'No',
doubleActionRightButtonText: 'Yes',
onLeftActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
onRightActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
: InfoBottomSheet(
return InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,

View file

@ -4,13 +4,16 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/address_book/contact_welcome_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_address_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_addresses_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_group_page.dart';
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_page.dart';
import 'package:cake_wallet/src/screens/address_book/new_contact_welcome_page.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/supported_handles_page.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:flutter/material.dart';
@ -108,24 +111,40 @@ class _AddContactNavigator extends StatelessWidget {
case Routes.supportedHandlesPage:
page = SupportedHandlesPage();
break;
case Routes.newContactWelcomePage:
final list = args as List<dynamic>;
final onSearch = list[0] as Future<List<ParsedAddress>> Function(String);
final handleOnly = list.length > 1 && list[1] == true;
final contact = list.length > 2 ? list[2] as ContactRecord? : null;
page = NewContactWelcomePage(
onSearch : onSearch,
handleOnly : handleOnly,
existingContact: contact,
);
break;
case Routes.editNewContactGroupPage:
page = getIt<EditNewContactGroupPage>(param1: args as ParsedAddress);
final list = args as List<dynamic>;
page = getIt<EditNewContactGroupPage>(
param1: list[0] as ParsedAddress,
param2: list.length > 1 ? list[1] as ContactRecord? : null,
);
break;
case Routes.editContactGroupPage:
final vm = args as ContactViewModel;
page = getIt<EditContactGroupPage>(param1: vm);
break;
case Routes.editNewContactPage:
final list = args as List<dynamic>;
page = getIt<EditNewContactPage>(param1: list.first as ContactRecord?);
break;
case Routes.editAddressesPage:
page = getIt<EditAddressesPage>(param1: args as ContactRecord);
page = getIt<EditNewContactPage>(param1: args as ContactRecord);
break;
case Routes.editContactPage:
page = getIt<EditContactPage>(param1: args as ContactRecord);
break;
case Routes.editAddressPage:
page = getIt<EditAddressPage>(param1: args as List<dynamic>);
page = getIt<EditAddressPage>(param1: args as AddressEditRequest);
break;
default:
page = NewContactWelcomePage(onSearch: onHandlerSearch);
page = NewContactWelcomePage(onSearch: onHandlerSearch, handleOnly: false);
}
return MaterialPageRoute(

View file

@ -1,237 +1,272 @@
import 'dart:io';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/screens/address_book/entities/address_edit_request.dart';
import 'package:cake_wallet/src/screens/address_book/entities/user_handles.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cw_core/crypto_currency.dart';
part 'contact_view_model.g.dart';
class ContactViewModel = ContactViewModelBase with _$ContactViewModel;
enum ContactEditMode {
contactInfo,
manualAddress,
parsedAddress,
}
abstract class ContactViewModelBase with Store {
ContactViewModelBase(this._box, {ContactRecord? contact, required List<dynamic>? initialParams,})
: state = InitialExecutionState(),
class ContactViewModel = _ContactViewModel with _$ContactViewModel;
abstract class _ContactViewModel with Store {
_ContactViewModel(
this.box,
this.wallet, {
AddressEditRequest? request,
}) : mode = request?.mode == EditMode.manualAddressAdd ||
request?.mode == EditMode.manualAddressEdit
? ContactEditMode.manualAddress
: request?.mode == EditMode.parsedAddressAdd ||
request?.mode == EditMode.parsedAddressEdit
? ContactEditMode.parsedAddress
: ContactEditMode.contactInfo,
record = request?.contact,
currencies = CryptoCurrency.all,
contactRecord = contact,
name = contact?.name ?? '',
handle = contact?.handle ?? '',
profileName = contact?.profileName ?? '',
description = contact?.description ?? '',
imagePath = contact?.imagePath ?? '',
sourceType = contact?.sourceType ?? AddressSource.notParsed,
state = InitialExecutionState(),
name = request?.contact?.name ?? '',
handle = request?.contact?.handle ?? '',
profileName = request?.contact?.profileName ?? '',
description = request?.contact?.description ?? '',
imagePath = request?.contact?.imagePath ?? '',
sourceType = request?.contact?.sourceType ?? AddressSource.notParsed,
currency = request?.currency ?? CryptoCurrency.xmr,
label = request?.label ?? '',
address = '',
handleKey = request?.handleKey ?? '' {
_initMapsFromRecord();
currency = (initialParams != null &&
initialParams.isNotEmpty &&
initialParams[0] is CryptoCurrency)
? initialParams[0] as CryptoCurrency
: CryptoCurrency.xmr,
if (request?.label != null && record != null) {
currency = request!.currency!;
label = request.label!;
address = _targetMap[currency]?[label] ?? '';
initialCurrency = (initialParams != null &&
initialParams.isNotEmpty &&
initialParams[0] is CryptoCurrency)
? initialParams[0] as CryptoCurrency
_rememberOriginal(
blockKey: mode == ContactEditMode.parsedAddress
? (request.handleKey ?? _defaultHandleKey())
: null,
manualLabel = (initialParams != null &&
initialParams.length > 1 &&
initialParams[1] is String)
? initialParams[1] as String
: '',
isNewAddress = !(initialParams != null &&
initialParams.isNotEmpty &&
initialParams[0] is CryptoCurrency) {
const _emptyParsed = <CryptoCurrency, Map<String, String>>{};
const _emptyManual = <CryptoCurrency, Map<String, String>>{};
final parsedRaw = contact?.parsedAddresses ?? _emptyParsed;
final manualRaw = contact?.manualAddresses ?? _emptyManual;
parsedAddressesByCurrency = ObservableMap.of({
for (final e in parsedRaw.entries)
e.key: Map<String, String>.of(e.value)
});
manualAddressesByCurrency = ObservableMap.of({
for (final e in manualRaw.entries)
e.key: Map<String, String>.of(e.value)
});
);
}
}
final Box<Contact> box;
final WalletBase wallet;
ContactRecord? record;
@observable
ExecutionState state;
@observable
String name;
@observable
String handle;
@observable
String profileName;
@observable
String description;
@observable
String imagePath;
String name, handle, profileName, description, imagePath;
@observable
AddressSource sourceType;
@observable
CryptoCurrency currency;
@observable
String manualAddress = '';
@observable
String manualLabel = '';
String label, address, handleKey;
@observable
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddressesByCurrency = ObservableMap<
CryptoCurrency,
Map<String, String>>();
ObservableMap<CryptoCurrency, Map<String, String>> manual = ObservableMap();
@observable
ObservableMap<CryptoCurrency, Map<String, String>> manualAddressesByCurrency = ObservableMap<
CryptoCurrency,
Map<String, String>>();
ObservableMap<CryptoCurrency, Map<String, String>> parsed = ObservableMap();
@observable
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks = ObservableMap();
final Box<Contact> _box;
final ContactRecord? contactRecord;
final ContactEditMode mode;
final List<CryptoCurrency> currencies;
late final bool isNewAddress;
CryptoCurrency? initialCurrency;
CryptoCurrency? _originalCur;
String? _originalLabel, _originalAddress, _originalHandleKey;
@computed
bool get isReady =>
name
.trim()
.isNotEmpty && parsedAddressesByCurrency.isNotEmpty;
bool get isReady => name.trim().isNotEmpty || manual.isNotEmpty || parsed.isNotEmpty;
ImageProvider get avatarProvider {
final file = avatarFile;
return (file != null && file.existsSync())
? FileImage(file)
: const AssetImage('assets/images/profile.png');
@computed
List<UserHandles> get userHandles =>
parsedBlocks.keys.map((k) => UserHandles(handleKey: k)).toList();
@computed
ImageProvider get avatar => imagePath.isEmpty
? const AssetImage('assets/images/profile.png')
: FileImage(File(imagePath));
bool get isAddressEdit =>
mode != ContactEditMode.contactInfo && record != null && (_originalLabel ?? '').isNotEmpty;
ObservableMap<CryptoCurrency, Map<String, String>> get _targetMap =>
mode == ContactEditMode.manualAddress
? manual
: parsed[currency] != null
? parsed
: manual;
@action
Future<void> saveContactInfo() async {
if (record != null) {
record!
..name = name.trim()
..handle = handle.trim()
..profileName = profileName.trim()
..description = description.trim()
..imagePath = imagePath
..sourceType = sourceType;
record!.original..lastChange = DateTime.now();
await record!.original.save();
state = ExecutedSuccessfullyState();
return;
}
final newContact = Contact(
name: name.trim(),
address: '',
)
..handle = handle.trim()
..profileName = profileName.trim()
..description = description.trim()
..imagePath = imagePath
..source = sourceType
..lastChange = DateTime.now();
await box.put(newContact.key, newContact);
record = ContactRecord(box, newContact);
state = ExecutedSuccessfullyState();
}
@action
void updateManualAddress() {
if (manualAddress
.trim()
.isEmpty) return;
Future<void> saveManualAddress() async {
_ensureRecord();
final inner = manualAddressesByCurrency.putIfAbsent(currency, () => {});
final base = manualLabel
.trim()
.isEmpty ? currency.title : manualLabel.trim();
final map = manual.putIfAbsent(currency, () => {});
final oldLabel = isAddressEdit ? _originalLabel : null;
final newLabel = label.trim().isEmpty ? currency.title : label.trim();
final newAddress = address.trim();
var label = base;
var i = 1;
while (inner.containsKey(label)) {
label = '$base $i';
i++;
}
if (oldLabel != null && oldLabel != newLabel) map.remove(oldLabel);
map[newLabel] = newAddress;
manual[currency] = Map.of(map);
inner[label] = manualAddress.trim();
manualAddressesByCurrency[currency] = Map<String, String>.of(inner);
record!.setManualAddress(currency, newLabel, newAddress);
_rememberOriginal();
state = ExecutedSuccessfullyState();
}
@action
Future<void> pickAvatar(String localPath) async {
imagePath = localPath;
Future<void> saveParsedAddress() async {
_ensureRecord();
final blockKey = handleKey.trim().isEmpty ? _defaultHandleKey() : handleKey.trim();
final block = parsedBlocks.putIfAbsent(blockKey, () => {});
final map = block.putIfAbsent(currency, () => {});
final oldLabel = isAddressEdit ? _originalLabel : null;
final newLabel = label.trim().isEmpty ? currency.title : label.trim();
final newAddress = address.trim();
if (oldLabel != null && oldLabel != newLabel) map.remove(oldLabel);
map[newLabel] = newAddress;
parsedBlocks[blockKey] = {for (final e in block.entries) e.key: Map.of(e.value)};
record!.setParsedAddress(blockKey, currency, newLabel, newAddress);
_rememberOriginal(blockKey: blockKey);
state = ExecutedSuccessfullyState();
}
void deleteManualAddress(CryptoCurrency cur, String label) {
final inner = manualAddressesByCurrency[cur];
if (inner == null) return;
inner.remove(label);
manualAddressesByCurrency[cur] = Map<String, String>.of(inner);
@action
Future<void> deleteCurrentAddress() async {
if (!isAddressEdit) return;
_ensureRecord();
if (mode == ContactEditMode.manualAddress) {
final map = manual[_originalCur]!;
map.remove(_originalLabel);
if (map.isEmpty) manual.remove(_originalCur);
manual[_originalCur!] = Map.of(map);
record!.removeManualAddress(_originalCur!, _originalLabel!);
} else {
final block = parsedBlocks[_originalHandleKey]!;
final curMap = block[_originalCur]!;
curMap.remove(_originalLabel);
if (curMap.isEmpty) block.remove(_originalCur);
if (block.isEmpty)
parsedBlocks.remove(_originalHandleKey);
else
parsedBlocks[_originalHandleKey!] = {for (final e in block.entries) e.key: Map.of(e.value)};
record!.removeParsedAddress(_originalHandleKey!, _originalCur!, _originalLabel!);
}
state = ExecutedSuccessfullyState();
}
@action
Future<void> deleteParsedBlock(String handleKey) async {
if (!parsedBlocks.containsKey(handleKey)) return;
parsedBlocks.remove(handleKey);
record!.removeParsedAddress(handleKey, null, null);
state = ExecutedSuccessfullyState();
}
@action
Future<void> deleteContact() async {
if (record == null) return;
await record!.original.delete();
record = null;
reset();
state = ExecutedSuccessfullyState();
}
@action
void reset() {
name = '';
handle = '';
profileName = '';
description = '';
imagePath = '';
parsedAddressesByCurrency.clear();
manualAddressesByCurrency.clear();
name = handle = profileName = description = imagePath = '';
label = address = handleKey = '';
currency = CryptoCurrency.xmr;
manual.clear();
parsed.clear();
parsedBlocks.clear();
_originalCur = null;
_originalLabel = null;
_originalAddress = null;
_originalHandleKey = null;
state = InitialExecutionState();
}
Future<void> save() async {
void _initMapsFromRecord() {
if (record == null) return;
try {
state = IsExecutingState();
final clash = _box.values.any(
(c) => c.name == name && c.key != contactRecord?.original.key,
);
if (clash) {
state = FailureState(S.current.contact_name_exists);
return;
manual = ObservableMap.of(record!.manual);
parsed = ObservableMap.of(record!.parsedByCurrency);
parsedBlocks = ObservableMap.of(record!.parsedBlocks);
}
if (contactRecord != null && contactRecord!.original.isInBox) {
final contact = contactRecord!.original;
contact
..name = name
..handle = handle
..profileName = profileName
..description = description
..imagePath = imagePath
..source = sourceType;
contact.parsedAddresses
..clear()
..addAll({
for (final e in parsedAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
});
contact.manualAddresses
..clear()
..addAll({
for (final e in manualAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
});
await contact.save();
contactRecord!
..parsedAddresses = ObservableMap.of(contact.parsedByCurrency)
..manualAddresses = ObservableMap.of(contact.manualByCurrency);
} else {
final newContact = Contact(
name: name,
parsedAddresses: {
for (final e in parsedAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
},
manualAddresses: {
for (final e in manualAddressesByCurrency.entries)
e.key.raw: Map<String, String>.of(e.value)
},
source: sourceType,
handle: handle,
profileName: profileName,
description: description,
imagePath: imagePath,
lastChange: DateTime.now(),
);
await _box.add(newContact);
}
state = ExecutedSuccessfullyState();
} catch (e, st) {
debugPrintStack(label: 'save() failed', stackTrace: st);
state = FailureState(e.toString());
}
void _ensureRecord() {
if (record != null) return;
final newContact = Contact(name: name.trim().isEmpty ? 'No name' : name, address: '');
box.put(newContact.key, newContact);
record = ContactRecord(box, newContact);
}
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
String _defaultHandleKey() => '${sourceType.label}-${handle}'.trim();
void _rememberOriginal({String? blockKey}) {
_originalCur = currency;
_originalLabel = label.trim().isEmpty ? currency.title : label.trim();
_originalAddress = address.trim();
_originalHandleKey = blockKey ?? _defaultHandleKey();
}
}

View file

@ -711,7 +711,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
contactListViewModel.contactSource,
Contact(
name: '',
parsedAddresses: {3: {'label4':address}
parsedByHandle: {
'handle':
{
3: {'label4': address}
}
},
manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value
},