mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +01:00
Merge branch 'refs/heads/dev' into cw-1440-new-buy-sell-flow
# Conflicts: # lib/new-ui/widgets/coins_page/cards/cards_view.dart # lib/new-ui/widgets/receive_page/receive_top_bar.dart
This commit is contained in:
+10
-18
@@ -687,28 +687,20 @@ class CWBitcoin extends Bitcoin {
|
||||
}
|
||||
|
||||
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
|
||||
if (pendingTransaction is PendingLightningTransaction) return outputs;
|
||||
|
||||
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
|
||||
|
||||
if (!pendingTx.hasSilentPayment) {
|
||||
if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) {
|
||||
return outputs;
|
||||
}
|
||||
|
||||
final updatedOutputs = outputs.map((output) {
|
||||
try {
|
||||
final pendingOut = pendingTx.outputs[outputs.indexOf(output)];
|
||||
final updatedOutput = output;
|
||||
final stealthAddresses = pendingTransaction.stealthAddresses;
|
||||
if (stealthAddresses.length != outputs.length) {
|
||||
printV("well, that shouldn't happen");
|
||||
return outputs;
|
||||
}
|
||||
|
||||
updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey)
|
||||
.toAddress(BitcoinNetwork.mainnet);
|
||||
return updatedOutput;
|
||||
} catch (_) {}
|
||||
|
||||
return output;
|
||||
}).toList();
|
||||
|
||||
return updatedOutputs;
|
||||
for (var i = 0; i < outputs.length; i++) {
|
||||
outputs[i].stealthAddress = stealthAddresses[i];
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -336,14 +336,23 @@ class DFXBuyProvider extends BuyProvider {
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
}));
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isReconnect: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Recheck to handle tap-backs
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
hardwareWalletVM!.initWallet(wallet);
|
||||
}
|
||||
|
||||
@@ -174,14 +174,21 @@ class RobinhoodBuyProvider extends BuyProvider {
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
}));
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isReconnect: false,
|
||||
),
|
||||
);
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
hardwareWalletVM!.initWallet(wallet);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class RoundedOverlayCards extends StatelessWidget {
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
BorderRadius.only(bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
@@ -30,8 +30,8 @@ class RoundedOverlayCards extends StatelessWidget {
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0),
|
||||
bottomRight: Radius.circular(25.0),
|
||||
bottomLeft: Radius.circular(25),
|
||||
bottomRight: Radius.circular(25),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -20,7 +20,7 @@ class YatAddressProvider extends AddressLookupProvider {
|
||||
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsYatService;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
|
||||
@@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider {
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/core/validator.dart';
|
||||
import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cake_wallet/zcash/zcash_network_type.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
|
||||
@@ -10,8 +11,11 @@ const BEFORE_REGEX = '(^|\\s)';
|
||||
const AFTER_REGEX = '(\$|\\s)';
|
||||
|
||||
class AddressValidator extends TextValidator {
|
||||
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
|
||||
: super(
|
||||
AddressValidator({
|
||||
required CryptoCurrency type,
|
||||
bool isTestnet = false,
|
||||
String? network,
|
||||
}) : super(
|
||||
errorMessage: S.current.error_text_address,
|
||||
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
|
||||
? (String txt) {
|
||||
@@ -32,7 +36,7 @@ class AddressValidator extends TextValidator {
|
||||
: type == CryptoCurrency.zano
|
||||
? zano?.validateAddress
|
||||
: null,
|
||||
pattern: getPattern(type, isTestnet: isTestnet),
|
||||
pattern: getPattern(type, isTestnet: isTestnet, network: network),
|
||||
length: getLength(type),
|
||||
);
|
||||
|
||||
@@ -61,7 +65,7 @@ class AddressValidator extends TextValidator {
|
||||
CryptoCurrency.btcln,
|
||||
];
|
||||
|
||||
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
|
||||
static String? getPattern(CryptoCurrency type, {bool isTestnet = false, String? network}) {
|
||||
var pattern = "";
|
||||
if (type is Erc20Token) {
|
||||
pattern = '0x[0-9a-zA-Z]+';
|
||||
@@ -170,6 +174,9 @@ class AddressValidator extends TextValidator {
|
||||
case CryptoCurrency.hbar:
|
||||
pattern = '[0-9a-zA-Z.]+';
|
||||
case CryptoCurrency.zec:
|
||||
if (ZcashNetworkType.isDevNetwork(network)) {
|
||||
return null;
|
||||
}
|
||||
pattern = '(?:'
|
||||
't1[0-9A-Za-z]{33}'
|
||||
'|t3[0-9A-Za-z]{33}'
|
||||
|
||||
@@ -210,7 +210,11 @@ class BackgroundSync {
|
||||
}
|
||||
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
|
||||
tx.date.add(Duration(minutes: 1)).toIso8601String());
|
||||
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
|
||||
final action = tx.additionalInfo['isAutoShield'] == true
|
||||
? "Autoshield"
|
||||
: tx.direction == TransactionDirection.incoming
|
||||
? "Received"
|
||||
: "Sent";
|
||||
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
|
||||
await showNotification(
|
||||
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import "dart:convert";
|
||||
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import "package:cw_core/utils/proxy_wrapper.dart";
|
||||
|
||||
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
|
||||
try {
|
||||
@@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) {
|
||||
}
|
||||
|
||||
class _LNURLPResponseDTO {
|
||||
const _LNURLPResponseDTO(
|
||||
this.callback,
|
||||
this.maxSendable,
|
||||
this.minSendable,
|
||||
this.tag,
|
||||
this.metadata,
|
||||
);
|
||||
|
||||
factory _LNURLPResponseDTO.fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
|
||||
map["callback"] as String,
|
||||
map["maxSendable"] as int,
|
||||
map["minSendable"] as int,
|
||||
map["tag"] as String,
|
||||
map["metadata"] as String,
|
||||
);
|
||||
|
||||
final String callback;
|
||||
final int maxSendable;
|
||||
final int minSendable;
|
||||
final String tag;
|
||||
final String metadata;
|
||||
|
||||
const _LNURLPResponseDTO(
|
||||
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
|
||||
|
||||
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
|
||||
map["callback"] as String,
|
||||
map["maxSendable"] as int,
|
||||
map["minSendable"] as int,
|
||||
map["tag"] as String,
|
||||
map["metadata"] as String);
|
||||
}
|
||||
|
||||
class _LNURLPCallbackResponseDTO {
|
||||
final String pr;
|
||||
|
||||
const _LNURLPCallbackResponseDTO(this.pr);
|
||||
|
||||
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
|
||||
factory _LNURLPCallbackResponseDTO.fromJson(Map<String, dynamic> map) =>
|
||||
_LNURLPCallbackResponseDTO(map["pr"] as String);
|
||||
|
||||
final String pr;
|
||||
}
|
||||
|
||||
+3
-1
@@ -436,7 +436,9 @@ Future<void> setup({
|
||||
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
|
||||
getIt.registerLazySingleton(
|
||||
() => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
|
||||
);
|
||||
|
||||
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/fs_migration.dart';
|
||||
import 'package:cake_wallet/entities/haven_seed_store.dart';
|
||||
import 'package:cake_wallet/entities/node_list.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/entities/secret_store_key.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
@@ -46,7 +45,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
|
||||
const nanoDefaultNodeUri = 'rpc.nano.to';
|
||||
const nanoDefaultPowNodeUri = 'rpc.nano.to';
|
||||
const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com';
|
||||
const tronDefaultNodeUri = 'api.trongrid.io';
|
||||
const tronDefaultNodeUri = 'trx.nownodes.io';
|
||||
const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
|
||||
const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568';
|
||||
const zanoDefaultNodeUri = '37.27.100.59:10500';
|
||||
@@ -56,7 +55,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
|
||||
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
|
||||
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
|
||||
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
|
||||
const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
|
||||
const zcashDefaultNodeUri = 'zec.rocks:443';
|
||||
|
||||
Future<void> defaultSettingsMigration(
|
||||
{required int version,
|
||||
@@ -607,6 +606,38 @@ Future<void> defaultSettingsMigration(
|
||||
enabled: true,
|
||||
);
|
||||
break;
|
||||
case 68:
|
||||
await _changeDefaultNode(
|
||||
sharedPreferences: sharedPreferences,
|
||||
type: WalletType.tron,
|
||||
newDefaultUri: tronDefaultNodeUri,
|
||||
currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey,
|
||||
useSSL: true,
|
||||
oldUri: [
|
||||
'tron-rpc.publicnode.com:443',
|
||||
'api.trongrid.io',
|
||||
],
|
||||
);
|
||||
await _changeDefaultNode(
|
||||
sharedPreferences: sharedPreferences,
|
||||
type: WalletType.monero,
|
||||
newDefaultUri: newCakeWalletMoneroUri,
|
||||
currentNodePreferenceKey: PreferencesKey.currentNodeIdKey,
|
||||
useSSL: true,
|
||||
trusted: true,
|
||||
oldUri: ['nodes.hashvault.pro:18081'],
|
||||
);
|
||||
break;
|
||||
case 69:
|
||||
_changeExchangeProviderAvailability(
|
||||
sharedPreferences,
|
||||
providerName: "Exolix",
|
||||
enabled: false,
|
||||
);
|
||||
break;
|
||||
case 70:
|
||||
await _addTbbTokenToExistingSolanaWallets();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -663,7 +694,7 @@ Future<void> _changeDefaultNode({
|
||||
} else {
|
||||
final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId);
|
||||
shouldReplace =
|
||||
currentNode == null || (oldUri?.any((e) => currentNode!.uriRaw.contains(e)) ?? true);
|
||||
currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true);
|
||||
}
|
||||
|
||||
if (shouldReplace) {
|
||||
@@ -1297,3 +1328,40 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
|
||||
printV('Error in XAUT0 migration: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addTbbTokenToExistingSolanaWallets() async {
|
||||
try {
|
||||
final tbbToken = SPLToken(
|
||||
name: "The Bitcoin Bull",
|
||||
symbol: "TBB",
|
||||
mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump",
|
||||
decimal: 6,
|
||||
mint: "tbb",
|
||||
enabled: false,
|
||||
iconPath: "assets/images/tbb_icon.png",
|
||||
);
|
||||
|
||||
final allWallets = await WalletInfo.getAll();
|
||||
|
||||
final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
|
||||
|
||||
for (final walletInfo in solanaWallets) {
|
||||
final sanitizedName = walletInfo.name.replaceAll(" ", "_");
|
||||
final boxName = "${sanitizedName}_${SPLToken.boxName}";
|
||||
|
||||
Box<SPLToken> tokenBox;
|
||||
if (CakeHive.isBoxOpen(boxName)) {
|
||||
tokenBox = CakeHive.box<SPLToken>(boxName);
|
||||
} else {
|
||||
tokenBox = await CakeHive.openBox<SPLToken>(boxName);
|
||||
}
|
||||
|
||||
final tbbAddress = tbbToken.mintAddress;
|
||||
if (!tokenBox.containsKey(tbbAddress)) {
|
||||
await tokenBox.put(tbbAddress, tbbToken);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV("Error in TBB migration: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +145,5 @@ class PreferencesKey {
|
||||
static const backgroundImage = 'background_image';
|
||||
static const mwebAdDismissed = "mweb_ad_dismissed";
|
||||
static const balanceHideCounter = "balance_hide_counter";
|
||||
static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: S.of(context).error_dialog_content,
|
||||
buttonText: 'ok',
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
||||
@@ -325,6 +325,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
return 'btc';
|
||||
case CryptoCurrency.arb:
|
||||
return 'arbitrum';
|
||||
case CryptoCurrency.nano:
|
||||
return 'nano';
|
||||
default:
|
||||
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase();
|
||||
}
|
||||
@@ -337,6 +339,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.zec:
|
||||
return 'zec';
|
||||
case CryptoCurrency.maticpoly:
|
||||
return 'matic';
|
||||
default:
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
@@ -344,7 +348,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
|
||||
String _normalizeTag(String tag) {
|
||||
switch (tag) {
|
||||
case 'POLY':
|
||||
case 'POL':
|
||||
return 'matic';
|
||||
case 'LN':
|
||||
return 'lightning';
|
||||
|
||||
@@ -539,7 +539,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
Future<Uri> _getUri(String path, Map<String, String> queryParams) async {
|
||||
final uri = Uri.http(onionApiAuthority, path, queryParams);
|
||||
final uri = Uri.https(onionApiAuthority, path, queryParams);
|
||||
|
||||
if (useTorOnly) return uri;
|
||||
|
||||
|
||||
+5
-3
@@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
|
||||
import 'package:cake_wallet/exchange/trade_legacy.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/locales/locale.dart';
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
|
||||
import 'package:cake_wallet/order/order.dart';
|
||||
import 'package:cake_wallet/reactions/bootstrap.dart';
|
||||
import 'package:cake_wallet/router.dart' as Router;
|
||||
@@ -315,7 +316,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
havenSeedStore: havenSeedStore,
|
||||
initialMigrationVersion: 67,
|
||||
initialMigrationVersion: 70,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -425,8 +426,9 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
|
||||
child: child!),
|
||||
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
|
||||
child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
|
||||
),
|
||||
theme: theme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: themeMode,
|
||||
|
||||
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
|
||||
required String password,
|
||||
required int height,
|
||||
required HardwareWalletService hardwareWalletService,
|
||||
required String? passphrase,
|
||||
}) =>
|
||||
MoneroRestoreWalletFromHardwareCredentials(
|
||||
name: name,
|
||||
password: password,
|
||||
height: height,
|
||||
hardwareWalletService: hardwareWalletService,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -418,6 +420,12 @@ class CWMonero extends Monero {
|
||||
return moneroWallet.needExportOutputs(amount);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hasUnknownKeyImages(Object wallet) {
|
||||
final moneroWallet = wallet as MoneroWallet;
|
||||
return moneroWallet.hasUnknownKeyImages();
|
||||
}
|
||||
|
||||
@override
|
||||
void monerocCheck() => checkIfMoneroCIsFine();
|
||||
|
||||
|
||||
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: Container(color: Colors.transparent),
|
||||
// Invisible pointer-absorbing strip behind the nav bar: it must not
|
||||
// be reachable by screen-reader traversal either.
|
||||
ExcludeSemantics(
|
||||
child: SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
|
||||
topBar: ModalTopBar(
|
||||
title: S.of(context).about,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
content: Container(
|
||||
|
||||
@@ -128,8 +128,10 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).wallet_accounts,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: Icon(Icons.refresh),
|
||||
trailingSemanticLabel: S.of(context).reset,
|
||||
onTrailingPressed: showResetDialog,
|
||||
),
|
||||
Padding(
|
||||
@@ -172,17 +174,29 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final card = _items[index].card;
|
||||
// The stack is ordered bottom to top, so the last item
|
||||
// is the account currently in front — the selected one.
|
||||
final selectedItemIndex = _items.length - 1;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(index),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
reorder(index, _items.length);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: card,
|
||||
// One labeled, selectable node per account; the card's own
|
||||
// texts stay reachable underneath it.
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selectedItemIndex == index,
|
||||
label: _items[index].accountListItem.label,
|
||||
onTap: () => reorder(index, _items.length),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: () {
|
||||
reorder(index, _items.length);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -197,31 +211,36 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
onTap: _showAddAccountModal,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
S.of(context).add_account,
|
||||
style: TextStyle(
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
onTap: _showAddAccountModal,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
S.of(context).add_account,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -404,6 +423,8 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
|
||||
Future<void> _generateAccountName() async => _controller.text = await generateName();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -439,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_controller.text = await generateName();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/randomize.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).generate_name,
|
||||
onTap: _generateAccountName,
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: _generateAccountName,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/randomize.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import 'package:cake_wallet/utils/list_item.dart';
|
||||
@@ -105,6 +106,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
ModalTopBar(
|
||||
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
|
||||
leadingIcon: Icon(Icons.arrow_back),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {}),
|
||||
Expanded(
|
||||
@@ -212,24 +214,31 @@ class AddressSearchBox extends StatelessWidget {
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
|
||||
borderRadius: BorderRadius.circular(99999)),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
// The hint names the field only while it is empty, so the label is
|
||||
// supplied once there is text to keep exactly one announcement.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: controller.text.isEmpty ? null : S.of(context).search,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
prefixIcon: ExcludeSemantics(child: Icon(Icons.search)),
|
||||
filled: false,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
prefixIcon: Icon(Icons.search),
|
||||
filled: false,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -333,116 +342,139 @@ class AddressRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasLabel = item.name != null && item.name!.isNotEmpty;
|
||||
final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onSelect,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LongPressPopupBuilder(
|
||||
popup: LongPressMenu(
|
||||
items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).set_label,
|
||||
iconPath: "assets/new-ui/address_set_label.svg",
|
||||
onSelected: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
final res = await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
|
||||
Future<void> editLabel() async {
|
||||
final res = await showPopUp(
|
||||
context: context, builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
|
||||
|
||||
if (res != null) {
|
||||
onLabelChanged();
|
||||
}
|
||||
}),
|
||||
LongPressMenuItem(
|
||||
label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onAddressHidden();
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
LongPressMenuItem(
|
||||
label: 'Info',
|
||||
iconPath: "assets/images/info_icon.svg",
|
||||
onSelected: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
|
||||
}),
|
||||
],
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(first ? 16 : 0),
|
||||
bottom: Radius.circular(last ? 16 : 0),
|
||||
if (res != null) {
|
||||
onLabelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showInfo() async {
|
||||
await showPopUp(
|
||||
context: context, builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
|
||||
}
|
||||
|
||||
// The row is a single control: selection on tap, and the actions that are
|
||||
// otherwise reachable only by long press exposed as custom actions.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
|
||||
CustomSemanticsAction(label: S.of(context).set_label): editLabel,
|
||||
CustomSemanticsAction(label: hideLabel): onAddressHidden,
|
||||
CustomSemanticsAction(label: S.of(context).info): showInfo,
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: onSelect,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LongPressPopupBuilder(
|
||||
popup: LongPressMenu(
|
||||
items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).set_label,
|
||||
iconPath: "assets/new-ui/address_set_label.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
editLabel();
|
||||
}),
|
||||
LongPressMenuItem(
|
||||
label: hideLabel,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onAddressHidden();
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).info,
|
||||
iconPath: "assets/images/info_icon.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showInfo();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(first ? 16 : 0),
|
||||
bottom: Radius.circular(last ? 16 : 0),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (hasLabel)
|
||||
Text(
|
||||
item.name!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Expanded(
|
||||
child: AddressFormatter.buildSegmentedAddress(
|
||||
address: item.address,
|
||||
walletType: walletType,
|
||||
textAlign: TextAlign.left,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (hasLabel)
|
||||
Text(
|
||||
item.name!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: hasLabel
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
shouldTruncate: (hasLabel)),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Expanded(
|
||||
child: AddressFormatter.buildSegmentedAddress(
|
||||
address: item.address,
|
||||
walletType: walletType,
|
||||
textAlign: TextAlign.left,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: hasLabel
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
shouldTruncate: (hasLabel)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).transactions}: ${item.txCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).transactions}: ${item.txCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
// The row's selected state carries this information.
|
||||
if (selected)
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg"))
|
||||
],
|
||||
),
|
||||
if (selected) CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg")
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -462,26 +494,34 @@ class ShowHiddenButton extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).show_hidden_addresses),
|
||||
RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg"))
|
||||
],
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).show_hidden_addresses),
|
||||
ExcludeSemantics(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child:
|
||||
CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).enter_amount,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||
trailingIcon: Icon(Icons.history),
|
||||
trailingSemanticLabel: S.of(context).history,
|
||||
onTrailingPressed: () {
|
||||
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
|
||||
arguments: widget.bridgeHistoryViewModel);
|
||||
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
iconColor: theme.colorScheme.onPrimary,
|
||||
icon: Icon(Icons.arrow_forward, size: 20),
|
||||
semanticLabel: S.of(context).continue_text,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
|
||||
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
|
||||
],
|
||||
),
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "Bridge Transfer Details",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
|
||||
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "Bridge history",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: 'Destination Network',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
|
||||
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
|
||||
ModalTopBar(
|
||||
title: 'Input Receive Address',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
|
||||
ModalTopBar(
|
||||
title: 'Receiving Wallet',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
@@ -84,6 +84,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
ModalTopBar(
|
||||
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
// onTrailingPressed: () {},
|
||||
|
||||
@@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/transaction_direction.dart";
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -48,6 +52,24 @@ class _NewHomePageState extends State<NewHomePage> {
|
||||
_lightningMode = false;
|
||||
});
|
||||
});
|
||||
|
||||
reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
|
||||
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
|
||||
if(!context.mounted) {
|
||||
return;
|
||||
}
|
||||
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ZcashMigrationModal(
|
||||
hasContinue: true,
|
||||
balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable
|
||||
.toStringWithSymbol(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setAccountViewModel() {
|
||||
|
||||
@@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
|
||||
ModalTopBar(
|
||||
title: "Lightning ${S.of(context).username}",
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
+138
-116
@@ -1,54 +1,54 @@
|
||||
import 'package:cake_wallet/core/utilities.dart';
|
||||
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart';
|
||||
import 'package:cake_wallet/utils/share_util.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cake_wallet/zcash/zcash.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart";
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:cake_wallet/core/utilities.dart";
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/auto_generate_subaddress_status.dart";
|
||||
import "package:cake_wallet/entities/preferences_key.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart";
|
||||
import "package:cake_wallet/utils/share_util.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart";
|
||||
import "package:cake_wallet/zcash/zcash.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/payment_uris.dart";
|
||||
import "package:cw_core/receive_page_option.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
class NewReceivePage extends StatefulWidget {
|
||||
NewReceivePage(
|
||||
{super.key,
|
||||
required this.addressListViewModel,
|
||||
required this.receiveOptionViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.lightningMode,
|
||||
CryptoCurrency? initialCurrency}) {
|
||||
NewReceivePage({
|
||||
required this.addressListViewModel,
|
||||
required this.receiveOptionViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.lightningMode,
|
||||
super.key,
|
||||
CryptoCurrency? initialCurrency,
|
||||
}) {
|
||||
if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
|
||||
addressListViewModel.setTokenCurrency(initialCurrency);
|
||||
}
|
||||
@@ -78,31 +78,33 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
.firstWhereOrNull((item) => item.value.contains("Lightning")) ??
|
||||
ReceivePageOption.mainnet,
|
||||
);
|
||||
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
|
||||
} else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
|
||||
widget.receiveOptionViewModel.selectReceiveOption(
|
||||
widget.receiveOptionViewModel.options
|
||||
.firstWhereOrNull((item) => item.value.contains("Standard")) ??
|
||||
ReceivePageOption.mainnet,
|
||||
);
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => widget.addressListViewModel.uri, (newAddress) {
|
||||
_reloadAddressWithLabel(newAddress);
|
||||
});
|
||||
reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel);
|
||||
|
||||
_addressItemWithLabel =
|
||||
widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) {
|
||||
return (item is WalletAddressListItem &&
|
||||
item.address == widget.addressListViewModel.uri.address);
|
||||
}) as WalletAddressListItem?;
|
||||
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
|
||||
(item) =>
|
||||
item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address,
|
||||
) as WalletAddressListItem?;
|
||||
|
||||
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption,
|
||||
(ReceivePageOption option) {
|
||||
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin &&
|
||||
bitcoin!.isBitcoinReceivePageOption(option)) {
|
||||
widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
|
||||
if (option.value.contains("Lightning")) {
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
|
||||
} else {
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (widget.dashboardViewModel.type == WalletType.zcash) {
|
||||
@@ -166,16 +168,18 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
|
||||
final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
|
||||
final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
|
||||
final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type,
|
||||
supportedCurrencies: widget.addressListViewModel.tokenCurrencies
|
||||
.whereType<CryptoCurrency>()
|
||||
.toList(), onDismissed: () {
|
||||
widget.addressListViewModel.dismissInfobox();
|
||||
setState(() {});
|
||||
},
|
||||
autoGenerateSubaddressStatus: widget.lightningMode
|
||||
? AutoGenerateSubaddressStatus.disabled
|
||||
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus);
|
||||
final infobox = ReceiveInfoBox.forWalletType(
|
||||
widget.addressListViewModel.type,
|
||||
supportedCurrencies:
|
||||
widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(),
|
||||
onDismissed: () {
|
||||
widget.addressListViewModel.dismissInfobox();
|
||||
setState(() {});
|
||||
},
|
||||
autoGenerateSubaddressStatus: widget.lightningMode
|
||||
? AutoGenerateSubaddressStatus.disabled
|
||||
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -187,9 +191,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(24),
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
@@ -198,13 +200,14 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _largeQrMode ? "" : S.of(context).receive,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
trailingWidget: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _largeQrMode ||
|
||||
widget.addressListViewModel.hasAddressRotation
|
||||
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
|
||||
@@ -217,10 +220,13 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
key: ValueKey(_largeQrMode),
|
||||
size: 36,
|
||||
icon: _largeQrMode
|
||||
? Icon(Icons.share)
|
||||
? const Icon(Icons.share)
|
||||
: widget.addressListViewModel.isRotatingAddress
|
||||
? CupertinoActivityIndicator()
|
||||
: Icon(Icons.refresh),
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(Icons.refresh),
|
||||
semanticLabel: _largeQrMode
|
||||
? S.of(context).share_address
|
||||
: S.of(context).rotate_address,
|
||||
onPressed: () {
|
||||
if (_largeQrMode) {
|
||||
ShareUtil.share(
|
||||
@@ -232,8 +238,9 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
widget.addressListViewModel.rotateAddress();
|
||||
}
|
||||
}
|
||||
})
|
||||
: SizedBox.shrink(),
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -268,12 +275,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
ReceiveAddressWidget(
|
||||
addressListViewModel: widget.addressListViewModel,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _showLabelModal,
|
||||
child: ReceiveLabelWidget(
|
||||
name: _addressItemWithLabel?.name ?? "",
|
||||
largeQrMode: _largeQrMode,
|
||||
)),
|
||||
// The label chip animates to zero height when there is no
|
||||
// label (or in large QR mode); keep it out of the semantics
|
||||
// tree entirely while it is collapsed.
|
||||
ExcludeSemantics(
|
||||
excluding: _largeQrMode || !hasLabel,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
hint: S.of(context).set_label,
|
||||
child: GestureDetector(
|
||||
onTap: _showLabelModal,
|
||||
child: ReceiveLabelWidget(
|
||||
name: _addressItemWithLabel?.name ?? "",
|
||||
largeQrMode: _largeQrMode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => ReceiveBottomButtons(
|
||||
key: const ValueKey(0),
|
||||
@@ -285,14 +305,16 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
: ClipboardData(
|
||||
text: widget.addressListViewModel.displayAmount.isEmpty
|
||||
? widget.addressListViewModel.uri.address
|
||||
: widget.addressListViewModel.uri.toString()),
|
||||
: widget.addressListViewModel.uri.toString(),
|
||||
),
|
||||
onCopyButtonPressed: () {
|
||||
if (widget.addressListViewModel.hasPayjoin) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PayjoinCopyModal(uri: widget.addressListViewModel.uri));
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PayjoinCopyModal(uri: widget.addressListViewModel.uri),
|
||||
);
|
||||
}
|
||||
},
|
||||
onAmountButtonPressed: () {
|
||||
@@ -300,12 +322,10 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (context) {
|
||||
return ReceiveAmountModal(
|
||||
walletAddressListViewModel: widget.addressListViewModel,
|
||||
onSubmitted: (amount) {},
|
||||
);
|
||||
},
|
||||
builder: (context) => ReceiveAmountModal(
|
||||
walletAddressListViewModel: widget.addressListViewModel,
|
||||
onSubmitted: (amount) {},
|
||||
),
|
||||
);
|
||||
},
|
||||
onLabelButtonPressed: _showLabelModal,
|
||||
@@ -318,22 +338,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
),
|
||||
),
|
||||
ReceiveLargeAmountPreview(
|
||||
amount: widget.addressListViewModel.displayAmount,
|
||||
currency: widget.addressListViewModel.cryptoCurrencySymbol,
|
||||
largeQrMode: _largeQrMode),
|
||||
amount: widget.addressListViewModel.displayAmount,
|
||||
currency: widget.addressListViewModel.cryptoCurrencySymbol,
|
||||
largeQrMode: _largeQrMode,
|
||||
),
|
||||
if (infobox != null && !widget.addressListViewModel.isLightning)
|
||||
ClipRect(
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
heightFactor: infoboxDismissed ? 0 : 1,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedOpacity(
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
heightFactor: infoboxDismissed ? 0 : 1,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: infoboxDismissed ? 0 : 1,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: infobox),
|
||||
))
|
||||
child: infobox,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -345,12 +368,11 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
|
||||
void _showLabelModal() {
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (context) {
|
||||
return getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel);
|
||||
}).then((value) {
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel),
|
||||
).then((value) {
|
||||
_reloadAddressWithLabel(widget.addressListViewModel.uri);
|
||||
});
|
||||
}
|
||||
@@ -359,8 +381,8 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
// FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
|
||||
setState(() {
|
||||
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
|
||||
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
|
||||
as WalletAddressListItem?;
|
||||
(item) => item is WalletAddressListItem && item.address == newAddress.address,
|
||||
) as WalletAddressListItem?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+130
-88
@@ -76,46 +76,50 @@ class _ScanPageState extends State<ScanPage> {
|
||||
controller: controller,
|
||||
onDetect: _handleBarcode,
|
||||
),
|
||||
// A full screen tap target would otherwise show up as one giant unlabeled
|
||||
// node; leaving text mode is also possible with the back button.
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_textInputMode = false;
|
||||
}),
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInQuad,
|
||||
duration: textModeSwitchDuration,
|
||||
child: _textInputMode
|
||||
? BackdropFilter(
|
||||
key: ValueKey(1),
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: Container(
|
||||
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
|
||||
),
|
||||
)
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: const ValueKey(0),
|
||||
tween: Tween<double>(begin: 0.0, end: targetRadius),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: BackdropFilter(
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_textInputMode = false;
|
||||
}),
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInQuad,
|
||||
duration: textModeSwitchDuration,
|
||||
child: _textInputMode
|
||||
? BackdropFilter(
|
||||
key: ValueKey(1),
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: Container(
|
||||
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
|
||||
),
|
||||
),
|
||||
builder: (context, radius, child) {
|
||||
return ClipPath(
|
||||
clipper: HoleClipper(
|
||||
width: cutoutSize,
|
||||
height: cutoutSize,
|
||||
radius: radius,
|
||||
)
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: const ValueKey(0),
|
||||
tween: Tween<double>(begin: 0.0, end: targetRadius),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: Container(
|
||||
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
builder: (context, radius, child) {
|
||||
return ClipPath(
|
||||
clipper: HoleClipper(
|
||||
width: cutoutSize,
|
||||
height: cutoutSize,
|
||||
radius: radius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -163,6 +167,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
},
|
||||
iconColor: buttonIconColor,
|
||||
backgroundColor: buttonColor,
|
||||
semanticLabel: S.of(context).switch_camera,
|
||||
),
|
||||
ModernButton(
|
||||
size: 36,
|
||||
@@ -171,6 +176,10 @@ class _ScanPageState extends State<ScanPage> {
|
||||
(controller.value.torchState == TorchState.on || _frontFlashMode)
|
||||
? Icons.flash_off_outlined
|
||||
: Icons.flash_on),
|
||||
semanticLabel:
|
||||
(controller.value.torchState == TorchState.on || _frontFlashMode)
|
||||
? S.of(context).turn_flash_off
|
||||
: S.of(context).turn_flash_on,
|
||||
onPressed: () {
|
||||
if (controller.value.cameraDirection == CameraFacing.front) {
|
||||
setState(() {
|
||||
@@ -194,6 +203,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
size: 36,
|
||||
iconSize: 18,
|
||||
icon: Icon(Icons.arrow_back_ios_new),
|
||||
semanticLabel: S.of(context).seed_alert_back,
|
||||
onPressed: () {
|
||||
if (_textInputMode) {
|
||||
setState(() {
|
||||
@@ -235,20 +245,25 @@ class _ScanPageState extends State<ScanPage> {
|
||||
spacing: 10,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
enabled: _textInputMode,
|
||||
controller: textController,
|
||||
focusNode: textFocusNode,
|
||||
onSubmitted: (val) {
|
||||
if (val.isNotEmpty) {
|
||||
Navigator.of(context).pop(val);
|
||||
} else {
|
||||
setState(() {
|
||||
_textInputMode = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(hintText: S.of(context).enter_code),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).enter_code,
|
||||
child: TextField(
|
||||
enabled: _textInputMode,
|
||||
controller: textController,
|
||||
focusNode: textFocusNode,
|
||||
onSubmitted: (val) {
|
||||
if (val.isNotEmpty) {
|
||||
Navigator.of(context).pop(val);
|
||||
} else {
|
||||
setState(() {
|
||||
_textInputMode = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(hintText: S.of(context).enter_code),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FloatingIconButton(
|
||||
@@ -330,6 +345,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
}
|
||||
},
|
||||
icon: Icons.question_mark,
|
||||
semanticsLabel: S.of(context).help,
|
||||
buttonColor: buttonColor,
|
||||
buttonIconColor: buttonIconColor)
|
||||
],
|
||||
@@ -360,27 +376,39 @@ class _ScanPageState extends State<ScanPage> {
|
||||
child: AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 500),
|
||||
opacity: isScanningURQR ? 1 : 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${decoder.processedPartsCount()}",
|
||||
style: TextStyle(
|
||||
fontSize: 45,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
// Three separate digits mean nothing on their own: announce the progress
|
||||
// as one node, and only while a multi part code is actually being scanned.
|
||||
child: ExcludeSemantics(
|
||||
excluding: !isScanningURQR,
|
||||
child: Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: S.of(context).qr_parts_scanned(
|
||||
"${decoder.processedPartsCount()}", "${decoder.expectedPartCount() ?? 0}"),
|
||||
excludeSemantics: true,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${decoder.processedPartsCount()}",
|
||||
style: TextStyle(
|
||||
fontSize: 45,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
"/",
|
||||
style: TextStyle(
|
||||
fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${decoder.expectedPartCount()}",
|
||||
style: TextStyle(
|
||||
fontSize: 45, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"/",
|
||||
style: TextStyle(
|
||||
fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${decoder.expectedPartCount()}",
|
||||
style:
|
||||
TextStyle(fontSize: 45, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -400,7 +428,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: S.of(context).error_dialog_content,
|
||||
buttonText: 'ok',
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -453,37 +481,51 @@ class ScanPageButton extends StatelessWidget {
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.label,
|
||||
this.semanticsLabel,
|
||||
required this.buttonColor,
|
||||
required this.buttonIconColor});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final IconData icon;
|
||||
final String? label;
|
||||
|
||||
/// Name for the icon-only variant, which has no visible [label].
|
||||
final String? semanticsLabel;
|
||||
final Color buttonColor;
|
||||
final Color buttonIconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: true,
|
||||
label: semanticsLabel ?? label,
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10, bottom: 10, left: label == null ? 10 : 16, right: label == null ? 10 : 20),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Icon(icon, size: 28, color: buttonIconColor),
|
||||
if (label != null)
|
||||
Text(label!,
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor))
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration:
|
||||
BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: label == null ? 10 : 16,
|
||||
right: label == null ? 10 : 20),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Icon(icon, size: 28, color: buttonIconColor),
|
||||
if (label != null)
|
||||
Text(label!,
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor))
|
||||
],
|
||||
),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+796
-705
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
bool _trueFunc(DashboardViewModel _) => true;
|
||||
|
||||
@@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
|
||||
|
||||
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
|
||||
|
||||
bool _isCupcake(DashboardViewModel vm) =>
|
||||
vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
|
||||
bool _requiresKeyImageSync(DashboardViewModel vm) =>
|
||||
vm.wallet.type == WalletType.monero &&
|
||||
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
|
||||
|
||||
class SettingsListItem {
|
||||
const SettingsListItem(
|
||||
this.iconPath,
|
||||
this.title,
|
||||
this.route, {
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc,
|
||||
this.routeArgs,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String route;
|
||||
@@ -37,21 +47,15 @@ class SettingsListItem {
|
||||
final bool requireAuth;
|
||||
final bool Function(DashboardViewModel) use2fa;
|
||||
final bool Function(DashboardViewModel) condition;
|
||||
|
||||
const SettingsListItem(this.iconPath, this.title, this.route,
|
||||
{this.routeArgs = null,
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc});
|
||||
}
|
||||
|
||||
class SettingsSectionData {
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
|
||||
final String title;
|
||||
final String titleIconPath;
|
||||
final List<SettingsListItem> items;
|
||||
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
|
||||
static SettingsSectionData walletSettings =
|
||||
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
|
||||
SettingsListItem(
|
||||
@@ -71,9 +75,13 @@ class SettingsSectionData {
|
||||
condition: _hasWalletConnect),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs,
|
||||
Routes.urqrAnimatedPage,
|
||||
routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/sync-balance.svg",
|
||||
S.current.resync_device,
|
||||
Routes.syncKeyImagesDevices,
|
||||
routeArgs: {'export-outputs': 'export-outputs'},
|
||||
condition: _requiresKeyImageSync,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
|
||||
]);
|
||||
@@ -152,8 +160,9 @@ class SettingsMainPage extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(children: [
|
||||
ModalTopBar(
|
||||
title: "Settings",
|
||||
title: S.of(context).settings_title,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
onTrailingPressed: () {},
|
||||
),
|
||||
|
||||
@@ -429,8 +429,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
final parsedAddress = await widget.adrResService
|
||||
.resolve(query: domain, wallet: widget.exchangeViewModel.wallet, currency: currency);
|
||||
return parsedAddress.isNotEmpty
|
||||
? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? ''
|
||||
: '';
|
||||
? (parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? domain)
|
||||
: domain;
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
@@ -472,12 +472,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).swap,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/options.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).configure,
|
||||
onTrailingPressed: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
@@ -576,6 +578,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
iconSize: 24,
|
||||
svgPath: "assets/new-ui/swap_amounts.svg",
|
||||
onPressed: widget.exchangeViewModel.reverseSwapDirection,
|
||||
semanticLabel: S.of(context).swap_reverse_direction,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1174,6 +1177,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
size: 36,
|
||||
iconSize: 18,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).refund_address,
|
||||
),
|
||||
if (widget.isReceiverCard &&
|
||||
widget.exchangeViewModel.receiveAddress.isEmpty) ...[
|
||||
@@ -1183,6 +1187,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
size: 36,
|
||||
iconSize: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).paste,
|
||||
),
|
||||
ModernButton.svg(
|
||||
svgPath: "assets/new-ui/scan.svg",
|
||||
@@ -1190,6 +1195,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
size: 36,
|
||||
iconSize: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).scan,
|
||||
),
|
||||
]
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
),
|
||||
padding: EdgeInsets.all(24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
// The card is one node; the arrow glyph's "leaves the app" meaning
|
||||
// becomes a hint instead of an unlabeled icon.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
hint: isLink == true ? S.of(context).opens_externally : null,
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
),
|
||||
padding: EdgeInsets.all(24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6.0,
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: ExcludeSemantics(
|
||||
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6.0,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
isCake == true
|
||||
? ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/cakelabs-icon.svg",
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant),
|
||||
)
|
||||
: SizedBox(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
subTitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
isCake == true
|
||||
? CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/cakelabs-icon.svg",
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)
|
||||
: SizedBox(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
subTitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
)
|
||||
if (hint != null) ...[
|
||||
SizedBox(height: 10),
|
||||
hint!,
|
||||
]
|
||||
],
|
||||
),
|
||||
if (hint != null) ...[
|
||||
SizedBox(height: 10),
|
||||
hint!,
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: onClose,
|
||||
//color: Theme.of(context).colorScheme.onSurface,
|
||||
// Label the icon-only button without adding a visible tooltip.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).close,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: onClose,
|
||||
//color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -107,7 +107,7 @@ class _ChangelogModalState extends State<ChangelogModal> {
|
||||
NewPrimaryButton(
|
||||
onPressed: () {
|
||||
try {
|
||||
launchUrl(Uri.https("blog.cakewallet.com"));
|
||||
launchUrl(Uri.https("docs.cakewallet.com", "/release-notes"));
|
||||
} catch (_) {}
|
||||
},
|
||||
text: S.of(context).view_more_info,
|
||||
|
||||
@@ -23,47 +23,57 @@ class CoinActionButton extends StatelessWidget {
|
||||
final size = MediaQuery.of(context).size.width * sizeFactor;
|
||||
final double effectiveSize = min(size, 80);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: effectiveSize,
|
||||
height: effectiveSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
action();
|
||||
},
|
||||
icon: icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
// The caption below the circle is the accessible name, so it is excluded
|
||||
// from the semantics tree to keep this a single button node.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: label,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: effectiveSize,
|
||||
height: effectiveSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
label,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
action();
|
||||
},
|
||||
icon: ExcludeSemantics(child: icon),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
label,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: Navigator.of(context).pop,
|
||||
padding: EdgeInsets.only(top: 12, right: 18),
|
||||
),
|
||||
@@ -356,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
onPressed: onPressed,
|
||||
svgPath: iconPath,
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
),
|
||||
Text(title)
|
||||
],
|
||||
// The caption is the button's accessible name, so it is excluded from the
|
||||
// semantics tree to keep the whole control a single button node.
|
||||
return MergeSemantics(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
onPressed: onPressed,
|
||||
svgPath: iconPath,
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
semanticLabel: title,
|
||||
),
|
||||
ExcludeSemantics(child: Text(title))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final iconPath = balance.asset.iconPath ?? "";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return AssetDetailsModal(
|
||||
showSwap: showSwap,
|
||||
showBridgeButton: showBridgeButton,
|
||||
asset: balance.asset,
|
||||
title: title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
chainTitle: "",
|
||||
subtitle: trailingText ?? _getChainTitle(),
|
||||
amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
|
||||
currencyTitle: balance.asset.title,
|
||||
fiatAmount: showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
iconPath: balance.asset.iconPath ?? "",
|
||||
chainIconPath: chainIconPath,
|
||||
mode: modalMode,
|
||||
wallet: wallet,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst ? Radius.circular(18) : Radius.zero,
|
||||
bottom: isLast ? Radius.circular(18) : Radius.zero,
|
||||
),
|
||||
),
|
||||
// The row is one control: name, amount and fiat value merge into a single
|
||||
// button node that opens the asset details sheet.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return AssetDetailsModal(
|
||||
showSwap: showSwap,
|
||||
showBridgeButton: showBridgeButton,
|
||||
asset: balance.asset,
|
||||
title: title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
chainTitle: "",
|
||||
subtitle: trailingText ?? _getChainTitle(),
|
||||
amount:
|
||||
showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
|
||||
currencyTitle: balance.asset.title,
|
||||
fiatAmount: showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
iconPath: balance.asset.iconPath ?? "",
|
||||
chainIconPath: chainIconPath,
|
||||
mode: modalMode,
|
||||
wallet: wallet,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
iconPath.isNotEmpty
|
||||
? TokenImageWidget(
|
||||
imageUrl: iconPath,
|
||||
size: 36,
|
||||
)
|
||||
: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
balance.asset.name
|
||||
.substring(0, min(2, balance.asset.name.length)),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst ? Radius.circular(18) : Radius.zero,
|
||||
bottom: isLast ? Radius.circular(18) : Radius.zero,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Decorative: the asset name is already in the row text.
|
||||
ExcludeSemantics(
|
||||
child: iconPath.isNotEmpty
|
||||
? TokenImageWidget(
|
||||
imageUrl: iconPath,
|
||||
size: 36,
|
||||
)
|
||||
: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
balance.asset.name
|
||||
.substring(0, min(2, balance.asset.name.length)),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
),
|
||||
SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (trailingText != null)
|
||||
Text(
|
||||
trailingText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (trailingText != null)
|
||||
Text(
|
||||
trailingText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -59,14 +59,13 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
AssetsSection(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
hasAssetsButton ?
|
||||
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
() {
|
||||
Navigator.of(context).pushNamed(
|
||||
hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
|
||||
() {Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: widget.dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
}) : null),
|
||||
}): null),
|
||||
AssetsHistorySectionTab(
|
||||
S.current.history,
|
||||
HistorySection(
|
||||
|
||||
@@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actionButton = tabs[selectedTab].actionButton;
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
|
||||
@@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget {
|
||||
key: ValueKey(selectedTab),
|
||||
spacing: 8,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: tabs[selectedTab].actionButton != null ? 1 : 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (tabs[selectedTab].actionButton != null) {
|
||||
tabs[selectedTab].actionButton?.onPressed();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty)
|
||||
Text(
|
||||
tabs[selectedTab].actionButton?.title ?? "",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: tabs[selectedTab].actionButton?.iconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
|
||||
],
|
||||
// Built conditionally rather than rendered at opacity 0: an
|
||||
// invisible chip stayed focusable for screen readers. The
|
||||
// SizedBox keeps the 40px header height the invisible chip
|
||||
// used to occupy.
|
||||
if (actionButton == null)
|
||||
const SizedBox(height: 40)
|
||||
else
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: actionButton.onPressed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (actionButton.title.isNotEmpty)
|
||||
Text(
|
||||
actionButton.title,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: actionButton.iconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -27,6 +27,7 @@ class HistoryFiltersPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).filters,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -25,11 +25,13 @@ class HistoryModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).history,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/tx_export.svg",
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).export_csv,
|
||||
onTrailingPressed: () =>
|
||||
CsvExportService().exportToCsv(dashboardViewModel.items, context),
|
||||
),
|
||||
|
||||
@@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget {
|
||||
final bool roundedTopSection;
|
||||
final bool detailsAsPage;
|
||||
|
||||
/// A history row is a single button node: every text inside it (direction,
|
||||
/// date, amounts) merges into one label.
|
||||
Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(onTap: onTap, child: child),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
@@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget {
|
||||
else
|
||||
asset = item.assetOfTransaction;
|
||||
|
||||
return GestureDetector(
|
||||
return _historyRow(
|
||||
onTap: () {
|
||||
final page =
|
||||
getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
@@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget {
|
||||
final tradeFrom = trade.from;
|
||||
final tradeTo = trade.to;
|
||||
|
||||
return GestureDetector(
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
child: HistoryTradeTile(
|
||||
@@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is OrderListItem) {
|
||||
return GestureDetector(
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: item.order),
|
||||
child: HistoryOrderTile(
|
||||
@@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget {
|
||||
} else if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return GestureDetector(
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
@@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget {
|
||||
} else if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return GestureDetector(
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.anonPayDetailsPage,
|
||||
arguments: transactionInfo),
|
||||
|
||||
@@ -24,6 +24,7 @@ class HistorySwapProvidersPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).filter_swap_providers,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget {
|
||||
date: date,
|
||||
amount: amount,
|
||||
amountFiat: amountFiat,
|
||||
leadingIcon: _getLeadingIcon(context),
|
||||
// Decorative: `title` already reads out sent/received/pending.
|
||||
leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)),
|
||||
primaryTextColor: _getPrimaryTextColor(),
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
|
||||
@@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: GestureDetector(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).history,
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox.shrink(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(S.of(context).history),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_right.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)
|
||||
SizedBox.shrink(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).history),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_right.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (!roundedBottom)
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
|
||||
)
|
||||
else
|
||||
Container(height: 2)
|
||||
],
|
||||
),
|
||||
if (!roundedBottom)
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
|
||||
)
|
||||
else
|
||||
Container(height: 2)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart";
|
||||
import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart";
|
||||
import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:cake_wallet/utils/address_formatter.dart";
|
||||
import "package:cake_wallet/view_model/transaction_details_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class TransactionDetailsModal extends StatefulWidget {
|
||||
const TransactionDetailsModal(
|
||||
{super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false});
|
||||
{
|
||||
required this.transactionDetailsViewModel,
|
||||
this.highlightNoteField = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TransactionDetailsViewModel transactionDetailsViewModel;
|
||||
final bool highlightNoteField;
|
||||
@@ -48,190 +52,230 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount;
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).transaction,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Column(
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl:
|
||||
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
|
||||
size: 64,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
CopyWrapper(
|
||||
requireLongPress: true,
|
||||
data: ClipboardData(text: transactionInfoAmount.toString()),
|
||||
builder: (context, copied) => AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
key: ValueKey(copied),
|
||||
copied
|
||||
? S.of(context).copied
|
||||
: transactionInfoAmount.toStringWithSymbol(),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: copied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).transaction,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Column(
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl:
|
||||
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => CopyWrapper(
|
||||
requireLongPress: true,
|
||||
data: ClipboardData(
|
||||
text: widget.transactionDetailsViewModel.transactionCopyAmount,
|
||||
),
|
||||
builder: (context, copied) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
key: ValueKey(copied),
|
||||
copied
|
||||
? S.of(context).copied
|
||||
: widget.transactionDetailsViewModel.transactionAmount,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: copied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) return null;
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final shouldBuildBottomWidget = item.value.length > 25;
|
||||
final shouldBuildBottomWidget = item.value.length > 25;
|
||||
|
||||
return ListItemRegularRow(
|
||||
copyableText: item.value,
|
||||
showArrow: false,
|
||||
keyValue: ((item.key as ValueKey?)?.value as String?) ??
|
||||
item.title,
|
||||
label: item.title,
|
||||
trailingWidget: shouldBuildBottomWidget
|
||||
? null
|
||||
: _buildTrailingWIdget(item),
|
||||
bottomWidget: shouldBuildBottomWidget
|
||||
? _buildBottomWidget(item)
|
||||
: null);
|
||||
})
|
||||
.whereType<ListItem>()
|
||||
.toList(),
|
||||
}),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
return ListItemRegularRow(
|
||||
copyableText: item.value,
|
||||
showArrow: false,
|
||||
keyValue: ((item.key as ValueKey?)?.value as String?) ??
|
||||
item.title,
|
||||
label: item.title,
|
||||
trailingWidget: shouldBuildBottomWidget
|
||||
? null
|
||||
: _buildTrailingWidget(item),
|
||||
bottomWidget: shouldBuildBottomWidget
|
||||
? _buildBottomWidget(item)
|
||||
: null,
|
||||
);
|
||||
})
|
||||
.whereType<ListItem>()
|
||||
.toList(),
|
||||
},
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).note),
|
||||
TextField(
|
||||
focusNode: noteFocusNode,
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).note),
|
||||
TextField(
|
||||
focusNode: noteFocusNode,
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).add_a_note,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
)
|
||||
],
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => NewListSections(sections: {
|
||||
"view tx": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label:
|
||||
widget.transactionDetailsViewModel.explorerDescription,
|
||||
onTap: widget.transactionDetailsViewModel.launchExplorer,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8)
|
||||
],
|
||||
if (widget.transactionDetailsViewModel.canReplaceByFee)
|
||||
"rbf": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "replace by fee",
|
||||
label: S.of(context).bump_fee,
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.bumpFeePage, arguments: [
|
||||
widget.transactionDetailsViewModel.transactionInfo,
|
||||
widget.transactionDetailsViewModel.rawTransaction
|
||||
]);
|
||||
})
|
||||
]
|
||||
}),
|
||||
)
|
||||
],
|
||||
Observer(
|
||||
builder: (_) => NewListSections(
|
||||
sections: {
|
||||
"view tx": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label: widget
|
||||
.transactionDetailsViewModel.explorerDescription,
|
||||
onTap: widget.transactionDetailsViewModel.launchExplorer,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8,
|
||||
),
|
||||
],
|
||||
if (widget.transactionDetailsViewModel.canReplaceByFee)
|
||||
"rbf": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "replace by fee",
|
||||
label: S.of(context).bump_fee,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.bumpFeePage,
|
||||
arguments: [
|
||||
widget
|
||||
.transactionDetailsViewModel.transactionInfo,
|
||||
widget.transactionDetailsViewModel.rawTransaction,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
|
||||
],
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: switch (item.runtimeType) {
|
||||
ConfirmationsListItem => Row(
|
||||
children: [
|
||||
Text((item as ConfirmationsListItem).current.toString(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
if (item.needed > 0)
|
||||
Text("/${item.needed}",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
Widget _buildTrailingWidget(TransactionDetailsListItem item) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: switch (item.runtimeType) {
|
||||
ConfirmationsListItem => Row(
|
||||
children: [
|
||||
Text(
|
||||
(item as ConfirmationsListItem).current.toString(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
if (item.needed > 0)
|
||||
Text(
|
||||
"/${item.needed}",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildBottomWidget(TransactionDetailsListItem item) {
|
||||
return switch (item.runtimeType) {
|
||||
AddressListItem => AddressFormatter.buildSegmentedAddress(
|
||||
address: item.value,
|
||||
evenTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
AddressListItem => _segmentedAddressList(item.value),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Widget _segmentedAddressList(String value) {
|
||||
final style = TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
final lines = value
|
||||
.split(RegExp(r'[,\n]'))
|
||||
.map((final line) => line.trim())
|
||||
.where((final line) => line.isNotEmpty)
|
||||
.toList();
|
||||
if (lines.length <= 1) {
|
||||
return AddressFormatter.buildSegmentedAddress(
|
||||
address: lines.isNotEmpty ? lines.first : value.trim(),
|
||||
evenTextStyle: style,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final line in lines)
|
||||
AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget {
|
||||
? ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
key: ValueKey(design.imagePath),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
// Purely decorative card artwork.
|
||||
child: ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
@@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget {
|
||||
AnimatedOpacity(
|
||||
opacity: selected ? 0 : 1,
|
||||
duration: textFadeDuration,
|
||||
child: Text(
|
||||
accountBalance,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
// Opacity alone keeps the text readable by screen
|
||||
// readers, so drop it while it is invisible.
|
||||
child: ExcludeSemantics(
|
||||
excluding: selected,
|
||||
child: Text(
|
||||
accountBalance,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget {
|
||||
AnimatedOpacity(
|
||||
opacity: selected ? 1 : 0,
|
||||
duration: textFadeDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
// Only the selected card's balance is visible, so only it
|
||||
// may be announced.
|
||||
child: ExcludeSemantics(
|
||||
excluding: !selected,
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColor,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(fiatFirst ? fiatBalance : balance),
|
||||
),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColorSecondary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(resolvedAssetName),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColor,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(fiatFirst ? fiatBalance : balance),
|
||||
),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColorSecondary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(resolvedAssetName),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget {
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
|
||||
? _CornerSvgIcon(design: design, iconWidth: iconWidth)
|
||||
// Purely decorative card artwork.
|
||||
? ExcludeSemantics(
|
||||
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgIconOff'),
|
||||
),
|
||||
@@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget {
|
||||
child: AnimatedOpacity(
|
||||
duration: designSwitchDuration,
|
||||
opacity: onCustomizeTapped == null ? 0 : 1,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onCustomizeTapped,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Center(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
alignment: Alignment.topRight,
|
||||
colorFilter:
|
||||
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
|
||||
// Faded out and inert: must not be a focusable phantom control.
|
||||
child: ExcludeSemantics(
|
||||
excluding: onCustomizeTapped == null,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).wallet_menu,
|
||||
onTap: onCustomizeTapped,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onCustomizeTapped,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Center(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
alignment: Alignment.topRight,
|
||||
colorFilter:
|
||||
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
|
||||
Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
|
||||
button: true,
|
||||
label: action.label,
|
||||
onTap: action.onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: design.colors.backgroundImageColor.withAlpha(75),
|
||||
borderRadius: BorderRadius.circular(10000000),
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
action.label,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 16),
|
||||
),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: action.onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: design.colors.backgroundImageColor.withAlpha(75),
|
||||
borderRadius: BorderRadius.circular(10000000),
|
||||
),
|
||||
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
|
||||
],
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
action.label,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 16),
|
||||
),
|
||||
),
|
||||
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -77,6 +77,32 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
final left = (parentWidth - effectiveCardWidth) / 2.0;
|
||||
|
||||
final isSelected = _selectedIndex == visualIndex;
|
||||
final accounts = widget.accountListViewModel?.accounts;
|
||||
final cardLabel = (accounts != null && realIndex < accounts.length)
|
||||
? accounts[realIndex].label
|
||||
: S.of(context).balance;
|
||||
|
||||
void onCardTap() {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
setState(() {
|
||||
if (widget.accountListViewModel != null)
|
||||
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
_selectedIndex = visualIndex;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onCardLongPress() {
|
||||
if (_selectedIndex == visualIndex) {
|
||||
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
|
||||
return AnimatedPositioned(
|
||||
key: ValueKey("$visualIndex $realIndex"),
|
||||
duration: animDuration,
|
||||
@@ -87,127 +113,125 @@ class _CardsViewState extends State<CardsView> {
|
||||
duration: animDuration,
|
||||
curve: Curves.easeOut,
|
||||
scale: scale,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
setState(() {
|
||||
if (widget.accountListViewModel != null)
|
||||
widget.accountListViewModel!
|
||||
.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
_selectedIndex = visualIndex;
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (_selectedIndex == visualIndex) {
|
||||
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
|
||||
}
|
||||
;
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: Observer(builder: (_) {
|
||||
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
|
||||
return Container();
|
||||
}
|
||||
final account = widget.accountListViewModel?.accounts[realIndex];
|
||||
|
||||
// The second balance should always be the lightning balance
|
||||
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
|
||||
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
|
||||
.getMainBalanceRecord(widget.lightningMode);
|
||||
|
||||
late final String walletBalance;
|
||||
late final String walletFiatBalance;
|
||||
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
|
||||
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance) {
|
||||
walletBalance = '●●●●●●';
|
||||
walletFiatBalance = '●●●●●●';
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
|
||||
// The card is the tap target; the balances and the card's own buttons stay
|
||||
// reachable as children of this node.
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
label: cardLabel,
|
||||
hint: isSelected
|
||||
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? S.of(context).long_press_show_balance
|
||||
: S.of(context).long_press_hide_balance)
|
||||
: null,
|
||||
onTap: onCardTap,
|
||||
onLongPress: isSelected ? onCardLongPress : null,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: onCardTap,
|
||||
onLongPress: onCardLongPress,
|
||||
child: Observer(builder: (_) {
|
||||
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
|
||||
return Container();
|
||||
}
|
||||
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
|
||||
walletBalance = "";
|
||||
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
final account = widget.accountListViewModel?.accounts[realIndex];
|
||||
|
||||
// the card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final CardDesign cardDesign;
|
||||
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
|
||||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
|
||||
cardDesign = CardDesign.genericDefault;
|
||||
else if (widget.lightningMode)
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
|
||||
else
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
|
||||
// The second balance should always be the lightning balance
|
||||
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
|
||||
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
|
||||
.getMainBalanceRecord(widget.lightningMode);
|
||||
|
||||
final String accountName;
|
||||
final String accountBalance;
|
||||
if (account == null) {
|
||||
accountName = "";
|
||||
accountBalance = "";
|
||||
} else {
|
||||
accountName = account.label;
|
||||
accountBalance = account.balance ?? "0.00";
|
||||
}
|
||||
late final String walletBalance;
|
||||
late final String walletFiatBalance;
|
||||
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
|
||||
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance) {
|
||||
walletBalance = '●●●●●●';
|
||||
walletFiatBalance = '●●●●●●';
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
|
||||
walletBalance = "";
|
||||
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
|
||||
// the card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final CardDesign cardDesign;
|
||||
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
|
||||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
|
||||
cardDesign = CardDesign.genericDefault;
|
||||
else if (widget.lightningMode)
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
|
||||
else
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
|
||||
|
||||
final List<BalanceCardAction> actions = widget.lightningMode
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_deposit,
|
||||
icon: Icons.arrow_downward,
|
||||
onTap: depositToL2,
|
||||
),
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_withdraw,
|
||||
icon: Icons.arrow_upward,
|
||||
onTap: withdrawFromL2,
|
||||
)
|
||||
]
|
||||
: widget.dashboardViewModel.isEnabledTradeAction
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.buy,
|
||||
icon: Icons.arrow_forward_ios_rounded,
|
||||
iconSize: 12,
|
||||
onTap: () {
|
||||
final String accountName;
|
||||
final String accountBalance;
|
||||
if (account == null) {
|
||||
accountName = "";
|
||||
accountBalance = "";
|
||||
} else {
|
||||
accountName = account.label;
|
||||
accountBalance = account.balance ?? "0.00";
|
||||
}
|
||||
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
|
||||
|
||||
final List<BalanceCardAction> actions = widget.lightningMode
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_deposit,
|
||||
icon: Icons.arrow_downward,
|
||||
onTap: depositToL2,
|
||||
),
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_withdraw,
|
||||
icon: Icons.arrow_upward,
|
||||
onTap: withdrawFromL2,
|
||||
)
|
||||
]
|
||||
: widget.dashboardViewModel.isEnabledTradeAction
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.buy,
|
||||
icon: Icons.arrow_forward_ios_rounded,
|
||||
iconSize: 12,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context, builder: (context) => BuySellSelectorModal());
|
||||
},
|
||||
)
|
||||
]
|
||||
: [];
|
||||
)
|
||||
]
|
||||
: [];
|
||||
|
||||
return BalanceCard(
|
||||
width: effectiveCardWidth,
|
||||
accountName: accountName,
|
||||
accountBalance: accountBalance,
|
||||
designSwitchDuration: Duration(milliseconds: 150),
|
||||
assetName: assetName,
|
||||
capitalizeAssetName: _shouldCapitalizeAssetName(),
|
||||
balance: walletBalance,
|
||||
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
|
||||
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
|
||||
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
|
||||
fiatBalance: walletFiatBalance,
|
||||
selected: _selectedIndex == visualIndex,
|
||||
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
|
||||
design: cardDesign,
|
||||
actions: actions,
|
||||
);
|
||||
}),
|
||||
return BalanceCard(
|
||||
width: effectiveCardWidth,
|
||||
accountName: accountName,
|
||||
accountBalance: accountBalance,
|
||||
designSwitchDuration: Duration(milliseconds: 150),
|
||||
assetName: assetName,
|
||||
capitalizeAssetName: _shouldCapitalizeAssetName(),
|
||||
balance: walletBalance,
|
||||
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
|
||||
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
|
||||
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
|
||||
fiatBalance: walletFiatBalance,
|
||||
selected: _selectedIndex == visualIndex,
|
||||
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
|
||||
design: cardDesign,
|
||||
actions: actions,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
S.of(context).mweb_ad,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 12),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
S.of(context).mweb_ad,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => dashboardViewModel.dismissMwebAd(false),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => dashboardViewModel.dismissMwebAd(false),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget {
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 100),
|
||||
opacity: done ? 0 : 1,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
color: Color(0xFFFFB84E),
|
||||
strokeWidth: 2,
|
||||
// Faded out means "nothing to report", so it must leave the tree too.
|
||||
child: ExcludeSemantics(
|
||||
excluding: done,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
color: Color(0xFFFFB84E),
|
||||
strokeWidth: 2,
|
||||
semanticsLabel: S.of(context).synchronizing,
|
||||
semanticsValue: "${(progress * 100).round()}%",
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedScale(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onLightningSwitchPress();
|
||||
},
|
||||
child: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
width: 70,
|
||||
height: 36,
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
alignment: Alignment.centerRight,
|
||||
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
|
||||
duration: Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: 32,
|
||||
height: 32,
|
||||
// height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Container(
|
||||
child: Row(
|
||||
spacing: 2.0,
|
||||
children: [
|
||||
SizedBox(),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-lightning.svg',
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// One toggle node: the knob position and the coloured glyphs are the only
|
||||
// visual cue for which mode is active.
|
||||
return Semantics(
|
||||
button: true,
|
||||
toggled: lightningMode,
|
||||
label: S.of(context).lightning_mode,
|
||||
child: SizedBox(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onLightningSwitchPress();
|
||||
},
|
||||
child: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape:
|
||||
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
width: 70,
|
||||
height: 36,
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
alignment: Alignment.centerRight,
|
||||
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
|
||||
duration: Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: 32,
|
||||
height: 32,
|
||||
// height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
child: Row(
|
||||
spacing: 2.0,
|
||||
children: [
|
||||
SizedBox(),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-lightning.svg',
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cake_wallet/core/sync_status_title.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
@@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget {
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
if (!_showFullBar())
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (dashboardViewModel.isTorEnabled)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/tor.svg",
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
if (_showDot()) PulsingDot(),
|
||||
],
|
||||
),
|
||||
if (!_showFullBar()) _buildCompactBar(context),
|
||||
if (_showFullBar())
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => FractionallySizedBox(
|
||||
child: Material(
|
||||
child: getIt.get<ManageNodesPage>(param1: false),
|
||||
)));
|
||||
},
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
key: ValueKey(status.runtimeType),
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999),
|
||||
border: _getBorder(context, status.runtimeType),
|
||||
color: _getBackgroundColor(context, status.runtimeType),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (icon != null) icon,
|
||||
// if (dashboardViewModel.silentPaymentsScanningActive &&
|
||||
// progressStatuses.contains(status.runtimeType)) ...[
|
||||
// Text(
|
||||
// "${(status.progress() * 100).toInt()}%",
|
||||
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
|
||||
// ),
|
||||
// Text(
|
||||
// "·",
|
||||
// style: TextStyle(fontSize: 12),
|
||||
// )
|
||||
// ],
|
||||
Text(
|
||||
syncStatusTitle(
|
||||
status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
|
||||
style: _getTextStyle(context, status.runtimeType),
|
||||
// A single node: the localized status text (plus any active
|
||||
// Tor/MWEB/Silent Payments badge) is the label, and the hint says
|
||||
// where tapping leads. Everything inside is redundant with it.
|
||||
Semantics(
|
||||
button: true,
|
||||
label: _statusSemanticsLabel(context, status),
|
||||
hint: S.of(context).manage_nodes,
|
||||
onTap: () => _openNodeManagement(context),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: () => _openNodeManagement(context),
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
key: ValueKey(status.runtimeType),
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999),
|
||||
border: _getBorder(context, status.runtimeType),
|
||||
color: _getBackgroundColor(context, status.runtimeType),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (icon != null) icon,
|
||||
// if (dashboardViewModel.silentPaymentsScanningActive &&
|
||||
// progressStatuses.contains(status.runtimeType)) ...[
|
||||
// Text(
|
||||
// "${(status.progress() * 100).toInt()}%",
|
||||
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
|
||||
// ),
|
||||
// Text(
|
||||
// "·",
|
||||
// style: TextStyle(fontSize: 12),
|
||||
// )
|
||||
// ],
|
||||
Text(
|
||||
syncStatusTitle(status,
|
||||
dashboardViewModel.settingsStore.syncStatusDisplayMode),
|
||||
style: _getTextStyle(context, status.runtimeType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _openNodeManagement(BuildContext context) =>
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => FractionallySizedBox(
|
||||
child: Material(
|
||||
child: getIt.get<ManageNodesPage>(param1: false),
|
||||
)));
|
||||
|
||||
/// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
|
||||
/// the whole row needs a text equivalent.
|
||||
Widget _buildCompactBar(BuildContext context) {
|
||||
final row = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (dashboardViewModel.isTorEnabled)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/tor.svg",
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
if (_showDot()) PulsingDot(),
|
||||
],
|
||||
);
|
||||
|
||||
final label = _joinLabels([
|
||||
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
|
||||
if (_showDot()) S.of(context).synchronizing,
|
||||
]);
|
||||
|
||||
if (label.isEmpty) return row;
|
||||
|
||||
return Semantics(label: label, child: ExcludeSemantics(child: row));
|
||||
}
|
||||
|
||||
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
|
||||
final isFailure = failStatuses.contains(status.runtimeType);
|
||||
|
||||
return _joinLabels([
|
||||
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
|
||||
if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
|
||||
if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
|
||||
if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
|
||||
]);
|
||||
}
|
||||
|
||||
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context, Type status) {
|
||||
if (failStatuses.contains(status)) {
|
||||
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
|
||||
@@ -53,6 +54,7 @@ class TopBar extends StatelessWidget {
|
||||
onSettingsButtonPress();
|
||||
},
|
||||
svgPath: "assets/new-ui/top-settings.svg",
|
||||
semanticLabel: S.of(context).settings_title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
@@ -21,6 +23,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
|
||||
return Observer(builder: (_) {
|
||||
final show = dashboardViewModel.balanceViewModel
|
||||
.hasAdditionalBalance(dashboardViewModel.wallet.currency);
|
||||
final isIronwoodMigration = dashboardViewModel.isMigratingToIronwood;
|
||||
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -44,13 +47,17 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final modal = isIronwoodMigration
|
||||
? ZcashMigrationModal(
|
||||
hasContinue: false, balance: "${balance} ${currency.symbol}")
|
||||
: UnconfirmedBalanceModal(
|
||||
balance: balance.toStringWithSymbol(),
|
||||
currencyIconPath: currency.iconPath ?? "",
|
||||
);
|
||||
showMaterialModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => UnconfirmedBalanceModal(
|
||||
balance: balance.toStringWithSymbol(),
|
||||
currencyIconPath: currency.iconPath ?? "",
|
||||
));
|
||||
builder: (context) => modal);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
@@ -63,8 +70,12 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
child: isIronwoodMigration
|
||||
? CupertinoActivityIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@@ -80,7 +91,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Text(S.of(context).confirming),
|
||||
Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming),
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -125,6 +136,7 @@ class UnconfirmedBalanceModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).balance_confirmation,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -171,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
@@ -20,15 +21,22 @@ class WalletInfoBar extends StatelessWidget {
|
||||
final bool hasCustomize;
|
||||
final VoidCallback onCustomizeButtonTap;
|
||||
|
||||
void _openAccountCustomizer() {
|
||||
if (hasCustomize) {
|
||||
onCustomizeButtonTap();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (hasCustomize) {
|
||||
onCustomizeButtonTap();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
},
|
||||
// The row, the hardware-wallet glyph and the inner accounts button are one
|
||||
// control for a screen reader: a single labeled node opening the customizer.
|
||||
final semanticsLabel =
|
||||
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
|
||||
|
||||
final row = GestureDetector(
|
||||
onTap: _openAccountCustomizer,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -69,18 +77,26 @@ class WalletInfoBar extends StatelessWidget {
|
||||
SizedBox(width: 8),
|
||||
ModernButton.svg(
|
||||
size: 24,
|
||||
onPressed: () {
|
||||
if (hasCustomize) {
|
||||
onCustomizeButtonTap();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
},
|
||||
onPressed: _openAccountCustomizer,
|
||||
svgPath: "assets/new-ui/icon-accounts.svg",
|
||||
semanticLabel: S.of(context).wallet_accounts,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!hasCustomize) {
|
||||
return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: semanticsLabel,
|
||||
hint: S.of(context).wallet_accounts,
|
||||
onTap: _openAccountCustomizer,
|
||||
child: ExcludeSemantics(child: row),
|
||||
);
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
class ZcashMigrationModal extends StatelessWidget {
|
||||
const ZcashMigrationModal({required this.hasContinue, required this.balance, super.key});
|
||||
|
||||
final String balance;
|
||||
final bool hasContinue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).zcash_network_upgrade,
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: Navigator.of(context).pop,
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 40,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 146,
|
||||
height: 79,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned(
|
||||
left: 67,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/ironwood_migration.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: Container(
|
||||
width: 79,
|
||||
height: 79,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(99999999),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 4,),),
|
||||
child: const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/crypto_full_icons/zcash.svg",
|
||||
height: 75,
|
||||
width: 75,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
balance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,),
|
||||
),
|
||||
Text(S.of(context).is_currently_being_migrated),
|
||||
],
|
||||
),
|
||||
),),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${S.of(context).zcash_network_upgrade_desc_1}\n\n",
|
||||
style: Theme.of(context).textTheme.bodyMedium,),
|
||||
TextSpan(
|
||||
text: "${S.of(context).zcash_network_upgrade_desc_2}\n\n",
|
||||
style: Theme.of(context).textTheme.bodyMedium,),
|
||||
TextSpan(
|
||||
text: "${S.of(context).zcash_network_upgrade_desc_3}\n\n",
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFF9C03C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewPrimaryButton(
|
||||
onPressed: _launchDocs,
|
||||
text: S.of(context).learn_more,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary,),
|
||||
if (hasContinue)
|
||||
NewPrimaryButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void _launchDocs() {
|
||||
try {
|
||||
launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/zcash/ironwood-migration"));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CopyWrapper extends StatefulWidget {
|
||||
@@ -11,13 +13,25 @@ class CopyWrapper extends StatefulWidget {
|
||||
this.data,
|
||||
this.requireLongPress = false,
|
||||
this.isSensitive = false,
|
||||
required this.builder,
|
||||
this.duration = const Duration(milliseconds: 1200)});
|
||||
this.builder,
|
||||
this.controlBuilder,
|
||||
this.duration = const Duration(milliseconds: 1200)})
|
||||
: assert((builder == null) != (controlBuilder == null),
|
||||
"Provide exactly one of builder or controlBuilder");
|
||||
|
||||
final ClipboardData? data;
|
||||
final bool isSensitive;
|
||||
final bool requireLongPress;
|
||||
final Widget Function(BuildContext, bool) builder;
|
||||
|
||||
/// Builds content that this widget makes copyable with its own gesture
|
||||
/// detector. Receives the transient "copied" state.
|
||||
final Widget Function(BuildContext, bool)? builder;
|
||||
|
||||
/// Builds a control that performs the copy itself, so no extra gesture
|
||||
/// detector or actionable semantics node is stacked on top of it. [onCopy] is
|
||||
/// null when there is nothing to copy.
|
||||
final Widget Function(BuildContext context, bool copied, VoidCallback? onCopy)? controlBuilder;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
@@ -31,6 +45,11 @@ class _CopyWrapperState extends State<CopyWrapper> {
|
||||
if (widget.data == null) return;
|
||||
ClipboardUtil.setSensitiveDataToClipboard(widget.data!, isSensitive: widget.isSensitive);
|
||||
HapticFeedback.mediumImpact();
|
||||
// Screen readers learn about the copy from the transient "copied" state
|
||||
// below, which build() marks as a live region, instead of from a push
|
||||
// announcement: those are deprecated on Android and make TalkBack drop
|
||||
// whatever it was saying. Where we don't render that state, android 13+
|
||||
// already shows its own clipboard confirmation.
|
||||
if (await shouldShowCopied()) {
|
||||
setState(() => copied = true);
|
||||
Future.delayed(widget.duration, () {
|
||||
@@ -41,11 +60,41 @@ class _CopyWrapperState extends State<CopyWrapper> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.requireLongPress ? null : handleCopy,
|
||||
onLongPress: !widget.requireLongPress ? null : handleCopy,
|
||||
child: widget.builder(context, copied),
|
||||
final VoidCallback? onCopy = widget.data == null ? null : handleCopy;
|
||||
|
||||
// The control's own label carries the copied state (Copy -> Copied), so
|
||||
// merging it into one node and marking that node a live region while copied
|
||||
// is enough for a screen reader to pick the change up.
|
||||
if (widget.controlBuilder != null) {
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
liveRegion: copied,
|
||||
child: widget.controlBuilder!(context, copied, onCopy),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final content = widget.builder!(context, copied);
|
||||
|
||||
// Nothing to copy: don't leave behind a gesture detector that a screen
|
||||
// reader would present as an actionable target that does nothing.
|
||||
if (onCopy == null) return content;
|
||||
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: !widget.requireLongPress,
|
||||
liveRegion: copied,
|
||||
hint: widget.requireLongPress ? S.of(context).long_press_to_copy : S.of(context).copy,
|
||||
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
|
||||
CustomSemanticsAction(label: S.of(context).copy): onCopy,
|
||||
},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.requireLongPress ? null : onCopy,
|
||||
onLongPress: !widget.requireLongPress ? null : onCopy,
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,24 +56,31 @@ class _ChainChip extends StatelessWidget {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? colors.primary : colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: isSelected ? 14 : 12,
|
||||
letterSpacing: -0.07,
|
||||
color: isSelected ? colors.onPrimary : colors.primary,
|
||||
),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? colors.primary : colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: isSelected ? 14 : 12,
|
||||
letterSpacing: -0.07,
|
||||
color: isSelected ? colors.onPrimary : colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,55 +24,65 @@ class CurrencyPickerRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
_IconWithBadge(currency: currency, badgePath: chainBadgePath),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
currency.fullName ?? currency.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: _IconWithBadge(currency: currency, badgePath: chainBadgePath),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
currency.fullName ?? currency.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chainPillLabel != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
chainPillLabel!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (chainPillLabel != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
chainPillLabel!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
trailing,
|
||||
const SizedBox(width: 8),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: isSelected ? colors.primary : colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
trailing,
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: isSelected ? colors.primary : colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,30 +28,41 @@ class CurrencyPickerSearchField extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: colors.primary),
|
||||
ExcludeSemantics(child: Icon(Icons.search, size: 20, color: colors.primary)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
// The hint names the field only while it is empty, so the label is
|
||||
// supplied once there is text to keep exactly one announcement.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: controller.text.isEmpty ? null : hintText,
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.text.isNotEmpty)
|
||||
InkWell(
|
||||
onTap: controller.clear,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant),
|
||||
Semantics(
|
||||
button: true,
|
||||
label: S.of(context).clear,
|
||||
child: InkWell(
|
||||
onTap: controller.clear,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ class CurrencyPickerSheet extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_asset,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -117,6 +117,7 @@ class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_fiat_currency_title,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -35,14 +35,18 @@ class SelectNetworkPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_network_title,
|
||||
leadingIcon: const Icon(Icons.arrow_back),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CakeImageWidget(
|
||||
imageUrl: assetIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.cover,
|
||||
// Illustration of the asset named by the title below it.
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: assetIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
@@ -106,33 +110,42 @@ class _NetworkRow extends StatelessWidget {
|
||||
final networkIconPath =
|
||||
wt != null ? getCryptoCurrencyIconForWalletListItem(wt) : variant.iconPath;
|
||||
final networkName = chainNameForCurrency(variant);
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.cover,
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class DigitInputController {
|
||||
String _text = "";
|
||||
@@ -19,38 +21,37 @@ class DigitInputController {
|
||||
}
|
||||
|
||||
class DigitInputPill extends StatelessWidget {
|
||||
const DigitInputPill({super.key, this.digit, required this.highlighted});
|
||||
const DigitInputPill({required this.highlighted, super.key, this.digit});
|
||||
|
||||
final String? digit;
|
||||
final bool highlighted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 51,
|
||||
height: 84,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border:
|
||||
highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 51,
|
||||
height: 84,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: highlighted
|
||||
? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class DigitInput extends StatefulWidget {
|
||||
const DigitInput({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.desiredLength,
|
||||
this.breakAt = 3,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DigitInputController controller;
|
||||
@@ -74,12 +75,9 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
|
||||
if (_textInputConnection == null || !_textInputConnection!.attached) {
|
||||
_textInputConnection = TextInput.attach(
|
||||
this,
|
||||
const TextInputConfiguration(
|
||||
inputType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: false,
|
||||
),
|
||||
inputAction: TextInputAction.none,
|
||||
TextInputConfiguration(
|
||||
inputType: TextInputType.number,
|
||||
inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -102,30 +100,28 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _openVirtualKeyboard,
|
||||
child: SizedBox(
|
||||
height: 84,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => DigitInputPill(
|
||||
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
|
||||
highlighted: widget.controller.text.length == index,
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: _openVirtualKeyboard,
|
||||
child: SizedBox(
|
||||
height: 84,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => DigitInputPill(
|
||||
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
|
||||
highlighted: widget.controller.text.length == index,
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
|
||||
itemCount: widget.desiredLength,
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
|
||||
itemCount: widget.desiredLength,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
|
||||
final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
|
||||
|
||||
if (numberRegExp.hasMatch(value.text)) {
|
||||
setState(() => widget.controller.text = value.text);
|
||||
|
||||
@@ -11,38 +11,45 @@ class DropdownRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
bottom: expanded ? Radius.zero : Radius.circular(18),
|
||||
return Semantics(
|
||||
button: true,
|
||||
expanded: expanded,
|
||||
label: text,
|
||||
onTap: onTap,
|
||||
excludeSemantics: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
bottom: expanded ? Radius.zero : Radius.circular(18),
|
||||
),
|
||||
),
|
||||
),
|
||||
height: 48,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration: Duration(milliseconds: 300),
|
||||
turns: expanded ? 0.0 : 0.5,
|
||||
curve: Curves.easeOut,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
))
|
||||
],
|
||||
height: 48,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration: Duration(milliseconds: 300),
|
||||
turns: expanded ? 0.0 : 0.5,
|
||||
curve: Curves.easeOut,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -106,6 +106,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
@@ -143,6 +144,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
|
||||
iconColor: Theme.of(context).colorScheme.onPrimary,
|
||||
size: 36,
|
||||
icon: Icon(Icons.arrow_forward),
|
||||
semanticLabel: S.of(context).confirm,
|
||||
onPressed: () =>
|
||||
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/utils/show_pop_up.dart";
|
||||
import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SyncKeyImagesSheet extends StatefulWidget {
|
||||
const SyncKeyImagesSheet({
|
||||
required this.appStore,
|
||||
required this.trezorConnectVM,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppStore appStore;
|
||||
final TrezorConnectViewModelBase trezorConnectVM;
|
||||
|
||||
WalletBase get wallet => appStore.wallet!;
|
||||
|
||||
@override
|
||||
State<SyncKeyImagesSheet> createState() => _HardwareWalletProceedOnDeviceSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletProceedOnDeviceSheetState extends State<SyncKeyImagesSheet> {
|
||||
_KeyImageSyncState _state = _KeyImageSyncState.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: false,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).resync_device,
|
||||
leadingIcon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onLeadingPressed: () => showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).are_you_sure_exit,
|
||||
alertContent: S.of(context).resync_device_cancel_warning_desc,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).yes_exit,
|
||||
rightAlertButtonStyle: AlertButtonStyle.error(context),
|
||||
actionLeftButton: Navigator.of(context).pop,
|
||||
actionRightButton: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: hardwareWalletIcon,
|
||||
width: 100,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
DirectionalAnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: content,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.easeOutQuad,
|
||||
opacity: _state == _KeyImageSyncState.initial ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: PrimaryButton(
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onContinuePressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get content {
|
||||
if (_state == _KeyImageSyncState.syncing) {
|
||||
return _syncingKeyImages();
|
||||
}
|
||||
|
||||
return _initial();
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (widget.wallet.hardwareWalletType) {
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
case null:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _initial() => Column(
|
||||
key: const ValueKey(0),
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).resync_device_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _syncingKeyImages() => Column(
|
||||
key: const ValueKey(1),
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).proceed_on_device,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).proceed_on_device_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> _onContinuePressed() async {
|
||||
setState(() => _state = _KeyImageSyncState.syncing);
|
||||
|
||||
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: widget.wallet.type,
|
||||
hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (_, __) {
|
||||
widget.trezorConnectVM.initWallet(widget.wallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isReconnect: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Recheck to handle tap-backs
|
||||
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
|
||||
setState(() => _state = _KeyImageSyncState.initial);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await widget.trezorConnectVM.initWallet(widget.wallet);
|
||||
}
|
||||
|
||||
final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet);
|
||||
if (result && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _KeyImageSyncState {
|
||||
initial,
|
||||
syncing;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorPa
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {},
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
|
||||
@@ -93,33 +93,42 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
|
||||
children: widget.tabs.map((item) {
|
||||
final index = widget.tabs.indexOf(item);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTabChange(index);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: Duration(milliseconds: 150),
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
inherit: true,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: widget.selectedTab == index
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
// One tab = one node: the visible label merges into a button node
|
||||
// that also carries the selected state of this tab.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: widget.selectedTab == index,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTabChange(index);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: Duration(milliseconds: 150),
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
inherit: true,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: widget.selectedTab == index
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
|
||||
child: Text(
|
||||
item,
|
||||
key: textWidgetKeys[index],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
|
||||
child: Text(
|
||||
item,
|
||||
key: textWidgetKeys[index],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList())),
|
||||
|
||||
@@ -60,31 +60,38 @@ class _LongPressMenuState extends State<LongPressMenu> {
|
||||
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: item.onSelected,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Container(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: item.iconPath,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: item.onSelected,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Container(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: item.iconPath,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
Text(item.label,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
Text(item.label,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,6 +11,11 @@ class ModernButton extends StatelessWidget {
|
||||
final Color? backgroundColor;
|
||||
final double? iconSize;
|
||||
final String? label;
|
||||
|
||||
/// Accessible name announced by screen readers. Falls back to [label] (the
|
||||
/// visible caption) when omitted, so one of the two is required. Must be
|
||||
/// localized by the caller.
|
||||
final String? semanticLabel;
|
||||
static const iconSvgSizeRatio = 2 / 3;
|
||||
|
||||
const ModernButton(
|
||||
@@ -21,7 +26,8 @@ class ModernButton extends StatelessWidget {
|
||||
this.iconSize,
|
||||
this.iconColor,
|
||||
this.backgroundColor,
|
||||
this.label})
|
||||
this.label,
|
||||
this.semanticLabel})
|
||||
: svgPath = null;
|
||||
|
||||
const ModernButton.svg(
|
||||
@@ -32,7 +38,8 @@ class ModernButton extends StatelessWidget {
|
||||
this.iconSize,
|
||||
this.iconColor,
|
||||
this.backgroundColor,
|
||||
this.label})
|
||||
this.label,
|
||||
this.semanticLabel})
|
||||
: icon = null;
|
||||
|
||||
@override
|
||||
@@ -56,31 +63,42 @@ class ModernButton extends StatelessWidget {
|
||||
child: icon!,
|
||||
);
|
||||
|
||||
return Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: resolvedBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(size),
|
||||
),
|
||||
width: size,
|
||||
height: size,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
icon: resolvedIcon,
|
||||
),
|
||||
// A single accessibility node for the whole control: the visible caption
|
||||
// duplicates the accessible name, so it is excluded to avoid a second,
|
||||
// detached stop for screen readers.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: semanticLabel ?? label,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: resolvedBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(size),
|
||||
),
|
||||
width: size,
|
||||
height: size,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
icon: ExcludeSemantics(child: resolvedIcon),
|
||||
),
|
||||
),
|
||||
if (label != null && label!.isNotEmpty)
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
label!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (label != null && label!.isNotEmpty)
|
||||
Text(
|
||||
label!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// The [CurrencySymbolText] widget displays [Currency.symbol] based on
|
||||
/// usersettings as a formated string of text with single style.
|
||||
class CurrencySymbolText extends StatelessWidget {
|
||||
const CurrencySymbolText(
|
||||
this.currency, {
|
||||
super.key,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign,
|
||||
this.textDirection,
|
||||
this.locale,
|
||||
this.softWrap,
|
||||
this.overflow,
|
||||
this.textScaler,
|
||||
this.maxLines,
|
||||
this.semanticsLabel,
|
||||
this.semanticsIdentifier,
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.useBaseUnit,
|
||||
});
|
||||
|
||||
/// The currency to display.
|
||||
final Currency currency;
|
||||
|
||||
/// If non-null, the style to use for this text.
|
||||
///
|
||||
/// If the style's "inherit" property is true, the style will be merged with
|
||||
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
|
||||
/// replace the closest enclosing [DefaultTextStyle].
|
||||
///
|
||||
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
|
||||
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
|
||||
/// [MediaQueryData.lineHeightScaleFactorOverride],
|
||||
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
|
||||
/// regardless of its [TextStyle.inherit] value.
|
||||
final TextStyle? style;
|
||||
|
||||
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
final TextAlign? textAlign;
|
||||
|
||||
/// The directionality of the text.
|
||||
///
|
||||
/// This decides how [textAlign] values like [TextAlign.start] and
|
||||
/// [TextAlign.end] are interpreted.
|
||||
///
|
||||
/// This is also used to disambiguate how to render bidirectional text. For
|
||||
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
|
||||
/// in a [TextDirection.ltr] context the English phrase will be on the left
|
||||
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
|
||||
/// context, the English phrase will be on the right and the Hebrew phrase on
|
||||
/// its left.
|
||||
///
|
||||
/// Defaults to the ambient [Directionality], if any.
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// Used to select a font when the same Unicode character can
|
||||
/// be rendered differently, depending on the locale.
|
||||
///
|
||||
/// It's rarely necessary to set this property. By default its value
|
||||
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
|
||||
///
|
||||
/// See [RenderParagraph.locale] for more information.
|
||||
final Locale? locale;
|
||||
|
||||
/// Whether the text should break at soft line breaks.
|
||||
///
|
||||
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
|
||||
final bool? softWrap;
|
||||
|
||||
/// How visual overflow should be handled.
|
||||
///
|
||||
/// If this is null [TextStyle.overflow] will be used, otherwise the value
|
||||
/// from the nearest [DefaultTextStyle] ancestor will be used.
|
||||
final TextOverflow? overflow;
|
||||
|
||||
final TextScaler? textScaler;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
///
|
||||
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
|
||||
/// edge of the box.
|
||||
///
|
||||
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
|
||||
/// an explicit number for its [DefaultTextStyle.maxLines], then the
|
||||
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
|
||||
/// widget directly to entirely override the [DefaultTextStyle].
|
||||
final int? maxLines;
|
||||
|
||||
/// An alternative semantics label for this text.
|
||||
///
|
||||
/// If present, the semantics of this widget will contain this value instead
|
||||
/// of the actual text. This will overwrite any of the semantics labels applied
|
||||
/// directly to the [TextSpan]s.
|
||||
///
|
||||
/// This is useful for replacing abbreviations or shorthands with the full
|
||||
/// text value:
|
||||
///
|
||||
/// ```dart
|
||||
/// const Text(r'$$', semanticsLabel: 'Double dollars')
|
||||
/// ```
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// A unique identifier for the semantics node for this widget.
|
||||
///
|
||||
/// This is useful for cases where the text widget needs to have a uniquely
|
||||
/// identifiable ID that is recognized through the automation tools without
|
||||
/// having a dependency on the actual content of the text that can possibly be
|
||||
/// dynamic in nature.
|
||||
final String? semanticsIdentifier;
|
||||
|
||||
final TextWidthBasis? textWidthBasis;
|
||||
|
||||
final TextHeightBehavior? textHeightBehavior;
|
||||
|
||||
/// The color to use when painting the selection.
|
||||
///
|
||||
/// This is ignored if [SelectionContainer.maybeOf] returns null
|
||||
/// in the [BuildContext] of the [Text] widget.
|
||||
///
|
||||
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
|
||||
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
|
||||
/// (semi-transparent grey).
|
||||
final Color? selectionColor;
|
||||
|
||||
/// Show the amount in the base unit format of [Money.currency]
|
||||
///
|
||||
/// If null, the displayAmountsInSatoshi setting is used.
|
||||
final bool? useBaseUnit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
|
||||
builder: (context, state) => Text(
|
||||
state.getSymbol(currency, overrideSettings: useBaseUnit),
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
locale: locale,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
semanticsLabel: semanticsLabel,
|
||||
semanticsIdentifier: semanticsIdentifier,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
selectionColor: selectionColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:cake_wallet/entities/balance_display_mode.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class MoneySettingsCubit extends Cubit<MoneySettingsState> {
|
||||
MoneySettingsCubit(SettingsStore _settingsStore)
|
||||
: super(
|
||||
MoneySettingsState(
|
||||
bitcoinAmountDisplayMode: _settingsStore.displayAmountsInSatoshi,
|
||||
displayMode: _settingsStore.balanceDisplayMode,
|
||||
),
|
||||
) {
|
||||
_bitcoinAmountDisplayModeDisposer = reaction(
|
||||
(_) => _settingsStore.displayAmountsInSatoshi,
|
||||
(displayMode) => emit(state.copyWith(bitcoinAmountDisplayMode: displayMode)),
|
||||
);
|
||||
_displayModeDisposer = reaction(
|
||||
(_) => _settingsStore.balanceDisplayMode,
|
||||
(displayMode) => emit(state.copyWith(displayMode: displayMode)),
|
||||
);
|
||||
}
|
||||
|
||||
late final ReactionDisposer _bitcoinAmountDisplayModeDisposer;
|
||||
|
||||
late final ReactionDisposer _displayModeDisposer;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
if (!_bitcoinAmountDisplayModeDisposer.reaction.isDisposed) {
|
||||
_bitcoinAmountDisplayModeDisposer.reaction.dispose();
|
||||
}
|
||||
if (!_displayModeDisposer.reaction.isDisposed) {
|
||||
_displayModeDisposer.reaction.dispose();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
class MoneySettingsState {
|
||||
const MoneySettingsState({required this.bitcoinAmountDisplayMode, required this.displayMode});
|
||||
|
||||
final BitcoinAmountDisplayMode bitcoinAmountDisplayMode;
|
||||
final BalanceDisplayMode displayMode;
|
||||
|
||||
bool useBaseUnit(Currency currency) =>
|
||||
([CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency) &&
|
||||
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshi) ||
|
||||
(CryptoCurrency.btcln == currency &&
|
||||
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning);
|
||||
|
||||
bool get isHidden => displayMode == BalanceDisplayMode.hiddenBalance;
|
||||
|
||||
String getSymbol(Currency currency, {bool? overrideSettings}) {
|
||||
if (overrideSettings == null) {
|
||||
return useBaseUnit(currency) ? "sats" : currency.symbol.safeSubString(0, 8);
|
||||
}
|
||||
|
||||
if (overrideSettings == true && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
|
||||
return "sats";
|
||||
}
|
||||
return currency.symbol.safeSubString(0, 8);
|
||||
}
|
||||
|
||||
|
||||
MoneySettingsState copyWith({
|
||||
BitcoinAmountDisplayMode? bitcoinAmountDisplayMode,
|
||||
BalanceDisplayMode? displayMode,
|
||||
}) =>
|
||||
MoneySettingsState(
|
||||
bitcoinAmountDisplayMode: bitcoinAmountDisplayMode ?? this.bitcoinAmountDisplayMode,
|
||||
displayMode: displayMode ?? this.displayMode,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class MoneySettingsProvider extends StatelessWidget {
|
||||
const MoneySettingsProvider({required this.settingsStore, this.child, super.key});
|
||||
|
||||
final Widget? child;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// The [MoneyText] widget displays [Money] as a formated string of text with single style.
|
||||
class MoneyText extends StatelessWidget {
|
||||
const MoneyText(
|
||||
this.amount, {
|
||||
super.key,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign,
|
||||
this.textDirection,
|
||||
this.locale,
|
||||
this.softWrap,
|
||||
this.overflow,
|
||||
this.textScaler,
|
||||
this.maxLines,
|
||||
this.semanticsLabel,
|
||||
this.semanticsIdentifier,
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.isHiddenAmount,
|
||||
this.useBaseUnit,
|
||||
this.fractionalDigits = 8,
|
||||
this.showSymbol = true,
|
||||
this.withSymbolPrefix = false,
|
||||
this.trimZeros = true,
|
||||
});
|
||||
|
||||
/// The [MoneyText.optional] returns a widget displaying [Money] as a
|
||||
/// formated string or an [SizedBox.shrink].
|
||||
static Widget optional(
|
||||
Money? amount, {
|
||||
Key? key,
|
||||
TextStyle? style,
|
||||
StrutStyle? strutStyle,
|
||||
TextAlign? textAlign,
|
||||
TextDirection? textDirection,
|
||||
Locale? locale,
|
||||
bool? softWrap,
|
||||
TextOverflow? overflow,
|
||||
TextScaler? textScaler,
|
||||
int? maxLines,
|
||||
String? semanticsLabel,
|
||||
String? semanticsIdentifier,
|
||||
TextWidthBasis? textWidthBasis,
|
||||
TextHeightBehavior? textHeightBehavior,
|
||||
Color? selectionColor,
|
||||
bool? isHiddenAmount,
|
||||
bool? useBaseUnit,
|
||||
int fractionalDigits = 8,
|
||||
bool showSymbol = true,
|
||||
bool withSymbolPrefix = false,
|
||||
bool trimZeros = true,
|
||||
}) =>
|
||||
amount != null
|
||||
? MoneyText(
|
||||
amount,
|
||||
key: key,
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
locale: locale,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
semanticsLabel: semanticsLabel,
|
||||
semanticsIdentifier: semanticsIdentifier,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
selectionColor: selectionColor,
|
||||
isHiddenAmount: isHiddenAmount,
|
||||
useBaseUnit: useBaseUnit,
|
||||
fractionalDigits: fractionalDigits,
|
||||
showSymbol: showSymbol,
|
||||
withSymbolPrefix: withSymbolPrefix,
|
||||
trimZeros: trimZeros,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
/// The amount to display.
|
||||
final Money amount;
|
||||
|
||||
/// If non-null, the style to use for this text.
|
||||
///
|
||||
/// If the style's "inherit" property is true, the style will be merged with
|
||||
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
|
||||
/// replace the closest enclosing [DefaultTextStyle].
|
||||
///
|
||||
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
|
||||
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
|
||||
/// [MediaQueryData.lineHeightScaleFactorOverride],
|
||||
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
|
||||
/// regardless of its [TextStyle.inherit] value.
|
||||
final TextStyle? style;
|
||||
|
||||
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
final TextAlign? textAlign;
|
||||
|
||||
/// The directionality of the text.
|
||||
///
|
||||
/// This decides how [textAlign] values like [TextAlign.start] and
|
||||
/// [TextAlign.end] are interpreted.
|
||||
///
|
||||
/// This is also used to disambiguate how to render bidirectional text. For
|
||||
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
|
||||
/// in a [TextDirection.ltr] context the English phrase will be on the left
|
||||
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
|
||||
/// context, the English phrase will be on the right and the Hebrew phrase on
|
||||
/// its left.
|
||||
///
|
||||
/// Defaults to the ambient [Directionality], if any.
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// Used to select a font when the same Unicode character can
|
||||
/// be rendered differently, depending on the locale.
|
||||
///
|
||||
/// It's rarely necessary to set this property. By default its value
|
||||
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
|
||||
///
|
||||
/// See [RenderParagraph.locale] for more information.
|
||||
final Locale? locale;
|
||||
|
||||
/// Whether the text should break at soft line breaks.
|
||||
///
|
||||
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
|
||||
final bool? softWrap;
|
||||
|
||||
/// How visual overflow should be handled.
|
||||
///
|
||||
/// If this is null [TextStyle.overflow] will be used, otherwise the value
|
||||
/// from the nearest [DefaultTextStyle] ancestor will be used.
|
||||
final TextOverflow? overflow;
|
||||
|
||||
final TextScaler? textScaler;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
///
|
||||
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
|
||||
/// edge of the box.
|
||||
///
|
||||
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
|
||||
/// an explicit number for its [DefaultTextStyle.maxLines], then the
|
||||
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
|
||||
/// widget directly to entirely override the [DefaultTextStyle].
|
||||
final int? maxLines;
|
||||
|
||||
/// An alternative semantics label for this text.
|
||||
///
|
||||
/// If present, the semantics of this widget will contain this value instead
|
||||
/// of the actual text. This will overwrite any of the semantics labels applied
|
||||
/// directly to the [TextSpan]s.
|
||||
///
|
||||
/// This is useful for replacing abbreviations or shorthands with the full
|
||||
/// text value:
|
||||
///
|
||||
/// ```dart
|
||||
/// const Text(r'$$', semanticsLabel: 'Double dollars')
|
||||
/// ```
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// A unique identifier for the semantics node for this widget.
|
||||
///
|
||||
/// This is useful for cases where the text widget needs to have a uniquely
|
||||
/// identifiable ID that is recognized through the automation tools without
|
||||
/// having a dependency on the actual content of the text that can possibly be
|
||||
/// dynamic in nature.
|
||||
final String? semanticsIdentifier;
|
||||
|
||||
final TextWidthBasis? textWidthBasis;
|
||||
|
||||
final TextHeightBehavior? textHeightBehavior;
|
||||
|
||||
/// The color to use when painting the selection.
|
||||
///
|
||||
/// This is ignored if [SelectionContainer.maybeOf] returns null
|
||||
/// in the [BuildContext] of the [Text] widget.
|
||||
///
|
||||
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
|
||||
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
|
||||
/// (semi-transparent grey).
|
||||
final Color? selectionColor;
|
||||
|
||||
/// Show the amount in the base unit format of [Money.currency]
|
||||
///
|
||||
/// If null, the displayAmountsInSatoshi setting is used.
|
||||
final bool? useBaseUnit;
|
||||
|
||||
/// A limit for the display of the fractional digits of the amount
|
||||
final int fractionalDigits;
|
||||
|
||||
/// Hide the amount
|
||||
///
|
||||
/// If null, the value from the balanceDisplayMode setting is used.
|
||||
final bool? isHiddenAmount;
|
||||
|
||||
/// Show the currency symbol
|
||||
final bool showSymbol;
|
||||
|
||||
/// Prefix the amount with the currency symbol if [showSymbol] is true
|
||||
final bool withSymbolPrefix;
|
||||
|
||||
/// Trim the zeros at the end of an amount
|
||||
final bool trimZeros;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
|
||||
builder: (context, state) => Text(
|
||||
isHiddenAmount ?? state.isHidden
|
||||
? "●●●●●●"
|
||||
: showSymbol
|
||||
? amount.toLocalStringWithSymbol(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
|
||||
withSymbolPrefix: withSymbolPrefix,
|
||||
locale: (locale ?? Localizations.localeOf(context)).toString(),
|
||||
)
|
||||
: amount.toLocalStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
|
||||
locale: (locale ?? Localizations.localeOf(context)).toString(),
|
||||
),
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
locale: locale,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
semanticsLabel: isHiddenAmount ?? state.isHidden
|
||||
? (semanticsLabel ?? S.of(context).amount_hidden)
|
||||
: semanticsLabel,
|
||||
semanticsIdentifier: semanticsIdentifier,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
selectionColor: selectionColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_svg/flutter_svg.dart";
|
||||
|
||||
class NewFuturePrimaryButton extends StatefulWidget {
|
||||
const NewFuturePrimaryButton({
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.disabled = false,
|
||||
this.image,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function() onPressed;
|
||||
final bool disabled;
|
||||
final SvgPicture? image;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final Color borderColor;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _NewFuturePrimaryButtonState();
|
||||
}
|
||||
|
||||
class _NewFuturePrimaryButtonState extends State<NewFuturePrimaryButton> {
|
||||
bool isLoading = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setState(() => isLoading = true);
|
||||
try {
|
||||
await widget.onPressed.call();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewPrimaryButton(
|
||||
onPressed: _onPressed,
|
||||
image: widget.image,
|
||||
text: widget.text,
|
||||
color: widget.color,
|
||||
textColor: widget.textColor,
|
||||
isLoading: isLoading,
|
||||
borderColor: widget.borderColor,
|
||||
disabled: widget.disabled,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
@@ -77,6 +78,7 @@ class _NewPickerState<Item> extends State<NewPicker<Item>> {
|
||||
ModalTopBar(
|
||||
title: widget.title,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
),
|
||||
Padding(
|
||||
@@ -387,6 +389,7 @@ class _PickerSliderPageState<Item> extends State<PickerSliderPage<Item>> {
|
||||
ModalTopBar(
|
||||
title: widget.title,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PayjoinCopyModal extends StatelessWidget {
|
||||
@@ -25,6 +26,7 @@ class PayjoinCopyModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_address_to_copy,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -32,7 +34,7 @@ class PayjoinCopyModal extends StatelessWidget {
|
||||
"": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "btc",
|
||||
label: "Standard",
|
||||
label: S.of(context).standard,
|
||||
iconPath: "assets/new-ui/pjmodal_btc.svg",
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
@@ -41,6 +43,7 @@ class PayjoinCopyModal extends StatelessWidget {
|
||||
? BitcoinURI(amount: uri.amount, address: uri.address).toString()
|
||||
: uri.address),
|
||||
);
|
||||
_announceCopied(context);
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
ListItemRegularRow(
|
||||
@@ -51,6 +54,7 @@ class PayjoinCopyModal extends StatelessWidget {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: uri.toString()),
|
||||
);
|
||||
_announceCopied(context);
|
||||
Navigator.of(context).pop();
|
||||
})
|
||||
]
|
||||
@@ -62,4 +66,15 @@ class PayjoinCopyModal extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Copying pops this sheet right away, so no widget survives to carry a
|
||||
// semantics state change: this path still needs a direct announcement. It is
|
||||
// skipped on platforms that deprecate announcements (android, where they
|
||||
// clear TalkBack's speech queue and the system shows its own clipboard
|
||||
// confirmation anyway).
|
||||
void _announceCopied(BuildContext context) {
|
||||
if (!MediaQuery.supportsAnnounceOf(context)) return;
|
||||
SemanticsService.sendAnnouncement(
|
||||
View.of(context), S.of(context).copied, Directionality.of(context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,52 +42,64 @@ class ReceiveAddressTypeDisplay extends StatelessWidget {
|
||||
iconPath = "assets/new-ui/address-type-picker-icons/litecoin.svg";
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _showPicker(context),
|
||||
child: Row(
|
||||
key: ValueKey("$text$largeQrMode"),
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
if (iconPath != null)
|
||||
CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (!largeQrMode)
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
onPressed: () => _showPicker(context),
|
||||
icon: (Icon(
|
||||
// The row and the chevron button trigger the same picker, so only the
|
||||
// row is exposed and the chevron is treated as decoration.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).address_type,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _showPicker(context),
|
||||
child: Row(
|
||||
key: ValueKey("$text$largeQrMode"),
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
if (iconPath != null)
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_down,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!largeQrMode)
|
||||
ExcludeSemantics(
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
onPressed: () => _showPicker(context),
|
||||
icon: (Icon(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_down,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/section_divider.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
@@ -34,6 +35,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (final _) {
|
||||
final commonOptions = widget.receiveOptionViewModel.options
|
||||
.where((element) =>
|
||||
element.isCommon ||
|
||||
@@ -71,6 +73,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
|
||||
ModalTopBar(
|
||||
title: S.of(context).address_type,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {}),
|
||||
SizedBox(height: 24),
|
||||
@@ -131,38 +134,49 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
bottom: _otherOptionsExpanded ? Radius.zero : Radius.circular(20)),
|
||||
child: InkWell(
|
||||
highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
bottom: !_otherOptionsExpanded ? Radius.zero : Radius.circular(20)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_otherOptionsExpanded = !_otherOptionsExpanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: 64.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).more_options,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
expanded: _otherOptionsExpanded,
|
||||
child: InkWell(
|
||||
highlightColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
bottom: !_otherOptionsExpanded
|
||||
? Radius.zero
|
||||
: Radius.circular(20)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_otherOptionsExpanded = !_otherOptionsExpanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: 64.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).more_options,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: AnimatedRotation(
|
||||
duration: ReceiveAddressTypeSelector
|
||||
.otherOptionsExpandDuration,
|
||||
turns: _otherOptionsExpanded ? 0.0 : 0.5,
|
||||
curve: Curves.easeOut,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg")),
|
||||
)
|
||||
],
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration:
|
||||
ReceiveAddressTypeSelector.otherOptionsExpandDuration,
|
||||
turns: _otherOptionsExpanded ? 0.0 : 0.5,
|
||||
curve: Curves.easeOut,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg"))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -177,40 +191,45 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
|
||||
duration: ReceiveAddressTypeSelector.otherOptionsExpandDuration,
|
||||
curve: Curves.easeOut,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
height: _otherOptionsExpanded ? null : 0,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: otherOptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final opt = otherOptions[index];
|
||||
// Collapsed to zero height: keep the options out of
|
||||
// the semantics tree so they cannot be focused.
|
||||
child: ExcludeSemantics(
|
||||
excluding: !_otherOptionsExpanded,
|
||||
child: Container(
|
||||
height: _otherOptionsExpanded ? null : 0,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: otherOptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final opt = otherOptions[index];
|
||||
|
||||
return ReceiveAddressTypeRow(
|
||||
option: opt,
|
||||
roundedTop: false,
|
||||
roundedBottom: index == otherOptions.length - 1,
|
||||
selected:
|
||||
widget.receiveOptionViewModel.selectedReceiveOption == opt,
|
||||
onItemTap: () {
|
||||
Navigator.of(context).pop(opt);
|
||||
},
|
||||
receiveOptionViewModel: widget.receiveOptionViewModel,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if ((widget.receiveOptionViewModel.selectedReceiveOption ==
|
||||
otherOptions[index]) ||
|
||||
(index != otherOptions.length - 1 &&
|
||||
widget.receiveOptionViewModel.selectedReceiveOption ==
|
||||
otherOptions[index + 1])) return Container();
|
||||
return ReceiveAddressTypeRow(
|
||||
option: opt,
|
||||
roundedTop: false,
|
||||
roundedBottom: index == otherOptions.length - 1,
|
||||
selected:
|
||||
widget.receiveOptionViewModel.selectedReceiveOption == opt,
|
||||
onItemTap: () {
|
||||
Navigator.of(context).pop(opt);
|
||||
},
|
||||
receiveOptionViewModel: widget.receiveOptionViewModel,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if ((widget.receiveOptionViewModel.selectedReceiveOption ==
|
||||
otherOptions[index]) ||
|
||||
(index != otherOptions.length - 1 &&
|
||||
widget.receiveOptionViewModel.selectedReceiveOption ==
|
||||
otherOptions[index + 1])) return Container();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 44.0, right: 36.0),
|
||||
child: HorizontalSectionDivider(),
|
||||
);
|
||||
},
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 44.0, right: 36.0),
|
||||
child: HorizontalSectionDivider(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -223,6 +242,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
|
||||
],
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,65 +280,75 @@ class ReceiveAddressTypeRow extends StatelessWidget {
|
||||
top: roundedTop ? Radius.circular(20) : Radius.zero,
|
||||
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onItemTap,
|
||||
child: Container(
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: roundedTop ? Radius.circular(20) : Radius.zero,
|
||||
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
|
||||
)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
// The row itself carries the selection state; the checkbox next to it is
|
||||
// only a visual echo of it and must not be announced separately.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
selected: selected,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: InkWell(
|
||||
onTap: onItemTap,
|
||||
child: Container(
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: roundedTop ? Radius.circular(20) : Radius.zero,
|
||||
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
|
||||
)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (iconPath != null)
|
||||
CakeImageWidget(
|
||||
imageUrl: iconPath!,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)
|
||||
else
|
||||
Container(width: iconSize, height: iconSize),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
Row(
|
||||
children: [
|
||||
if (iconPath != null)
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: iconPath!,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(width: iconSize, height: iconSize),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
if (option.description != null)
|
||||
Text(
|
||||
option.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (option.description != null)
|
||||
Text(
|
||||
option.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
ExcludeSemantics(child: RoundedCheckbox(value: selected))
|
||||
],
|
||||
),
|
||||
RoundedCheckbox(value: selected)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,50 +22,54 @@ class ReceiveAmountDisplay extends StatelessWidget {
|
||||
curve: Curves.easeOutCubic,
|
||||
heightFactor: largeQrMode || walletAddressListViewModel.displayAmount.isEmpty ? 0 : 1,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Text(
|
||||
walletAddressListViewModel.displayAmount,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(walletAddressListViewModel.cryptoCurrencySymbol,
|
||||
// Amount, currency symbol and fiat equivalent describe one value, so
|
||||
// they are announced as a single node.
|
||||
child: MergeSemantics(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Text(
|
||||
walletAddressListViewModel.displayAmount,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500))
|
||||
],
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(walletAddressListViewModel.cryptoCurrencySymbol,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!walletAddressListViewModel.isFiatDisabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
_getFiatAmount(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
],
|
||||
if (!walletAddressListViewModel.isFiatDisabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
_getFiatAmount(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -56,6 +56,7 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {},
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
@@ -65,54 +66,68 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (widget.walletAddressListViewModel.hasTokensList) ...[
|
||||
Text("Token"),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_presentTokenCurrencyPicker(context);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (_) => Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl: widget.walletAddressListViewModel.tokenCurrency
|
||||
?.iconPath ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
.iconPath ??
|
||||
"",
|
||||
size: 32,
|
||||
),
|
||||
Text((widget.walletAddressListViewModel.tokenCurrency ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
as CryptoCurrency)
|
||||
.title
|
||||
.toUpperCase())
|
||||
],
|
||||
),
|
||||
RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg"))
|
||||
],
|
||||
// The caption is reused as the picker's semantics label,
|
||||
// so it must not be announced as a separate node.
|
||||
ExcludeSemantics(child: Text(S.of(context).token)),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).select_token,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_presentTokenCurrencyPicker(context);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (_) => Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: TokenImageWidget(
|
||||
imageUrl: widget.walletAddressListViewModel
|
||||
.tokenCurrency?.iconPath ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
.iconPath ??
|
||||
"",
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
Text((widget.walletAddressListViewModel.tokenCurrency ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
as CryptoCurrency)
|
||||
.title
|
||||
.toUpperCase())
|
||||
],
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg")),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(),
|
||||
Text(S.of(context).amount),
|
||||
// The caption is reused as the field's semantics label.
|
||||
ExcludeSemantics(child: Text(S.of(context).amount)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -132,74 +147,91 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
textAlign: TextAlign.left,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal:
|
||||
widget.walletAddressListViewModel.selectedCurrencyDecimals > 0,
|
||||
),
|
||||
inputFormatters: [
|
||||
DecimalInputFormatter(
|
||||
maxDecimals:
|
||||
widget.walletAddressListViewModel.selectedCurrencyDecimals,
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hint: Text(
|
||||
widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000",
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).amount,
|
||||
child: TextField(
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal:
|
||||
widget.walletAddressListViewModel.selectedCurrencyDecimals >
|
||||
0,
|
||||
),
|
||||
inputFormatters: [
|
||||
DecimalInputFormatter(
|
||||
maxDecimals:
|
||||
widget.walletAddressListViewModel.selectedCurrencyDecimals,
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hint: Text(
|
||||
widget.walletAddressListViewModel.useSatoshi
|
||||
? "0"
|
||||
: "0.00000000",
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
),
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 25,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_presentFiatCurrencyPicker(context);
|
||||
},
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(0),
|
||||
bottomLeft: Radius.circular(0),
|
||||
topRight: Radius.circular(18),
|
||||
bottomRight: Radius.circular(18),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => Text(
|
||||
widget.walletAddressListViewModel.selectedCurrencySymbol,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).select_fiat_currency_title,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_presentFiatCurrencyPicker(context);
|
||||
},
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(0),
|
||||
bottomLeft: Radius.circular(0),
|
||||
topRight: Radius.circular(18),
|
||||
bottomRight: Radius.circular(18),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => Text(
|
||||
widget.walletAddressListViewModel.selectedCurrencySymbol,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class ReceiveBottomButtons extends StatefulWidget {
|
||||
final bool largeQrMode;
|
||||
final ClipboardData? copyData;
|
||||
final VoidCallback onCopyButtonPressed;
|
||||
final VoidCallback onAmountButtonPressed;
|
||||
final VoidCallback onLabelButtonPressed;
|
||||
final VoidCallback onAccountsButtonPressed;
|
||||
final bool showLabelButton;
|
||||
final bool showAccountsButton;
|
||||
|
||||
const ReceiveBottomButtons({
|
||||
required this.largeQrMode,
|
||||
required this.onCopyButtonPressed,
|
||||
@@ -29,6 +17,15 @@ class ReceiveBottomButtons extends StatefulWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool largeQrMode;
|
||||
final ClipboardData? copyData;
|
||||
final VoidCallback onCopyButtonPressed;
|
||||
final VoidCallback onAmountButtonPressed;
|
||||
final VoidCallback onLabelButtonPressed;
|
||||
final VoidCallback onAccountsButtonPressed;
|
||||
final bool showLabelButton;
|
||||
final bool showAccountsButton;
|
||||
|
||||
@override
|
||||
State<ReceiveBottomButtons> createState() => _ReceiveBottomButtonsState();
|
||||
}
|
||||
@@ -49,60 +46,57 @@ class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
|
||||
opacity: targetOpacity,
|
||||
curve: Curves.easeOut,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: widget.copyData == null ? widget.onCopyButtonPressed : null,
|
||||
child: IgnorePointer(
|
||||
ignoring: widget.copyData == null,
|
||||
child: CopyWrapper(
|
||||
data: widget.copyData,
|
||||
builder: (context, copied) => AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 200),
|
||||
child: IgnorePointer(
|
||||
child: ModernButton.svg(
|
||||
key: ValueKey(copied),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/copy.svg",
|
||||
onPressed: () {},
|
||||
label: copied ? S.of(context).copied : S.of(context).copy,
|
||||
iconColor: copied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
backgroundColor: copied
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// The button itself is the only control: it copies when there
|
||||
// is data to copy, otherwise it opens the payjoin copy modal.
|
||||
CopyWrapper(
|
||||
data: widget.copyData,
|
||||
controlBuilder: (context, copied, onCopy) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: ModernButton.svg(
|
||||
key: ValueKey(copied),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/copy.svg",
|
||||
onPressed: onCopy ?? widget.onCopyButtonPressed,
|
||||
label: copied ? S.of(context).copied : S.of(context).copy,
|
||||
iconColor: copied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
backgroundColor: copied
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ModernButton.svg(
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/set-amount.svg",
|
||||
onPressed: widget.onAmountButtonPressed,
|
||||
label: S.of(context).set_amount),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/set-amount.svg",
|
||||
onPressed: widget.onAmountButtonPressed,
|
||||
label: S.of(context).set_amount,
|
||||
),
|
||||
if (widget.showLabelButton)
|
||||
ModernButton.svg(
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/add-label.svg",
|
||||
onPressed: widget.onLabelButtonPressed,
|
||||
label: S.of(context).label),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/add-label.svg",
|
||||
onPressed: widget.onLabelButtonPressed,
|
||||
label: S.of(context).label,
|
||||
),
|
||||
if (widget.showAccountsButton)
|
||||
ModernButton.svg(
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/addr-book.svg",
|
||||
onPressed: widget.onAccountsButtonPressed,
|
||||
label: S.of(context).addresses),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/addr-book.svg",
|
||||
onPressed: widget.onAccountsButtonPressed,
|
||||
label: S.of(context).addresses,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -71,12 +71,14 @@ class ReceiveInfoBox extends StatelessWidget {
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (iconPath.isNotEmpty)
|
||||
CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
ExcludeSemantics(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
@@ -96,15 +98,20 @@ class ReceiveInfoBox extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (bottomWidget != null) bottomWidget!,
|
||||
GestureDetector(
|
||||
onTap: onDismissed,
|
||||
child: Text(
|
||||
S.of(context).dismiss,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w300),
|
||||
)),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: onDismissed,
|
||||
child: Text(
|
||||
S.of(context).dismiss,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w300),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]),
|
||||
@@ -136,65 +143,69 @@ class InfoboxCurrencyRow extends StatelessWidget {
|
||||
|
||||
final double stackWidth = iconSize + (overlap * (currenciesLimited.length));
|
||||
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
SizedBox(
|
||||
height: iconSize + iconBorder * 2,
|
||||
width: stackWidth,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: iconBorder,
|
||||
left: overlap * currenciesLimited.length,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(9999999)),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 16,
|
||||
color: Colors.white.withAlpha(128),
|
||||
// Purely illustrative: the meaning ("receive any token on <chain>") is
|
||||
// carried by the infobox message text next to it.
|
||||
return ExcludeSemantics(
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
SizedBox(
|
||||
height: iconSize + iconBorder * 2,
|
||||
width: stackWidth,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: iconBorder,
|
||||
left: overlap * currenciesLimited.length,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(9999999)),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 16,
|
||||
color: Colors.white.withAlpha(128),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...currenciesLimited
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) => Positioned(
|
||||
left: 16.0 * entry.key,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: iconBorder),
|
||||
borderRadius: BorderRadius.circular(9999999)),
|
||||
child: TokenImageWidget(
|
||||
imageUrl: entry.value.iconPath ?? '',
|
||||
size: 24,
|
||||
...currenciesLimited
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) => Positioned(
|
||||
left: 16.0 * entry.key,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: iconBorder),
|
||||
borderRadius: BorderRadius.circular(9999999)),
|
||||
child: TokenImageWidget(
|
||||
imageUrl: entry.value.iconPath ?? '',
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList()
|
||||
.reversed,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
))
|
||||
.toList()
|
||||
.reversed,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).label_address,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {}),
|
||||
Padding(
|
||||
@@ -91,27 +92,32 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
|
||||
return SizedBox(width: 8);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(999)),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: () {
|
||||
_controller.text = defaultLabels[index];
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
defaultLabels[index],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(999)),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: () {
|
||||
_controller.text = defaultLabels[index];
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
defaultLabels[index],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,14 +22,16 @@ class ReceiveLabelWidget extends StatelessWidget {
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/label.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)),
|
||||
ExcludeSemantics(
|
||||
child: ClipRect(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/label.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)),
|
||||
),
|
||||
Text(name,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
|
||||
@@ -15,7 +15,9 @@ class ModalTopBar extends StatelessWidget {
|
||||
this.padding,
|
||||
this.bottomText,
|
||||
this.leadingWidget,
|
||||
this.trailingWidget}) {
|
||||
this.trailingWidget,
|
||||
this.leadingSemanticLabel,
|
||||
this.trailingSemanticLabel}) {
|
||||
if (leadingIcon != null && leadingWidget != null) {
|
||||
throw Exception("Cannot have both leadingIcon and leadingWidget");
|
||||
}
|
||||
@@ -35,6 +37,17 @@ class ModalTopBar extends StatelessWidget {
|
||||
final Widget? leadingWidget;
|
||||
final Widget? trailingWidget;
|
||||
|
||||
/// Accessible name for the leading chrome button. Required (and must be
|
||||
/// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
|
||||
/// does not say whether it closes the modal or goes back. Must be localized
|
||||
/// by the caller.
|
||||
final String? leadingSemanticLabel;
|
||||
|
||||
/// Accessible name for the trailing chrome button. Required (and must be
|
||||
/// non-empty) whenever a [trailingIcon] is supplied. Must be localized by
|
||||
/// the caller.
|
||||
final String? trailingSemanticLabel;
|
||||
|
||||
static const buttonSize = 36.0;
|
||||
|
||||
@override
|
||||
@@ -56,21 +69,27 @@ class ModalTopBar extends StatelessWidget {
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
title,
|
||||
key: ValueKey(title),
|
||||
child: Semantics(
|
||||
key: ValueKey(title),
|
||||
header: title.isNotEmpty,
|
||||
// Android reads the heading from headingLevel since the
|
||||
// Flutter 3.41 engine; header: alone only covers iOS.
|
||||
headingLevel: title.isNotEmpty ? 1 : null,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasBottomText)
|
||||
Text(
|
||||
bottomText!,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (subtitle != null && subtitle!.isNotEmpty)
|
||||
Text(subtitle!,
|
||||
style: Theme.of(context)
|
||||
@@ -91,6 +110,7 @@ class ModalTopBar extends StatelessWidget {
|
||||
size: buttonSize,
|
||||
onPressed: onLeadingPressed,
|
||||
icon: leadingIcon!,
|
||||
semanticLabel: leadingSemanticLabel,
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
|
||||
: leadingWidget!
|
||||
else
|
||||
@@ -104,6 +124,7 @@ class ModalTopBar extends StatelessWidget {
|
||||
size: buttonSize,
|
||||
onPressed: onTrailingPressed,
|
||||
icon: trailingIcon!,
|
||||
semanticLabel: trailingSemanticLabel,
|
||||
)
|
||||
: trailingWidget!,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,20 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
class ScanPageNetworkList extends StatelessWidget {
|
||||
const ScanPageNetworkList({super.key});
|
||||
|
||||
/// Same icon the row would render from `trailingIconPath`, but it now carries the
|
||||
/// information that following the row leaves the app.
|
||||
Widget _externalLinkIcon(BuildContext context) => Semantics(
|
||||
label: S.of(context).opens_in_browser,
|
||||
excludeSemantics: true,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/external_link.svg",
|
||||
height: 18,
|
||||
width: 18,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -22,6 +36,7 @@ class ScanPageNetworkList extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).compatible_services,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Padding(
|
||||
@@ -80,7 +95,7 @@ class ScanPageNetworkList extends StatelessWidget {
|
||||
keyValue: "wc",
|
||||
label: "WalletConnect",
|
||||
subtitle: S.of(context).wc_desc,
|
||||
trailingIconPath: "assets/new-ui/external_link.svg",
|
||||
trailingWidget: _externalLinkIcon(context),
|
||||
onTap: () {
|
||||
launchUrl(Uri.https("walletconnect.com"));
|
||||
}),
|
||||
@@ -89,7 +104,7 @@ class ScanPageNetworkList extends StatelessWidget {
|
||||
keyValue: "ocp",
|
||||
label: "OpenCryptoPay",
|
||||
subtitle: S.of(context).ocp_desc,
|
||||
trailingIconPath: "assets/new-ui/external_link.svg",
|
||||
trailingWidget: _externalLinkIcon(context),
|
||||
onTap: () {
|
||||
launchUrl(Uri.https("opencryptopay.io"));
|
||||
})
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cw_core/crypto_amount_format.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class FiatAmountBar extends StatelessWidget {
|
||||
const FiatAmountBar(
|
||||
{super.key,
|
||||
required this.fiatInputMode,
|
||||
required this.onSwitchButtonPressed,
|
||||
this.onAllButtonPressed,
|
||||
required this.cryptoAmount,
|
||||
required this.fiatAmount,
|
||||
required this.cryptoCurrencySymbol,
|
||||
required this.fiatCurrencySymbol,
|
||||
this.allAmount,
|
||||
this.foregroundElementColor,
|
||||
this.textColor,
|
||||
this.allAmountColor,
|
||||
this.allAmountTextColor});
|
||||
const FiatAmountBar({
|
||||
required this.fiatInputMode,
|
||||
required this.onSwitchButtonPressed,
|
||||
required this.cryptoAmount,
|
||||
required this.fiatAmount,
|
||||
required this.cryptoCurrencySymbol,
|
||||
required this.fiatCurrencySymbol,
|
||||
super.key,
|
||||
this.onAllButtonPressed,
|
||||
this.allAmount,
|
||||
this.foregroundElementColor,
|
||||
this.textColor,
|
||||
this.allAmountColor,
|
||||
this.allAmountTextColor,
|
||||
});
|
||||
|
||||
final bool fiatInputMode;
|
||||
final VoidCallback onSwitchButtonPressed;
|
||||
@@ -35,6 +36,10 @@ class FiatAmountBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final convertedAmount = fiatInputMode
|
||||
? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol"
|
||||
: "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol";
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -47,14 +52,19 @@ class FiatAmountBar extends StatelessWidget {
|
||||
svgPath: "assets/new-ui/switch.svg",
|
||||
iconSize: 18,
|
||||
onPressed: onSwitchButtonPressed,
|
||||
semanticLabel: S.of(context).switch_input_currency,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onSwitchButtonPressed,
|
||||
child: Text(
|
||||
fiatInputMode
|
||||
? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol"
|
||||
: "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol",
|
||||
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
|
||||
// Announced as the converted value only: the switch button next to it already
|
||||
// exposes the same action, so this must not become a second control.
|
||||
Semantics(
|
||||
label: convertedAmount,
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: onSwitchButtonPressed,
|
||||
child: Text(
|
||||
convertedAmount,
|
||||
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -63,37 +73,50 @@ class FiatAmountBar extends StatelessWidget {
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).max}.",
|
||||
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
|
||||
// The caption is part of the chip's label below.
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
"${S.of(context).max}.",
|
||||
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(999999)),
|
||||
child: Material(
|
||||
color: allAmountColor ??
|
||||
foregroundElementColor ??
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
color: allAmountColor ??
|
||||
foregroundElementColor ??
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
enabled: onAllButtonPressed != null,
|
||||
label: S.of(context).max,
|
||||
value: _formatAmount(allAmount!),
|
||||
onTap: onAllButtonPressed,
|
||||
excludeSemantics: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
onTap: onAllButtonPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
formatAmount(allAmount!),
|
||||
_formatAmount(allAmount!),
|
||||
style: TextStyle(
|
||||
color: allAmountTextColor ?? Theme.of(context).colorScheme.primary),
|
||||
color: allAmountTextColor ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String formatAmount(String amount) {
|
||||
String _formatAmount(String amount) {
|
||||
try {
|
||||
return double.parse(amount).toStringAsPrecision(8).replaceFirst(RegExp(r"\.?0+$"), "");
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart";
|
||||
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
|
||||
import "package:cake_wallet/view_model/send/send_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
enum l2actions { deposit, withdraw }
|
||||
enum L2Actions { deposit, withdraw }
|
||||
|
||||
class L2ActionWalletSelector extends StatefulWidget {
|
||||
const L2ActionWalletSelector({
|
||||
super.key,
|
||||
required this.showOtherWallets,
|
||||
required this.sendViewModel,
|
||||
required this.action,
|
||||
required this.onSendInitiated,
|
||||
required this.contactListViewModel,
|
||||
required this.walletSwitcherViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool showOtherWallets;
|
||||
final SendViewModel sendViewModel;
|
||||
final l2actions action;
|
||||
final L2Actions action;
|
||||
final VoidCallback onSendInitiated;
|
||||
final ContactListViewModel contactListViewModel;
|
||||
final WalletSwitcherViewModel walletSwitcherViewModel;
|
||||
@@ -56,12 +56,18 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
super.initState();
|
||||
if (widget.showOtherWallets) {
|
||||
() async {
|
||||
items.addAll((await WalletInfo.getAll()).where((item) =>
|
||||
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null));
|
||||
items.addAll(
|
||||
(await WalletInfo.getAll()).where(
|
||||
(item) =>
|
||||
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null,
|
||||
),
|
||||
);
|
||||
items.sort((a, b) {
|
||||
if (a.name == widget.sendViewModel.wallet.name)
|
||||
if (a.name == widget.sendViewModel.wallet.name) {
|
||||
return -1;
|
||||
else if (b.name == widget.sendViewModel.wallet.name) return 1;
|
||||
} else if (b.name == widget.sendViewModel.wallet.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setState(() {});
|
||||
@@ -80,16 +86,17 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: widget.action == l2actions.deposit
|
||||
title: widget.action == L2Actions.deposit
|
||||
? "${S.of(context).send_from}..."
|
||||
: "${S.of(context).receive_to}...",
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Flexible(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -123,29 +130,17 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: WalletRow(
|
||||
currencyIconPath: getCryptoCurrencyIconForWalletListItem(item.type),
|
||||
walletName: item.name,
|
||||
isCurrent: item.name == widget.sendViewModel.wallet.name,
|
||||
isSelected: _selectedWalletIndex == index && !textEntered,
|
||||
onTap: () async {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
addressController.text = "";
|
||||
_selectedWalletIndex = index;
|
||||
});
|
||||
// if (widget.action == l2actions.withdraw) {
|
||||
// widget.sendViewModel.outputs.first.address = item.address;
|
||||
// widget.onSendInitiated();
|
||||
// } else if (widget.action == l2actions.deposit) {
|
||||
// setState(() {
|
||||
// loadingWalletName = item.name;
|
||||
// });
|
||||
// await _handleChangeWallet(item);
|
||||
// widget.onSendInitiated();
|
||||
// setState(() {
|
||||
// loadingWalletName = null;
|
||||
// });
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -198,7 +193,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
if (widget.action == l2actions.withdraw)
|
||||
if (widget.action == L2Actions.withdraw)
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
@@ -215,12 +210,12 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (widget.action == l2actions.deposit && widget.showOtherWallets) ...[
|
||||
if (widget.action == L2Actions.deposit && widget.showOtherWallets) ...[
|
||||
Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(64),
|
||||
),
|
||||
SizedBox(),
|
||||
const SizedBox(),
|
||||
Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
@@ -232,7 +227,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () async {
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showCupertinoModalBottomSheet(
|
||||
context: navigatorKey.currentContext ?? context,
|
||||
@@ -246,7 +241,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
@@ -275,19 +270,22 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
onPressed: () async {
|
||||
if (widget.sendViewModel.wallet.type == WalletType.bitcoin ||
|
||||
widget.sendViewModel.wallet.type == WalletType.litecoin) {
|
||||
if (widget.action == l2actions.withdraw) {
|
||||
if (widget.action == L2Actions.withdraw) {
|
||||
widget.sendViewModel.outputs.first.address =
|
||||
bitcoin!.getUnusedSegwitAddress(widget.sendViewModel.wallet)!;
|
||||
}
|
||||
|
||||
if (widget.showOtherWallets) {
|
||||
if (widget.action == l2actions.deposit) {
|
||||
if (widget.action == L2Actions.deposit) {
|
||||
await _handleChangeWallet(items[_selectedWalletIndex]);
|
||||
} else {
|
||||
widget.sendViewModel.outputs.first.address =
|
||||
addressController.text.isNotEmpty
|
||||
? addressController.text
|
||||
: items[_selectedWalletIndex].address;
|
||||
if (items[_selectedWalletIndex].name !=
|
||||
widget.sendViewModel.wallet.name) {
|
||||
widget.sendViewModel.outputs.first.address =
|
||||
addressController.text.isNotEmpty
|
||||
? addressController.text
|
||||
: items[_selectedWalletIndex].address;
|
||||
}
|
||||
}
|
||||
}
|
||||
widget.onSendInitiated();
|
||||
@@ -298,7 +296,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
SizedBox()
|
||||
const SizedBox()
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -339,13 +337,13 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
|
||||
class WalletRow extends StatelessWidget {
|
||||
const WalletRow({
|
||||
super.key,
|
||||
required this.currencyIconPath,
|
||||
required this.walletName,
|
||||
required this.onTap,
|
||||
this.isLoading = false,
|
||||
this.isCurrent = false,
|
||||
this.isSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String currencyIconPath;
|
||||
@@ -370,7 +368,7 @@ class WalletRow extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
||||
@@ -73,6 +73,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
|
||||
],
|
||||
),
|
||||
trailingIcon: Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
SafeArea(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
@@ -179,32 +180,44 @@ class _RecipientDotState extends State<RecipientDot> {
|
||||
curve: Curves.easeOutCubic,
|
||||
top: _created ? 0 : widget.size,
|
||||
child: Material(
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
child: GestureDetector(
|
||||
key: ValueKey(widget.selected),
|
||||
// Dots scaled all the way down are not visible, so they must not be
|
||||
// reachable by a screen reader either.
|
||||
child: ExcludeSemantics(
|
||||
excluding: widget.textOpacity <= 0,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: widget.selected,
|
||||
label: S.of(context).recipient_number("${widget.index + 1}"),
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: widget.textOpacity,
|
||||
child: Text(
|
||||
(widget.index + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: widget.selected
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
excludeSemantics: true,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
child: GestureDetector(
|
||||
key: ValueKey(widget.selected),
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: widget.textOpacity,
|
||||
child: Text(
|
||||
(widget.index + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: widget.selected
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -86,22 +86,27 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
focusNode: widget.focusNode,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
onSubmitted: (val) => FocusScope.of(context).unfocus(),
|
||||
onChanged: state.didChange,
|
||||
onEditingComplete: () {
|
||||
widget.onEditingComplete();
|
||||
},
|
||||
onTapOutside: (_) {
|
||||
widget.onEditingComplete();
|
||||
},
|
||||
controller: widget.addressController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? S.of(context).search_or_enter,
|
||||
errorMaxLines: 3,
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
label: _fieldSemanticsLabel(context),
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
onSubmitted: (val) => FocusScope.of(context).unfocus(),
|
||||
onChanged: state.didChange,
|
||||
onEditingComplete: () {
|
||||
widget.onEditingComplete();
|
||||
},
|
||||
onTapOutside: (_) {
|
||||
widget.onEditingComplete();
|
||||
},
|
||||
controller: widget.addressController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? S.of(context).search_or_enter,
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
@@ -113,9 +118,13 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
|
||||
widget.addressController.text.isEmpty)
|
||||
? 0
|
||||
: 1,
|
||||
child: SendAddressOverlay(
|
||||
address: widget.addressController.text,
|
||||
displayName: widget.displayName,
|
||||
// Purely visual copy of the field content; announcing it again
|
||||
// would read the address twice.
|
||||
child: ExcludeSemantics(
|
||||
child: SendAddressOverlay(
|
||||
address: widget.addressController.text,
|
||||
displayName: widget.displayName,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -150,9 +159,15 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
|
||||
if (state.hasError)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 6, left: 8),
|
||||
child: Text(
|
||||
state.errorText!,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
|
||||
child: Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: "${S.of(context).address_or_alias}, ${state.errorText!}",
|
||||
excludeSemantics: true,
|
||||
child: Text(
|
||||
state.errorText!,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -161,6 +176,19 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
|
||||
);
|
||||
}
|
||||
|
||||
String _fieldSemanticsLabel(BuildContext context) {
|
||||
final label = S.of(context).address_or_alias;
|
||||
final displayName = widget.displayName;
|
||||
|
||||
if (displayName == null ||
|
||||
displayName.isEmpty ||
|
||||
displayName == widget.addressController.text) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return "$label, $displayName";
|
||||
}
|
||||
|
||||
Future<void> _presentQRScanner(BuildContext context) async {
|
||||
bool isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/decimal_input_formatter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/utils/decimal_input_formatter.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class NewSendAmountInput extends StatefulWidget {
|
||||
const NewSendAmountInput({
|
||||
super.key,
|
||||
required this.currency,
|
||||
required this.maxDecimals,
|
||||
required this.hasPicker,
|
||||
required this.onPickerClicked,
|
||||
required this.currencyIconPath,
|
||||
required this.amountController,
|
||||
super.key,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@@ -41,31 +42,40 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormField<String>(
|
||||
key: formFieldKey,
|
||||
initialValue: widget.amountController.text,
|
||||
validator: widget.validator,
|
||||
builder: (state) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
Widget build(BuildContext context) => FormField<String>(
|
||||
key: formFieldKey,
|
||||
initialValue: widget.amountController.text,
|
||||
validator: widget.validator,
|
||||
builder: (state) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.hasPicker
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
color: widget.hasPicker
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.horizontal(left: Radius.circular(18))),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.horizontal(left: Radius.circular(18)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 8,
|
||||
children: [
|
||||
// Deliberately unwrapped. Naming the field with
|
||||
// MergeSemantics > Semantics(label:) made Android announce
|
||||
// the amount twice: FormField's own wrapper node reflects
|
||||
// its descendants' text, so the authored label came back a
|
||||
// second time on the container. The visible "Amount:"
|
||||
// caption on the send page carries the name instead, and it
|
||||
// must stay in the semantics tree for this field to be
|
||||
// named at all.
|
||||
Expanded(
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
@@ -79,82 +89,107 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
|
||||
],
|
||||
controller: widget.amountController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.maxDecimals > 0 ? "0" : "0.00",
|
||||
hintText: widget.maxDecimals == 0 ? "0" : "0.00",
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
onChanged: state.didChange,
|
||||
),
|
||||
),
|
||||
FloatingIconButton(
|
||||
iconPath: "assets/new-ui/paste.svg",
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
final text = data.text!;
|
||||
widget.amountController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
);
|
||||
}
|
||||
}),
|
||||
iconPath: "assets/new-ui/paste.svg",
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
final text = data.text!;
|
||||
widget.amountController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IntrinsicWidth(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return GestureDetector(
|
||||
// `container: true` is load bearing. FormField wraps this
|
||||
// builder in its own non-container Semantics annotation, so
|
||||
// without a container of its own this configuration is
|
||||
// absorbed into that wrapper node — which then parents the
|
||||
// amount TextField and hides it from screen readers.
|
||||
builder: (_) => Semantics(
|
||||
container: true,
|
||||
button: widget.hasPicker ? true : null,
|
||||
enabled: widget.hasPicker ? true : null,
|
||||
label: widget.hasPicker ? S.of(context).select_asset : widget.currency,
|
||||
value: widget.hasPicker ? widget.currency : null,
|
||||
onTap: widget.hasPicker ? widget.onPickerClicked : null,
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onPickerClicked,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(18),
|
||||
bottomRight: Radius.circular(18)),
|
||||
color: widget.hasPicker
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(18),
|
||||
bottomRight: Radius.circular(18),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
|
||||
TokenImageWidget(
|
||||
imageUrl: widget.currencyIconPath, size: 24),
|
||||
Text(widget.currency),
|
||||
if (widget.hasPicker)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/chooser.svg",
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn),
|
||||
)
|
||||
],
|
||||
))),
|
||||
);
|
||||
},
|
||||
color: widget.hasPicker
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
|
||||
TokenImageWidget(
|
||||
imageUrl: widget.currencyIconPath,
|
||||
size: 24,
|
||||
),
|
||||
Text(widget.currency),
|
||||
if (widget.hasPicker)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/chooser.svg",
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
if (state.hasError)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 6, left: 8),
|
||||
child: Text(
|
||||
state.errorText!,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
if (state.hasError)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6, left: 8),
|
||||
child: Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: "${S.of(context).amount}${state.errorText!}",
|
||||
excludeSemantics: true,
|
||||
child: Text(
|
||||
state.errorText!,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,18 +80,25 @@ class LoadingBottomWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
CupertinoActivityIndicator(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
// One node per state so each step of the send is announced exactly once.
|
||||
return Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: text,
|
||||
excludeSemantics: true,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
CupertinoActivityIndicator(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,45 +113,51 @@ class TransactionErrorActions extends StatelessWidget {
|
||||
return Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withAlpha(64),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/warning.svg",
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).transaction_error,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
errorText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: "${S.of(context).transaction_error}\n$errorText",
|
||||
excludeSemantics: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withAlpha(64),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/warning.svg",
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).transaction_error,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
errorText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -87,11 +87,15 @@ class _SendConfirmSheetState extends State<SendConfirmSheet> {
|
||||
offset: _committed ? const Offset(-1, 0) : Offset.zero,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: SendTransactionDetails(
|
||||
sendViewModel: widget.sendViewModel,
|
||||
isPage: widget.isPage,
|
||||
title: widget.title,
|
||||
iconPath: widget.iconPath,
|
||||
// Both screens stay mounted; only the visible one may be reachable.
|
||||
child: ExcludeSemantics(
|
||||
excluding: _committed,
|
||||
child: SendTransactionDetails(
|
||||
sendViewModel: widget.sendViewModel,
|
||||
isPage: widget.isPage,
|
||||
title: widget.title,
|
||||
iconPath: widget.iconPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -102,8 +106,11 @@ class _SendConfirmSheetState extends State<SendConfirmSheet> {
|
||||
offset: _committed ? Offset.zero : const Offset(1, 0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: TransactionCommitedScreen(
|
||||
sendViewModel: widget.sendViewModel,
|
||||
child: ExcludeSemantics(
|
||||
excluding: !_committed,
|
||||
child: TransactionCommitedScreen(
|
||||
sendViewModel: widget.sendViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -154,13 +161,20 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
Text(
|
||||
title ?? S.of(context).send,
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
Semantics(
|
||||
header: true,
|
||||
// Android reads the heading from headingLevel since the
|
||||
// Flutter 3.41 engine; header: alone only covers iOS.
|
||||
headingLevel: 1,
|
||||
child: Text(
|
||||
title ?? S.of(context).send,
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
trailingIcon: Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: Navigator.of(context).maybePop,
|
||||
),
|
||||
isPage
|
||||
@@ -249,37 +263,40 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 24,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
// The amount being sent is the value under review: announce it as one group.
|
||||
MergeSemantics(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(currencySymbol,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
),
|
||||
Text(
|
||||
fiatAmount,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
Text(currencySymbol,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
),
|
||||
Text(
|
||||
fiatAmount,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (outputs.length >= 1 &&
|
||||
(outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) &&
|
||||
@@ -351,32 +368,34 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).fee,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}",
|
||||
child: MergeSemantics(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).fee,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
)
|
||||
],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (sendViewModel.isElectrumWallet) ...[
|
||||
@@ -389,27 +408,29 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).network,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
|
||||
? "Lightning"
|
||||
: bitcoin!.getNetworkName(sendViewModel.wallet),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
)
|
||||
],
|
||||
child: MergeSemantics(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).network,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
|
||||
? "Lightning"
|
||||
: bitcoin!.getNetworkName(sendViewModel.wallet),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -458,9 +479,14 @@ class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
|
||||
SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Text(
|
||||
S.of(context).transaction_sent_new,
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
||||
// The sheet swaps its content in place, so this title becoming visible is what
|
||||
// tells a screen reader that the transaction went through.
|
||||
Semantics(
|
||||
liveRegion: true,
|
||||
child: Text(
|
||||
S.of(context).transaction_sent_new,
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
SizedBox(),
|
||||
CakeImageWidget(width: 200, height: 200, imageUrl: "assets/new-ui/birthday_cake.svg"),
|
||||
@@ -513,7 +539,8 @@ class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => page);
|
||||
builder: (context) =>
|
||||
FractionallySizedBox(heightFactor: 0.9, child: page));
|
||||
}),
|
||||
],
|
||||
),
|
||||
@@ -550,37 +577,44 @@ class TransactionCommittedScreenActionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
isLoading
|
||||
? CupertinoActivityIndicator()
|
||||
: CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)));
|
||||
child: Semantics(
|
||||
button: true,
|
||||
enabled: !isLoading,
|
||||
label: text,
|
||||
value: isLoading ? S.of(context).loading : null,
|
||||
onTap: isLoading ? null : onTap,
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: isLoading ? null : onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
isLoading
|
||||
? CupertinoActivityIndicator()
|
||||
: CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,16 +655,28 @@ class _MultiSendAddressPreviewState extends State<MultiSendAddressPreview> {
|
||||
style: TextStyle(fontFamily: "IBM Plex Mono"),
|
||||
),
|
||||
if (!_expanded)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = true;
|
||||
});
|
||||
},
|
||||
child: Text(middleTruncate(widget.address, 8, 8),
|
||||
style: TextStyle(
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.primary)))
|
||||
Semantics(
|
||||
button: true,
|
||||
// Read the whole address rather than the truncated form.
|
||||
label: widget.address,
|
||||
hint: S.of(context).show_full_address,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = true;
|
||||
});
|
||||
},
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = true;
|
||||
});
|
||||
},
|
||||
child: Text(middleTruncate(widget.address, 8, 8),
|
||||
style: TextStyle(
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.primary))),
|
||||
)
|
||||
else
|
||||
AddressFormatter.buildSegmentedAddress(
|
||||
address: widget.address,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user