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