mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 20:39:51 +00:00
fix: save hive issue
This commit is contained in:
parent
66c49f795b
commit
8dc3bb649d
27 changed files with 1411 additions and 1295 deletions
|
@ -30,7 +30,6 @@ class AuthService with Store {
|
||||||
Routes.modify2FAPage,
|
Routes.modify2FAPage,
|
||||||
Routes.newWallet,
|
Routes.newWallet,
|
||||||
Routes.newWalletType,
|
Routes.newWalletType,
|
||||||
Routes.addressBookAddContact,
|
|
||||||
Routes.restoreOptions,
|
Routes.restoreOptions,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
102
lib/di.dart
102
lib/di.dart
|
@ -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/entities/parsed_address.dart';
|
||||||
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||||
import 'package:cake_wallet/haven/cw_haven.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_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_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_group_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_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/contact/contact_list_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||||
|
@ -971,54 +971,72 @@ Future<void> setup({
|
||||||
getIt.registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>()));
|
getIt.registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>()));
|
||||||
|
|
||||||
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
|
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
|
||||||
|
|
||||||
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
|
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
|
||||||
|
|
||||||
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
||||||
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
||||||
|
|
||||||
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, List<dynamic>?>(
|
getIt.registerFactoryParam<ContactViewModel, AddressEditRequest?, void>(
|
||||||
(contact, initialContactParams) => ContactViewModel(
|
(req, _) => ContactViewModel(_contactSource,getIt<AppStore>().wallet!, request: req),
|
||||||
_contactSource,
|
|
||||||
contact: contact,
|
|
||||||
initialParams: initialContactParams ?? <dynamic>[],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
|
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
|
||||||
(CryptoCurrency? cur, _) =>
|
(cur, _) => ContactListViewModel(
|
||||||
ContactListViewModel(_contactSource, _walletInfoSource, getIt
|
_contactSource,
|
||||||
.get<AppStore>()
|
_walletInfoSource,
|
||||||
.wallet!, cur, getIt.get<SettingsStore>()));
|
getIt<AppStore>().wallet!,
|
||||||
|
cur,
|
||||||
|
getIt<SettingsStore>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
|
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
|
||||||
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
|
(cur, _) => ContactListPage(
|
||||||
|
getIt.get<ContactListViewModel>(param1: cur),
|
||||||
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
|
getIt<AuthService>(),
|
||||||
(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]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
getIt.registerFactoryParam<EditNewContactPage, ContactRecord, void>(
|
|
||||||
(ContactRecord contact, _) => EditNewContactPage(contactViewModel: getIt.get<ContactViewModel>(param1: contact)));
|
|
||||||
|
|
||||||
getIt.registerFactoryParam<EditContactPage, ContactRecord, void>(
|
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>()));
|
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
|
||||||
|
|
||||||
|
@ -1544,10 +1562,10 @@ Future<void> setup({
|
||||||
getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
|
getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
|
||||||
|
|
||||||
getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
|
getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
|
||||||
|
|
||||||
getIt.registerFactory(() => BackgroundSyncLogsViewModel());
|
getIt.registerFactory(() => BackgroundSyncLogsViewModel());
|
||||||
|
|
||||||
getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
|
getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
|
||||||
|
|
||||||
_isSetupFinished = true;
|
_isSetupFinished = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/hive_type_ids.dart';
|
import 'package:cw_core/hive_type_ids.dart';
|
||||||
|
@ -10,21 +12,152 @@ part 'contact.g.dart';
|
||||||
class Contact extends HiveObject with Keyable {
|
class Contact extends HiveObject with Keyable {
|
||||||
Contact({
|
Contact({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.parsedAddresses,
|
this.address = '',
|
||||||
required this.manualAddresses,
|
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,
|
AddressSource source = AddressSource.notParsed,
|
||||||
this.handle = '',
|
this.handle = '',
|
||||||
this.imagePath = '',
|
this.imagePath = '',
|
||||||
this.profileName = '',
|
this.profileName = '',
|
||||||
this.description = '',
|
this.description = '',
|
||||||
DateTime? lastChange,
|
DateTime? lastChange,
|
||||||
}) : sourceRaw = source.raw,
|
}) : raw = type?.raw ?? 0,
|
||||||
|
_parsedJson = _encode(parsedByHandle),
|
||||||
|
_manualJson = _encode(manualAddresses),
|
||||||
|
extraJsonBlobs = extraBlobs,
|
||||||
|
sourceRaw = source.raw,
|
||||||
lastChange = lastChange ?? DateTime.now();
|
lastChange = lastChange ?? DateTime.now();
|
||||||
|
|
||||||
static const typeId = CONTACT_TYPE_ID;
|
static const typeId = CONTACT_TYPE_ID;
|
||||||
static const boxName = 'Contacts';
|
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}) {
|
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(
|
return Contact(
|
||||||
name: p.profileName.isNotEmpty ? p.profileName : p.handle,
|
name: p.profileName.isNotEmpty ? p.profileName : p.handle,
|
||||||
profileName: p.profileName,
|
profileName: p.profileName,
|
||||||
|
@ -32,88 +165,8 @@ class Contact extends HiveObject with Keyable {
|
||||||
description: p.description,
|
description: p.description,
|
||||||
source: p.addressSource,
|
source: p.addressSource,
|
||||||
imagePath: localImage ?? '',
|
imagePath: localImage ?? '',
|
||||||
parsedAddresses: {
|
manualAddresses: manual,
|
||||||
if (p.parsedAddressByCurrencyMap.isNotEmpty)
|
parsedByHandle: parsed,
|
||||||
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}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,117 +14,143 @@ part 'contact_record.g.dart';
|
||||||
class ContactRecord = ContactRecordBase with _$ContactRecord;
|
class ContactRecord = ContactRecordBase with _$ContactRecord;
|
||||||
|
|
||||||
abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
|
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,
|
: name = original.name,
|
||||||
handle = original.handle,
|
handle = original.handle,
|
||||||
profileName = original.profileName,
|
profileName = original.profileName,
|
||||||
description = original.description,
|
description = original.description,
|
||||||
imagePath = original.imagePath,
|
imagePath = original.imagePath,
|
||||||
sourceType = original.source,
|
sourceType = original.source,
|
||||||
parsedAddresses = ObservableMap.of(original.parsedByCurrency),
|
manual = ObservableMap.of(original.manualByCurrency),
|
||||||
manualAddresses = ObservableMap.of(original.manualByCurrency),
|
parsedBlocks = ObservableMap.of({
|
||||||
super(source, original);
|
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
|
@observable
|
||||||
String name;
|
String name, handle, profileName, description, imagePath;
|
||||||
@observable
|
|
||||||
String handle;
|
|
||||||
@observable
|
|
||||||
String profileName;
|
|
||||||
@observable
|
|
||||||
String description;
|
|
||||||
@observable
|
|
||||||
String imagePath;
|
|
||||||
@observable
|
@observable
|
||||||
AddressSource sourceType;
|
AddressSource sourceType;
|
||||||
|
|
||||||
String address = '';
|
@observable
|
||||||
|
ObservableMap<CryptoCurrency, Map<String, String>> manual;
|
||||||
CryptoCurrency type = CryptoCurrency.btc;
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddresses;
|
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks;
|
||||||
|
|
||||||
@observable
|
@computed
|
||||||
ObservableMap<CryptoCurrency, Map<String, String>> manualAddresses;
|
Map<CryptoCurrency, Map<String, String>> get parsedByCurrency {
|
||||||
|
final out = <CryptoCurrency, Map<String, String>>{};
|
||||||
@override
|
parsedBlocks.forEach((_, byCur) {
|
||||||
void toBind(Contact original) {
|
byCur.forEach((cur, lbl) => out.putIfAbsent(cur, () => {})..addAll(lbl));
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
|
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
ImageProvider get avatarProvider {
|
ImageProvider get avatarProvider => (avatarFile?.existsSync() ?? false)
|
||||||
final f = avatarFile;
|
? FileImage(avatarFile!)
|
||||||
return (f != null && f.existsSync())
|
: const AssetImage('assets/images/profile.png');
|
||||||
? FileImage(f)
|
|
||||||
: 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
|
@action
|
||||||
void setParsedAddress(CryptoCurrency cur, String label, String addr) {
|
void setManualAddress(CryptoCurrency cur, String label, String addr) {
|
||||||
final oldInner = parsedAddresses[cur] ?? {};
|
manual.putIfAbsent(cur, () => {})[label] = addr;
|
||||||
parsedAddresses[cur] = {...oldInner, 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -432,7 +432,10 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
|
||||||
final address = _item["address"] as String;
|
final address = _item["address"] as String;
|
||||||
final name = _item["name"] 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);
|
}, manualAddresses: {}, name: name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,19 @@ extension AddressSourceIndex on AddressSource {
|
||||||
AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
|
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 {
|
class ParsedAddress {
|
||||||
const ParsedAddress({
|
const ParsedAddress({
|
||||||
required this.parsedAddressByCurrencyMap,
|
required this.parsedAddressByCurrencyMap,
|
||||||
|
|
|
@ -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/exchange/trade.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.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/anonpay_details/anonpay_details_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
|
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
|
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
|
||||||
|
@ -579,11 +578,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||||
case Routes.pickerWalletAddress:
|
case Routes.pickerWalletAddress:
|
||||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
|
||||||
|
|
||||||
case Routes.addressBookAddContact:
|
|
||||||
return handleRouteWithPlatformAwareness(
|
|
||||||
(context) => getIt.get<ContactPage>(param1: settings.arguments as ContactRecord?),
|
|
||||||
);
|
|
||||||
|
|
||||||
case Routes.showKeys:
|
case Routes.showKeys:
|
||||||
return handleRouteWithPlatformAwareness(
|
return handleRouteWithPlatformAwareness(
|
||||||
(context) => getIt.get<WalletKeysPage>(),
|
(context) => getIt.get<WalletKeysPage>(),
|
||||||
|
|
|
@ -34,10 +34,10 @@ class Routes {
|
||||||
static const addressBook = '/address_book';
|
static const addressBook = '/address_book';
|
||||||
static const pickerAddressBook = '/picker_address_book';
|
static const pickerAddressBook = '/picker_address_book';
|
||||||
static const pickerWalletAddress = '/picker_wallet_address';
|
static const pickerWalletAddress = '/picker_wallet_address';
|
||||||
static const addressBookAddContact = '/address_book_add_contact';
|
|
||||||
static const newContactWelcomePage = '/new_contact_welcome_page';
|
static const newContactWelcomePage = '/new_contact_welcome_page';
|
||||||
static const supportedHandlesPage = '/supported_handles_page';
|
static const supportedHandlesPage = '/supported_handles_page';
|
||||||
static const editNewContactGroupPage = '/edit_new_contact_group_page';
|
static const editNewContactGroupPage = '/edit_new_contact_group_page';
|
||||||
|
static const editContactGroupPage = '/edit_contact_group_page';
|
||||||
static const editAddressesPage = '/edit_addresses_page';
|
static const editAddressesPage = '/edit_addresses_page';
|
||||||
static const editAddressPage = '/edit_address_page';
|
static const editAddressPage = '/edit_address_page';
|
||||||
static const editNewContactPage = '/edit_new_contact_page';
|
static const editNewContactPage = '/edit_new_contact_page';
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,14 @@
|
||||||
import 'dart:async';
|
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/entities/parsed_address.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.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/base_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.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/src/widgets/primary_button.dart';
|
||||||
import 'package:cake_wallet/themes/core/material_base_theme.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/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/crypto_currency.dart';
|
||||||
import 'package:cw_core/currency.dart';
|
import 'package:cw_core/currency.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
enum _InputMode { handle, address }
|
enum _InputMode { handle, address }
|
||||||
|
|
||||||
class NewContactWelcomePage extends BasePage {
|
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 Future<List<ParsedAddress>> Function(String query) onSearch;
|
||||||
|
final bool handleOnly;
|
||||||
|
final ContactRecord? existingContact;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget body(BuildContext context) =>
|
Widget body(BuildContext context) => NewContactWelcomePageBody(
|
||||||
NewContactWelcomePageBody(currentTheme: currentTheme, onSearch: onSearch);
|
currentTheme: currentTheme,
|
||||||
|
onSearch: onSearch,
|
||||||
|
handleOnly: handleOnly,
|
||||||
|
existingContact: existingContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewContactWelcomePageBody extends StatefulWidget {
|
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 MaterialThemeBase currentTheme;
|
||||||
final Future<List<ParsedAddress>> Function(String query) onSearch;
|
final Future<List<ParsedAddress>> Function(String query) onSearch;
|
||||||
|
final bool handleOnly;
|
||||||
|
final ContactRecord? existingContact;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState();
|
State<NewContactWelcomePageBody> createState() => _NewContactWelcomePageBodyState();
|
||||||
|
@ -130,6 +145,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _segmentedSwitcher(BuildContext ctx) {
|
Widget _segmentedSwitcher(BuildContext ctx) {
|
||||||
|
if (widget.handleOnly) return const SizedBox.shrink();
|
||||||
final txt = Theme.of(ctx).textTheme.bodyMedium;
|
final txt = Theme.of(ctx).textTheme.bodyMedium;
|
||||||
final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>(
|
final seg = (_InputMode m, String label) => ButtonSegment<_InputMode>(
|
||||||
value: m,
|
value: m,
|
||||||
|
@ -150,13 +166,8 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: SegmentedButton<_InputMode>(
|
child: SegmentedButton<_InputMode>(
|
||||||
segments: [
|
segments: [seg(_InputMode.handle, 'handle'), seg(_InputMode.address, 'address')],
|
||||||
seg(_InputMode.handle, 'handle'),
|
selected: <_InputMode>{_mode},
|
||||||
seg(_InputMode.address, 'address'),
|
|
||||||
],
|
|
||||||
selected: <_InputMode>{
|
|
||||||
_mode
|
|
||||||
},
|
|
||||||
showSelectedIcon: false,
|
showSelectedIcon: false,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: WidgetStatePropertyAll(
|
shape: WidgetStatePropertyAll(
|
||||||
|
@ -191,51 +202,40 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||||
? CustomThemeColors.backgroundGradientColorDark
|
? CustomThemeColors.backgroundGradientColorDark
|
||||||
: CustomThemeColors.backgroundGradientColorLight;
|
: CustomThemeColors.backgroundGradientColorLight;
|
||||||
|
|
||||||
final hasDropdown = _results.isNotEmpty || _isSearching;
|
final isHandleMode = widget.handleOnly ? true : _mode == _InputMode.handle;
|
||||||
final border = OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(
|
return StandardTextFormFieldWidget(
|
||||||
top: const Radius.circular(12),
|
focusNode: _focusNode,
|
||||||
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
|
controller: isHandleMode ? _handleCtl : _addressCtl,
|
||||||
),
|
labelText: isHandleMode ? 'Enter handle' : 'Enter address',
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
fillColor: fillColor,
|
||||||
);
|
onChanged: isHandleMode ? _handleChanged : (v) => setState(() => _typedAddress = v.trim()),
|
||||||
|
prefixIcon: isHandleMode
|
||||||
final rounded = BorderRadius.vertical(
|
? null
|
||||||
top: const Radius.circular(12),
|
: Padding(
|
||||||
bottom: hasDropdown ? Radius.zero : const Radius.circular(12),
|
padding: const EdgeInsets.all(8.0),
|
||||||
);
|
child: _currencyPrefix(context),
|
||||||
|
),
|
||||||
final noStroke = OutlineInputBorder(borderRadius: rounded, borderSide: BorderSide.none);
|
suffixIcon: RoundedIconButton(
|
||||||
|
icon: Icons.paste_outlined,
|
||||||
final isHandleMode = _mode == _InputMode.handle;
|
onPressed: () async {
|
||||||
|
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
return Theme(
|
final text = data?.text?.trim() ?? '';
|
||||||
data: Theme.of(context).copyWith(
|
if (text.isEmpty) return;
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
|
||||||
border: noStroke,
|
final isHandleMode = widget.handleOnly || _mode == _InputMode.handle;
|
||||||
focusedBorder: noStroke,
|
|
||||||
enabledBorder: noStroke,
|
if (isHandleMode) {
|
||||||
disabledBorder: border,
|
_handleCtl.text = text;
|
||||||
errorBorder: border,
|
_handleChanged(text);
|
||||||
focusedErrorBorder: border,
|
} else {
|
||||||
prefixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0),
|
_addressCtl.text = text;
|
||||||
),
|
setState(() => _typedAddress = text);
|
||||||
),
|
}
|
||||||
child: BaseTextFormField(
|
},
|
||||||
key: ValueKey(_mode),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))),
|
||||||
controller: isHandleMode ? _handleCtl : _addressCtl,
|
|
||||||
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!,
|
|
||||||
),
|
),
|
||||||
|
addressValidator: AddressValidator(type: _selectedCurrency),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +311,7 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_mode == _InputMode.handle
|
widget.handleOnly || _mode == _InputMode.handle
|
||||||
? InkWell(
|
? InkWell(
|
||||||
splashFactory: NoSplash.splashFactory,
|
splashFactory: NoSplash.splashFactory,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -340,41 +340,49 @@ class _NewContactWelcomePageBodyState extends State<NewContactWelcomePageBody> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 24, top: 8),
|
padding: const EdgeInsets.only(bottom: 24, top: 8),
|
||||||
child: LoadingPrimaryButton(
|
child: LoadingPrimaryButton(
|
||||||
text: S.of(context).seed_language_next,
|
text: S.of(context).seed_language_next,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
if (_mode == _InputMode.handle) {
|
if (_mode == _InputMode.handle) {
|
||||||
Navigator.pushNamed(context, Routes.editNewContactGroupPage,
|
Navigator.pushNamed(
|
||||||
arguments: _selected);
|
context,
|
||||||
_selected = null;
|
|
||||||
} else {
|
|
||||||
Navigator.pushNamed(context,
|
|
||||||
Routes.editNewContactGroupPage,
|
Routes.editNewContactGroupPage,
|
||||||
arguments: ParsedAddress(
|
arguments: [_selected!, widget.existingContact],
|
||||||
parsedAddressByCurrencyMap: {},
|
);
|
||||||
manualAddressByCurrencyMap: {
|
_selected = null;
|
||||||
_selectedCurrency: _typedAddress.trim(),
|
} else {
|
||||||
},
|
final parsed = ParsedAddress(
|
||||||
addressSource: AddressSource.contact,
|
parsedAddressByCurrencyMap: {},
|
||||||
handle: '',
|
manualAddressByCurrencyMap: {
|
||||||
profileName: '',
|
_selectedCurrency: _typedAddress.trim(),
|
||||||
profileImageUrl: 'assets/images/profile.png',
|
},
|
||||||
description: '',
|
addressSource: AddressSource.contact,
|
||||||
));
|
handle: '',
|
||||||
}
|
profileName: '',
|
||||||
},
|
profileImageUrl: 'assets/images/profile.png',
|
||||||
color: Theme.of(context).colorScheme.primary,
|
description: '',
|
||||||
width: 150,
|
);
|
||||||
height: 40,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
Navigator.pushNamed(
|
||||||
isLoading: false,
|
context,
|
||||||
isDisabled: _mode == _InputMode.handle
|
Routes.editNewContactGroupPage,
|
||||||
? _selected == null || _isSearching
|
arguments: [parsed, null],
|
||||||
: _typedAddress.isEmpty,
|
);
|
||||||
),
|
}
|
||||||
)
|
},
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 150,
|
||||||
|
height: 40,
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
isLoading: false,
|
||||||
|
isDisabled: widget.handleOnly
|
||||||
|
? _selected == null || _isSearching
|
||||||
|
: (_mode == _InputMode.handle
|
||||||
|
? _selected == null || _isSearching
|
||||||
|
: _typedAddress.isEmpty),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
|
@ -1,75 +1,38 @@
|
||||||
import 'package:cake_wallet/core/address_validator.dart';
|
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/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/base_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.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/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/image_utill.dart';
|
||||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/currency.dart';
|
import 'package:cw_core/currency.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
|
||||||
|
|
||||||
class EditAddressPage extends BasePage {
|
class EditAddressPage extends BasePage {
|
||||||
EditAddressPage({required this.contactViewModel})
|
EditAddressPage({required this.contactViewModel})
|
||||||
: _formKey = GlobalKey<FormState>(),
|
: _formKey = GlobalKey<FormState>(),
|
||||||
manualAddress = contactViewModel.manualAddressesByCurrency[contactViewModel.initialCurrency]
|
_labelController = TextEditingController(text: contactViewModel.label),
|
||||||
?[contactViewModel.manualLabel] ??
|
_addressController = TextEditingController(text: contactViewModel.address) {
|
||||||
'',
|
_labelController.addListener(() => contactViewModel.label = _labelController.text);
|
||||||
_labelController = TextEditingController(),
|
_addressController.addListener(() => contactViewModel.address = _addressController.text);
|
||||||
_manualAddressController = TextEditingController() {
|
|
||||||
_labelController.text = contactViewModel.manualLabel;
|
|
||||||
_manualAddressController.text = contactViewModel.isNewAddress ? '' : manualAddress ?? '';
|
|
||||||
|
|
||||||
_labelController.addListener(() => contactViewModel.manualLabel = _labelController.text);
|
|
||||||
|
|
||||||
_manualAddressController
|
|
||||||
.addListener(() => contactViewModel.manualAddress = _manualAddressController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => contactViewModel.isNewAddress ? 'Add Address' : 'Edit Address';
|
String get title => contactViewModel.isAddressEdit ? 'Edit Address' : 'Add Address';
|
||||||
|
|
||||||
final ContactViewModel contactViewModel;
|
final ContactViewModel contactViewModel;
|
||||||
final GlobalKey<FormState> _formKey;
|
final GlobalKey<FormState> _formKey;
|
||||||
final TextEditingController _labelController;
|
final TextEditingController _labelController;
|
||||||
final TextEditingController _manualAddressController;
|
final TextEditingController _addressController;
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget body(BuildContext context) {
|
Widget body(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final fillColor = currentTheme.isDark
|
final fillColor = currentTheme.isDark
|
||||||
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
|
? CustomThemeColors.backgroundGradientColorDark.withAlpha(100)
|
||||||
: CustomThemeColors.backgroundGradientColorLight;
|
: CustomThemeColors.backgroundGradientColorLight;
|
||||||
|
@ -87,126 +50,70 @@ class EditAddressPage extends BasePage {
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(initialCurrency.fullName ?? initialCurrency.name,
|
title: Text(initialCurrency.fullName ?? initialCurrency.name,
|
||||||
style: Theme.of(context).textTheme.bodyMedium),
|
style: Theme.of(context).textTheme.bodyMedium),
|
||||||
trailing: Icon(Icons.keyboard_arrow_down_outlined,
|
trailing: Icon(Icons.keyboard_arrow_down_outlined,
|
||||||
size: 24, color: theme.colorScheme.onSurface),
|
size: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||||
tileColor: fillColor,
|
tileColor: fillColor,
|
||||||
dense: true,
|
dense: true,
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
|
visualDensity: VisualDensity(horizontal: 0, vertical: -3),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12),
|
contentPadding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
leading: ImageUtil.getImageFromPath(
|
leading: ImageUtil.getImageFromPath(
|
||||||
imagePath: initialCurrency.iconPath ?? '', height: 24, width: 24),
|
imagePath: initialCurrency.iconPath ?? '', height: 24, width: 24),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
onTap: () => _presentCurrencyPicker(context, contactViewModel),
|
onTap: () => _presentCurrencyPicker(context, contactViewModel)),
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
StandardTextFormFieldWidget(
|
||||||
controller: _labelController,
|
controller: _labelController,
|
||||||
decoration: InputDecoration(
|
labelText: 'Address label',
|
||||||
isDense: true,
|
fillColor: fillColor,
|
||||||
isCollapsed: true,
|
suffixIcon: RoundedIconButton(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
icon: Icons.paste_outlined,
|
||||||
labelText: 'Address label',
|
onPressed: () async {
|
||||||
labelStyle: Theme.of(context)
|
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
.textTheme
|
final text = data?.text ?? '';
|
||||||
.bodyMedium!
|
if (text.trim().isEmpty) return;
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
_labelController.text = text.trim();
|
||||||
hintStyle: Theme.of(context)
|
},
|
||||||
.textTheme
|
shape: RoundedRectangleBorder(
|
||||||
.bodyMedium!
|
borderRadius: BorderRadius.all(Radius.circular(6)))),
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
addressValidator: (value) {
|
||||||
fillColor: fillColor,
|
if (value == null || value.trim().isEmpty) return 'Label cannot be empty';
|
||||||
border: OutlineInputBorder(
|
return null;
|
||||||
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: () {},
|
|
||||||
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) {},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextFormField(
|
StandardTextFormFieldWidget(
|
||||||
controller: _manualAddressController,
|
controller: _addressController,
|
||||||
decoration: InputDecoration(
|
labelText: S.of(context).address,
|
||||||
isDense: true,
|
fillColor: fillColor,
|
||||||
isCollapsed: true,
|
suffixIcon: RoundedIconButton(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
icon: Icons.paste_outlined,
|
||||||
labelText: 'Address',
|
onPressed: () async {
|
||||||
labelStyle: Theme.of(context)
|
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
.textTheme
|
final text = data?.text ?? '';
|
||||||
.bodyMedium!
|
if (text.trim().isEmpty) return;
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
_addressController.text = text.trim();
|
||||||
hintStyle: Theme.of(context)
|
},
|
||||||
.textTheme
|
shape: RoundedRectangleBorder(
|
||||||
.bodyMedium!
|
borderRadius: BorderRadius.all(Radius.circular(6)))),
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
addressValidator: AddressValidator(type: contactViewModel.currency)),
|
||||||
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: () {},
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (!contactViewModel.isNewAddress)
|
if (contactViewModel.isAddressEdit)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: _circleIcon(
|
child: RoundedIconButton(
|
||||||
context: context,
|
|
||||||
icon: Icons.delete_outline_rounded,
|
icon: Icons.delete_outline_rounded,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
contactViewModel.deleteManualAddress(
|
await contactViewModel.deleteCurrentAddress();
|
||||||
initialCurrency, contactViewModel.manualLabel);
|
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
|
||||||
contactViewModel.updateManualAddress();
|
|
||||||
await contactViewModel.save();
|
|
||||||
contactViewModel.reset();
|
|
||||||
_manualAddressController.clear();
|
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
|
||||||
},
|
},
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
@ -219,10 +126,7 @@ class EditAddressPage extends BasePage {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
contactViewModel.reset();
|
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
|
||||||
_labelController.clear();
|
|
||||||
_manualAddressController.clear();
|
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: fillColor,
|
backgroundColor: fillColor,
|
||||||
|
@ -245,12 +149,12 @@ class EditAddressPage extends BasePage {
|
||||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
contactViewModel.updateManualAddress();
|
if (contactViewModel.mode == ContactEditMode.manualAddress) {
|
||||||
await contactViewModel.save();
|
await contactViewModel.saveManualAddress();
|
||||||
|
} else {
|
||||||
if (context.mounted && contactViewModel.state is! FailureState) {
|
await contactViewModel.saveParsedAddress();
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
|
||||||
}
|
}
|
||||||
|
if (context.mounted) Navigator.of(context, rootNavigator: true).pop();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
|
|
@ -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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
264
lib/src/screens/address_book/edit_contact_group_page.dart
Normal file
264
lib/src/screens/address_book/edit_contact_group_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,81 +1,80 @@
|
||||||
|
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/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/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/base_page.dart';
|
||||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||||
import 'package:cake_wallet/utils/image_utill.dart';
|
import 'package:cake_wallet/utils/image_utill.dart';
|
||||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
|
|
||||||
class EditContactPage extends BasePage {
|
class EditContactPage extends BasePage {
|
||||||
EditContactPage({required this.contactViewModel});
|
EditContactPage({required this.contactViewModel});
|
||||||
|
|
||||||
final ContactViewModel 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
|
@override
|
||||||
Widget middle(BuildContext context) {
|
Widget middle(BuildContext context) {
|
||||||
return Center(
|
return Observer(
|
||||||
child: Row(
|
builder: (_) => Center(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
ClipRRect(
|
children: [
|
||||||
borderRadius: BorderRadius.circular(5),
|
ClipRRect(
|
||||||
child: Image(
|
borderRadius: BorderRadius.circular(5),
|
||||||
width: 24,
|
child: Image(
|
||||||
height: 24,
|
width: 24,
|
||||||
image: contactViewModel.avatarProvider,
|
height: 24,
|
||||||
fit: BoxFit.cover,
|
image: contactViewModel.avatar,
|
||||||
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
Text(
|
|
||||||
contactViewModel.name,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: titleColor(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget trailing(BuildContext context) {
|
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(
|
return SizedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_circleIcon(context, Icons.add, () {}),
|
RoundedIconButton(icon: Icons.add, onPressed: () {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
Routes.newContactWelcomePage,
|
||||||
|
arguments: [onHandlerSearch, true, contactViewModel.record],
|
||||||
|
);
|
||||||
|
}),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_circleIcon(context, Icons.edit, () {}),
|
RoundedIconButton(icon: Icons.edit, onPressed: () {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
Routes.editContactGroupPage,
|
||||||
|
arguments: contactViewModel,
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -98,55 +97,74 @@ class EditContactPage extends BasePage {
|
||||||
key: ValueKey(contactViewModel.name),
|
key: ValueKey(contactViewModel.name),
|
||||||
title: Text('Manual Addresses'),
|
title: Text('Manual Addresses'),
|
||||||
fillColor: fillColor,
|
fillColor: fillColor,
|
||||||
manualByCurrency: contactViewModel.manualAddressesByCurrency,
|
manualByCurrency: contactViewModel.manual,
|
||||||
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
|
onCopyPressed: (address) async =>
|
||||||
|
await Clipboard.setData(ClipboardData(text: address)),
|
||||||
onEditPressed: (cur, lbl) {
|
onEditPressed: (cur, lbl) {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
Routes.editAddressPage,
|
Routes.editAddressPage,
|
||||||
arguments: [
|
arguments: AddressEditRequest.address(
|
||||||
contactViewModel.contactRecord,
|
contact: contactViewModel.record,
|
||||||
cur,
|
currency: cur,
|
||||||
lbl,
|
label: lbl,
|
||||||
],
|
kindIsManual: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ContactAddressesExpansionTile(
|
...contactViewModel.parsedBlocks.entries.map((entry) {
|
||||||
key: ValueKey(contactViewModel.name + '_Parsed'),
|
final String handle = entry.key;
|
||||||
title: Row(
|
final stringSrc = handle.split('-').first;
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final Map<CryptoCurrency, Map<String, String>> byCurrency = entry.value;
|
||||||
children: [
|
|
||||||
ImageUtil.getImageFromPath(
|
final src = AddressSourceNameParser.fromLabel(stringSrc);
|
||||||
imagePath: contactViewModel.sourceType.iconPath, height: 24, width: 24),
|
|
||||||
const SizedBox(width: 6),
|
return Padding(
|
||||||
Expanded(
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Text(
|
child: ContactAddressesExpansionTile(
|
||||||
contactViewModel.sourceType.label + ' - ' + contactViewModel.handle,
|
key: ValueKey('${contactViewModel.name}'),
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
title: Row(
|
||||||
fontSize: 12,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
children: [
|
||||||
|
ImageUtil.getImageFromPath(
|
||||||
|
imagePath: src.iconPath,
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
handle,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
fillColor: fillColor,
|
||||||
),
|
manualByCurrency: byCurrency,
|
||||||
fillColor: fillColor,
|
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
|
||||||
manualByCurrency: contactViewModel.parsedAddressesByCurrency,
|
onEditPressed: (cur, lbl) {
|
||||||
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
|
Navigator.pushNamed(
|
||||||
onEditPressed: (cur, lbl) {
|
context,
|
||||||
Navigator.pushNamed(
|
Routes.editAddressPage,
|
||||||
context,
|
arguments: AddressEditRequest.address(
|
||||||
Routes.editAddressPage,
|
contact: contactViewModel.record,
|
||||||
arguments: [
|
currency: cur,
|
||||||
contactViewModel.contactRecord,
|
label: lbl,
|
||||||
cur,
|
kindIsManual: false,
|
||||||
lbl,
|
handle: handle,
|
||||||
],
|
handleKey: entry.key,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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/src/widgets/primary_button.dart';
|
||||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||||
import 'package:cake_wallet/utils/image_utill.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/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
|
|
||||||
class EditNewContactGroupPage extends BasePage {
|
class EditNewContactGroupPage extends BasePage {
|
||||||
EditNewContactGroupPage({
|
EditNewContactGroupPage({
|
||||||
required this.selectedParsedAddress,
|
required this.selectedParsedAddress,
|
||||||
required this.contacts,
|
required this.contactViewModel,
|
||||||
}) : _formKey = GlobalKey<FormState>(),
|
}) : _formKey = GlobalKey<FormState>(),
|
||||||
_groupLabelCtl = TextEditingController(
|
_groupLabelCtl = TextEditingController(
|
||||||
text: selectedParsedAddress.profileName ?? '',
|
text: selectedParsedAddress.profileName ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
final ParsedAddress selectedParsedAddress;
|
final ParsedAddress selectedParsedAddress;
|
||||||
final Box<Contact> contacts;
|
final ContactViewModel contactViewModel;
|
||||||
|
|
||||||
final GlobalKey<FormState> _formKey;
|
final GlobalKey<FormState> _formKey;
|
||||||
final TextEditingController _groupLabelCtl;
|
final TextEditingController _groupLabelCtl;
|
||||||
|
@ -127,7 +127,7 @@ class EditNewContactGroupPage extends BasePage {
|
||||||
final text = value?.trim() ?? '';
|
final text = value?.trim() ?? '';
|
||||||
if (text.isEmpty) return 'Name cannot be empty';
|
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(),
|
(c) => c.name.toLowerCase() == text.toLowerCase(),
|
||||||
);
|
);
|
||||||
return clash ? 'Group with this name already exists' : null;
|
return clash ? 'Group with this name already exists' : null;
|
||||||
|
@ -149,22 +149,46 @@ class EditNewContactGroupPage extends BasePage {
|
||||||
height: 40,
|
height: 40,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||||
final localImg =
|
|
||||||
await ImageUtil.saveAvatarLocally(selectedParsedAddress.profileImageUrl);
|
|
||||||
|
|
||||||
final contact = Contact.fromParsed(
|
if (contactViewModel.record != null) {
|
||||||
selectedParsedAddress.copyWith(profileName: _groupLabelCtl.text.trim()),
|
final record = contactViewModel.record!;
|
||||||
localImage: localImg);
|
final handleKey =
|
||||||
contacts.add(contact);
|
'${selectedParsedAddress.addressSource.label}-${selectedParsedAddress.handle}'
|
||||||
final contactRecord = ContactRecord(
|
.trim();
|
||||||
contacts,
|
|
||||||
contact,
|
selectedParsedAddress.parsedAddressByCurrencyMap.forEach((cur, addr) {
|
||||||
);
|
record.setParsedAddress(
|
||||||
if (context.mounted) {
|
handleKey,
|
||||||
Navigator.of(context).pushNamed(
|
cur,
|
||||||
Routes.editNewContactPage,
|
cur.title,
|
||||||
arguments: [contactRecord],
|
addr.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
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,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
|
||||||
import 'package:cake_wallet/routes.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/addresses_expansion_tile_widget.dart';
|
||||||
import 'package:cake_wallet/src/screens/base_page.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/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/image_utill.dart';
|
||||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
@ -56,7 +53,7 @@ class EditNewContactPage extends BasePage {
|
||||||
child: Image(
|
child: Image(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
image: contactViewModel.avatarProvider,
|
image: contactViewModel.avatar,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -141,9 +138,11 @@ class EditNewContactPage extends BasePage {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: contactViewModel.parsedAddressesByCurrency.keys
|
children: contactViewModel.parsedBlocks.values
|
||||||
.map((currency) => currency.iconPath != null
|
.expand((map) => map.keys)
|
||||||
? Image.asset(currency.iconPath!, height: 24, width: 24)
|
.toSet()
|
||||||
|
.map((CryptoCurrency cur) => cur.iconPath != null
|
||||||
|
? Image.asset(cur.iconPath!, height: 24, width: 24)
|
||||||
: const SizedBox.shrink())
|
: const SizedBox.shrink())
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
|
@ -159,17 +158,18 @@ class EditNewContactPage extends BasePage {
|
||||||
key: ValueKey(contactViewModel.name),
|
key: ValueKey(contactViewModel.name),
|
||||||
title: Text('Manual Addresses'),
|
title: Text('Manual Addresses'),
|
||||||
fillColor: fillColor,
|
fillColor: fillColor,
|
||||||
manualByCurrency: contactViewModel.manualAddressesByCurrency,
|
manualByCurrency: contactViewModel.manual,
|
||||||
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
|
onCopyPressed: (address) async => await Clipboard.setData(ClipboardData(text: address)),
|
||||||
onEditPressed: (cur, lbl) {
|
onEditPressed: (cur, lbl) {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
Routes.editAddressPage,
|
Routes.editAddressPage,
|
||||||
arguments: [
|
arguments: AddressEditRequest.address(
|
||||||
contactViewModel.contactRecord,
|
contact: contactViewModel.record,
|
||||||
cur,
|
currency: cur,
|
||||||
lbl,
|
label: lbl,
|
||||||
],
|
kindIsManual: false,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
23
lib/src/screens/address_book/entities/user_handles.dart
Normal file
23
lib/src/screens/address_book/entities/user_handles.dart
Normal 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;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
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/src/screens/base_page.dart';
|
||||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||||
import 'package:cake_wallet/utils/image_utill.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SupportedHandlesPage extends BasePage {
|
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: () {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/entities/wallet_list_order_types.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.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/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/base_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
|
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
|
||||||
import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
|
import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
|
||||||
|
@ -63,19 +65,8 @@ class ContactListPage extends BasePage {
|
||||||
// FIX-ME: Style
|
// FIX-ME: Style
|
||||||
//shape: CircleBorder(),
|
//shape: CircleBorder(),
|
||||||
onPressed: () async {
|
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(
|
await _showAddressBookBottomSheet(
|
||||||
context: context, contactListViewModel: contactListViewModel);
|
context: context, contactListViewModel: contactListViewModel);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Offstage(),
|
child: Offstage(),
|
||||||
),
|
),
|
||||||
|
@ -340,7 +331,7 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||||
child: ContactAddressesExpansionTile(
|
child: ContactAddressesExpansionTile(
|
||||||
key: Key(contact.key.toString()),
|
key: Key(contact.key.toString()),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
manualByCurrency: contact.manualAddresses,
|
manualByCurrency: contact.manual,
|
||||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
title: _buildContactTitle(
|
title: _buildContactTitle(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -351,7 +342,12 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||||
context: context,
|
context: context,
|
||||||
contactListViewModel: widget.contactListViewModel,
|
contactListViewModel: widget.contactListViewModel,
|
||||||
initialRoute: Routes.editAddressPage,
|
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)),
|
onCopyPressed: (addr) => Clipboard.setData(ClipboardData(text: addr)),
|
||||||
|
@ -394,17 +390,20 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_circleIcon(
|
RoundedIconButton(
|
||||||
context: context,
|
|
||||||
icon: Icons.add,
|
icon: Icons.add,
|
||||||
onPressed: () async => await _showAddressBookBottomSheet(
|
onPressed: () async => await _showAddressBookBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
contactListViewModel: contactListViewModel,
|
contactListViewModel: contactListViewModel,
|
||||||
initialRoute: Routes.editAddressPage,
|
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),
|
const SizedBox(width: 8),
|
||||||
_circleIcon(
|
RoundedIconButton(
|
||||||
context: context,
|
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
onPressed: () async => await _showAddressBookBottomSheet(
|
onPressed: () async => await _showAddressBookBottomSheet(
|
||||||
context: context,
|
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(
|
Future<void> _showAddressBookBottomSheet(
|
||||||
|
|
|
@ -612,64 +612,13 @@ class SendPage extends BasePage {
|
||||||
// newContactAddress = null;
|
// newContactAddress = null;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
|
// bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
|
||||||
|
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
builder: (BuildContext bottomSheetContext) {
|
builder: (BuildContext bottomSheetContext) {
|
||||||
return showContactSheet && sendViewModel.ocpRequest == null
|
return InfoBottomSheet(
|
||||||
? 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(
|
|
||||||
'Don’t 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(
|
|
||||||
currentTheme: currentTheme,
|
currentTheme: currentTheme,
|
||||||
footerType: FooterType.singleActionButton,
|
footerType: FooterType.singleActionButton,
|
||||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||||
|
|
|
@ -4,13 +4,16 @@ import 'package:cake_wallet/di.dart';
|
||||||
import 'package:cake_wallet/entities/contact_record.dart';
|
import 'package:cake_wallet/entities/contact_record.dart';
|
||||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||||
import 'package:cake_wallet/routes.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_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_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_group_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/address_book/edit_new_contact_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/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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,24 +111,40 @@ class _AddContactNavigator extends StatelessWidget {
|
||||||
case Routes.supportedHandlesPage:
|
case Routes.supportedHandlesPage:
|
||||||
page = SupportedHandlesPage();
|
page = SupportedHandlesPage();
|
||||||
break;
|
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:
|
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;
|
break;
|
||||||
case Routes.editNewContactPage:
|
case Routes.editNewContactPage:
|
||||||
final list = args as List<dynamic>;
|
page = getIt<EditNewContactPage>(param1: args as ContactRecord);
|
||||||
page = getIt<EditNewContactPage>(param1: list.first as ContactRecord?);
|
|
||||||
break;
|
|
||||||
case Routes.editAddressesPage:
|
|
||||||
page = getIt<EditAddressesPage>(param1: args as ContactRecord);
|
|
||||||
break;
|
break;
|
||||||
case Routes.editContactPage:
|
case Routes.editContactPage:
|
||||||
page = getIt<EditContactPage>(param1: args as ContactRecord);
|
page = getIt<EditContactPage>(param1: args as ContactRecord);
|
||||||
break;
|
break;
|
||||||
case Routes.editAddressPage:
|
case Routes.editAddressPage:
|
||||||
page = getIt<EditAddressPage>(param1: args as List<dynamic>);
|
page = getIt<EditAddressPage>(param1: args as AddressEditRequest);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
page = NewContactWelcomePage(onSearch: onHandlerSearch);
|
page = NewContactWelcomePage(onSearch: onHandlerSearch, handleOnly: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
|
|
|
@ -1,237 +1,272 @@
|
||||||
import 'dart:io';
|
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/contact_record.dart';
|
||||||
import 'package:cake_wallet/entities/parsed_address.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:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:cake_wallet/core/execution_state.dart';
|
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
|
||||||
import 'package:cake_wallet/entities/contact.dart';
|
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
|
||||||
|
|
||||||
part 'contact_view_model.g.dart';
|
part 'contact_view_model.g.dart';
|
||||||
|
|
||||||
class ContactViewModel = ContactViewModelBase with _$ContactViewModel;
|
enum ContactEditMode {
|
||||||
|
contactInfo,
|
||||||
|
manualAddress,
|
||||||
|
parsedAddress,
|
||||||
|
}
|
||||||
|
|
||||||
abstract class ContactViewModelBase with Store {
|
class ContactViewModel = _ContactViewModel with _$ContactViewModel;
|
||||||
ContactViewModelBase(this._box, {ContactRecord? contact, required List<dynamic>? initialParams,})
|
|
||||||
: state = InitialExecutionState(),
|
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,
|
currencies = CryptoCurrency.all,
|
||||||
contactRecord = contact,
|
state = InitialExecutionState(),
|
||||||
name = contact?.name ?? '',
|
name = request?.contact?.name ?? '',
|
||||||
handle = contact?.handle ?? '',
|
handle = request?.contact?.handle ?? '',
|
||||||
profileName = contact?.profileName ?? '',
|
profileName = request?.contact?.profileName ?? '',
|
||||||
description = contact?.description ?? '',
|
description = request?.contact?.description ?? '',
|
||||||
imagePath = contact?.imagePath ?? '',
|
imagePath = request?.contact?.imagePath ?? '',
|
||||||
sourceType = contact?.sourceType ?? AddressSource.notParsed,
|
sourceType = request?.contact?.sourceType ?? AddressSource.notParsed,
|
||||||
|
currency = request?.currency ?? CryptoCurrency.xmr,
|
||||||
|
label = request?.label ?? '',
|
||||||
|
address = '',
|
||||||
|
handleKey = request?.handleKey ?? '' {
|
||||||
|
_initMapsFromRecord();
|
||||||
|
|
||||||
currency = (initialParams != null &&
|
if (request?.label != null && record != null) {
|
||||||
initialParams.isNotEmpty &&
|
currency = request!.currency!;
|
||||||
initialParams[0] is CryptoCurrency)
|
label = request.label!;
|
||||||
? initialParams[0] as CryptoCurrency
|
address = _targetMap[currency]?[label] ?? '';
|
||||||
: CryptoCurrency.xmr,
|
|
||||||
|
|
||||||
initialCurrency = (initialParams != null &&
|
_rememberOriginal(
|
||||||
initialParams.isNotEmpty &&
|
blockKey: mode == ContactEditMode.parsedAddress
|
||||||
initialParams[0] is CryptoCurrency)
|
? (request.handleKey ?? _defaultHandleKey())
|
||||||
? initialParams[0] as CryptoCurrency
|
|
||||||
: null,
|
: 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
|
@observable
|
||||||
ExecutionState state;
|
ExecutionState state;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
String name;
|
String name, handle, profileName, description, imagePath;
|
||||||
@observable
|
|
||||||
String handle;
|
|
||||||
@observable
|
|
||||||
String profileName;
|
|
||||||
@observable
|
|
||||||
String description;
|
|
||||||
@observable
|
|
||||||
String imagePath;
|
|
||||||
@observable
|
@observable
|
||||||
AddressSource sourceType;
|
AddressSource sourceType;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
CryptoCurrency currency;
|
CryptoCurrency currency;
|
||||||
@observable
|
@observable
|
||||||
String manualAddress = '';
|
String label, address, handleKey;
|
||||||
@observable
|
|
||||||
String manualLabel = '';
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
ObservableMap<CryptoCurrency, Map<String, String>> parsedAddressesByCurrency = ObservableMap<
|
ObservableMap<CryptoCurrency, Map<String, String>> manual = ObservableMap();
|
||||||
CryptoCurrency,
|
|
||||||
Map<String, String>>();
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
ObservableMap<CryptoCurrency, Map<String, String>> manualAddressesByCurrency = ObservableMap<
|
ObservableMap<CryptoCurrency, Map<String, String>> parsed = ObservableMap();
|
||||||
CryptoCurrency,
|
@observable
|
||||||
Map<String, String>>();
|
ObservableMap<String, Map<CryptoCurrency, Map<String, String>>> parsedBlocks = ObservableMap();
|
||||||
|
|
||||||
final Box<Contact> _box;
|
final ContactEditMode mode;
|
||||||
final ContactRecord? contactRecord;
|
|
||||||
final List<CryptoCurrency> currencies;
|
final List<CryptoCurrency> currencies;
|
||||||
late final bool isNewAddress;
|
|
||||||
CryptoCurrency? initialCurrency;
|
CryptoCurrency? _originalCur;
|
||||||
|
String? _originalLabel, _originalAddress, _originalHandleKey;
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
bool get isReady =>
|
bool get isReady => name.trim().isNotEmpty || manual.isNotEmpty || parsed.isNotEmpty;
|
||||||
name
|
|
||||||
.trim()
|
|
||||||
.isNotEmpty && parsedAddressesByCurrency.isNotEmpty;
|
|
||||||
|
|
||||||
ImageProvider get avatarProvider {
|
@computed
|
||||||
final file = avatarFile;
|
List<UserHandles> get userHandles =>
|
||||||
return (file != null && file.existsSync())
|
parsedBlocks.keys.map((k) => UserHandles(handleKey: k)).toList();
|
||||||
? FileImage(file)
|
|
||||||
: const AssetImage('assets/images/profile.png');
|
@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
|
@action
|
||||||
void updateManualAddress() {
|
Future<void> saveManualAddress() async {
|
||||||
if (manualAddress
|
_ensureRecord();
|
||||||
.trim()
|
|
||||||
.isEmpty) return;
|
|
||||||
|
|
||||||
final inner = manualAddressesByCurrency.putIfAbsent(currency, () => {});
|
final map = manual.putIfAbsent(currency, () => {});
|
||||||
final base = manualLabel
|
final oldLabel = isAddressEdit ? _originalLabel : null;
|
||||||
.trim()
|
final newLabel = label.trim().isEmpty ? currency.title : label.trim();
|
||||||
.isEmpty ? currency.title : manualLabel.trim();
|
final newAddress = address.trim();
|
||||||
|
|
||||||
var label = base;
|
if (oldLabel != null && oldLabel != newLabel) map.remove(oldLabel);
|
||||||
var i = 1;
|
map[newLabel] = newAddress;
|
||||||
while (inner.containsKey(label)) {
|
manual[currency] = Map.of(map);
|
||||||
label = '$base $i';
|
|
||||||
i++;
|
record!.setManualAddress(currency, newLabel, newAddress);
|
||||||
|
_rememberOriginal();
|
||||||
|
state = ExecutedSuccessfullyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
inner[label] = manualAddress.trim();
|
state = ExecutedSuccessfullyState();
|
||||||
|
|
||||||
manualAddressesByCurrency[currency] = Map<String, String>.of(inner);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> pickAvatar(String localPath) async {
|
Future<void> deleteParsedBlock(String handleKey) async {
|
||||||
imagePath = localPath;
|
if (!parsedBlocks.containsKey(handleKey)) return;
|
||||||
|
|
||||||
|
parsedBlocks.remove(handleKey);
|
||||||
|
record!.removeParsedAddress(handleKey, null, null);
|
||||||
|
state = ExecutedSuccessfullyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteManualAddress(CryptoCurrency cur, String label) {
|
@action
|
||||||
final inner = manualAddressesByCurrency[cur];
|
Future<void> deleteContact() async {
|
||||||
if (inner == null) return;
|
if (record == null) return;
|
||||||
inner.remove(label);
|
|
||||||
manualAddressesByCurrency[cur] = Map<String, String>.of(inner);
|
await record!.original.delete();
|
||||||
|
record = null;
|
||||||
|
reset();
|
||||||
|
state = ExecutedSuccessfullyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
void reset() {
|
void reset() {
|
||||||
name = '';
|
name = handle = profileName = description = imagePath = '';
|
||||||
handle = '';
|
label = address = handleKey = '';
|
||||||
profileName = '';
|
currency = CryptoCurrency.xmr;
|
||||||
description = '';
|
manual.clear();
|
||||||
imagePath = '';
|
parsed.clear();
|
||||||
parsedAddressesByCurrency.clear();
|
parsedBlocks.clear();
|
||||||
manualAddressesByCurrency.clear();
|
_originalCur = null;
|
||||||
|
_originalLabel = null;
|
||||||
|
_originalAddress = null;
|
||||||
|
_originalHandleKey = null;
|
||||||
|
state = InitialExecutionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> save() async {
|
void _initMapsFromRecord() {
|
||||||
|
if (record == null) return;
|
||||||
|
|
||||||
try {
|
manual = ObservableMap.of(record!.manual);
|
||||||
state = IsExecutingState();
|
parsed = ObservableMap.of(record!.parsedByCurrency);
|
||||||
|
parsedBlocks = ObservableMap.of(record!.parsedBlocks);
|
||||||
final clash = _box.values.any(
|
|
||||||
(c) => c.name == name && c.key != contactRecord?.original.key,
|
|
||||||
);
|
|
||||||
if (clash) {
|
|
||||||
state = FailureState(S.current.contact_name_exists);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File? get avatarFile => imagePath.isEmpty ? null : File(imagePath);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -711,7 +711,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
contactListViewModel.contactSource,
|
contactListViewModel.contactSource,
|
||||||
Contact(
|
Contact(
|
||||||
name: '',
|
name: '',
|
||||||
parsedAddresses: {3: {'label4':address}
|
parsedByHandle: {
|
||||||
|
'handle':
|
||||||
|
|
||||||
|
{
|
||||||
|
3: {'label4': address}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value
|
manualAddresses: {3: {'label5':address} //TODO : Fix this hardcoded value
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue