mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-30 06:48:49 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ArchiveConfirmationContent extends StatelessWidget {
|
||||
const ArchiveConfirmationContent({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.isFunded,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final bool isFunded;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isFunded) ...[
|
||||
Text(
|
||||
S.of(context).archive_account_funds_title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AccountFundsSummary(
|
||||
account: account,
|
||||
fiatBalance: fiatBalance,
|
||||
borderColor: context.customColors.warningOutlineColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_move_funds,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
S.of(context).archive_account_funded_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
AccountSummary(account: account),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_empty_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).archive_account_restore_hint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class AccountSummary extends StatelessWidget {
|
||||
const AccountSummary({required this.account});
|
||||
|
||||
final AccountListItem account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = account.label.trim();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${account.id + 1}. ",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: label.isEmpty ? S.of(context).unnamed_account : label,
|
||||
),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountFundsSummary extends StatelessWidget {
|
||||
const AccountFundsSummary({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.borderColor,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final Color borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
border: Border.all(color: borderColor),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: account.balance.currency.iconPath ??
|
||||
"assets/new-ui/crypto_full_icons/${account.balance.currency.name.toLowerCase()}.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MoneyText(
|
||||
account.balance,
|
||||
fractionalDigits: account.balance.decimals,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (fiatBalance != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
fiatBalance!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import "package:hash_wallet/core/address_resolver/address_resolver_service.dart";
|
||||
import "package:hash_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:hash_wallet/core/anypay/anypay_service.dart";
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/evm/evm.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/swap_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/network_decision_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
|
||||
import "package:hash_wallet/reactions/wallet_connect.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/exchange/exchange_view_model.dart";
|
||||
import "package:hash_wallet/view_model/send/send_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_switcher_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/unspent_coin_type.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class AnyPayFormFill {
|
||||
const AnyPayFormFill({
|
||||
required this.request,
|
||||
this.token,
|
||||
this.fallbackCurrency,
|
||||
this.amountOverride,
|
||||
});
|
||||
|
||||
final AnyPayRequest request;
|
||||
final CryptoCurrency? token;
|
||||
final CryptoCurrency? fallbackCurrency;
|
||||
final String? amountOverride;
|
||||
}
|
||||
|
||||
class AnyPayFlow {
|
||||
AnyPayFlow({
|
||||
required this.anyPayService,
|
||||
required this.sendViewModel,
|
||||
required this.authService,
|
||||
required this.walletSwitcherViewModel,
|
||||
});
|
||||
|
||||
final AnyPayService anyPayService;
|
||||
final SendViewModel sendViewModel;
|
||||
final AuthService authService;
|
||||
final WalletSwitcherViewModel walletSwitcherViewModel;
|
||||
|
||||
bool _isSwitchingWallet = false;
|
||||
|
||||
bool get isSwitchingWallet => _isSwitchingWallet;
|
||||
|
||||
Future<AnyPayFormFill?> handleEvaluation(
|
||||
BuildContext context,
|
||||
AnyPayEvaluation evaluation, {
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
}) async {
|
||||
final request = evaluation.request;
|
||||
|
||||
if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
|
||||
(request.rawInput.contains("@") || request.address.contains("@"))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await _handleDecision(context, evaluation, fallbackCurrency);
|
||||
} catch (e) {
|
||||
printV("Payment flow error: $e");
|
||||
return AnyPayFormFill(request: request);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _handleDecision(
|
||||
BuildContext context,
|
||||
AnyPayEvaluation evaluation,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
final request = evaluation.request;
|
||||
|
||||
switch (evaluation.decision) {
|
||||
case final AnyPayApplyToCurrentWallet decision:
|
||||
if (request.isLightning) {
|
||||
_enterLightningMode();
|
||||
}
|
||||
return AnyPayFormFill(
|
||||
request: request,
|
||||
token: decision.token,
|
||||
fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
|
||||
amountOverride: decision.amountOverride,
|
||||
);
|
||||
case AnyPayEvmNetworkChoice():
|
||||
return _pickEvmNetwork(context, request);
|
||||
case final AnyPayCrossChainPayment decision:
|
||||
return _handleCrossChain(context, request, decision, fallbackCurrency);
|
||||
case final AnyPayUnsupportedNetwork decision:
|
||||
_showError(
|
||||
context,
|
||||
S.of(context).unsupported_network_requested(decision.chainId.toString()),
|
||||
);
|
||||
return null;
|
||||
case AnyPayUnsupportedToken():
|
||||
_showError(context, S.of(context).unsupported_token_requested);
|
||||
return null;
|
||||
case AnyPayEmptyInput():
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
|
||||
final chains = evm!.getAllChains();
|
||||
final networks = chains.map((chain) {
|
||||
final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
|
||||
return RecipientNetworkItem(
|
||||
chainId: chain.chainId,
|
||||
name: chain.name,
|
||||
iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
|
||||
chainBadgeIconPath: symbolIconPathForWalletType(walletType),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final selectedChainId =
|
||||
await EvmAddressDetectedSheet.show(context: context, networks: networks);
|
||||
if (!context.mounted || selectedChainId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final target = chains.firstWhere(
|
||||
(chain) => chain.chainId == selectedChainId,
|
||||
orElse: () => chains.first,
|
||||
);
|
||||
|
||||
final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _handleCrossChain(
|
||||
BuildContext context,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
final destinationType = decision.targetWalletType;
|
||||
final isEvmTarget = isEVMCompatibleChain(destinationType);
|
||||
final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
|
||||
final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
|
||||
|
||||
if (!decision.canSwap && !decision.hasCompatibleWallet) {
|
||||
_showError(
|
||||
context,
|
||||
S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final currentType = sendViewModel.wallet.type;
|
||||
final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
|
||||
final currentNetworkName = networkDisplayName(currentType, currentChainId);
|
||||
final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
|
||||
|
||||
if (!decision.hasCompatibleWallet) {
|
||||
final swapConfirmTitle = isEvmTarget
|
||||
? S.of(context).swap_from_network(currentNetworkName)
|
||||
: S.of(context).network_address_detected(destinationNetworkName);
|
||||
final swapConfirmPrimary = isEvmTarget
|
||||
? S.of(context).continue_to_swap
|
||||
: S.of(context).swap_from_network(currentNetworkName);
|
||||
|
||||
await Navigator.of(context).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (pageContext) => NetworkDecisionPage(
|
||||
title: swapConfirmTitle,
|
||||
description: S.of(context).swap_from_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
),
|
||||
destinationIconPath: destinationNetworkIcon,
|
||||
currentIconPath: currentNetworkIcon,
|
||||
primaryText: swapConfirmPrimary,
|
||||
primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
|
||||
onPrimary: () => _openSwap(pageContext, request, decision),
|
||||
secondaryText: S.of(context).cancel,
|
||||
),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final decisionTitle = isEvmTarget
|
||||
? S.of(context).send_to_network(destinationNetworkName)
|
||||
: S.of(context).network_address_detected(destinationNetworkName);
|
||||
|
||||
final selectedWallet = await Navigator.of(context).push<WalletInfo>(
|
||||
CupertinoPageRoute(
|
||||
builder: (pageContext) => NetworkDecisionPage(
|
||||
title: decisionTitle,
|
||||
description: decision.canSwap
|
||||
? S.of(context).send_to_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
)
|
||||
: S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
|
||||
destinationIconPath: destinationNetworkIcon,
|
||||
primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
|
||||
primaryIconPath: "assets/new-ui/wallet_filled.svg",
|
||||
onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
|
||||
secondaryText: decision.canSwap
|
||||
? S.of(context).swap_from_network(currentNetworkName)
|
||||
: S.of(context).cancel,
|
||||
secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
|
||||
onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedWallet == null || !context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
|
||||
}
|
||||
|
||||
Future<void> _pickDestinationWallet(
|
||||
BuildContext pageContext,
|
||||
AnyPayCrossChainPayment decision,
|
||||
String networkName,
|
||||
) async {
|
||||
WalletInfo? destinationWalletInfo;
|
||||
if (decision.wallets.length > 1) {
|
||||
destinationWalletInfo = await SwitchNetworkWalletPage.push(
|
||||
context: pageContext,
|
||||
networkName: networkName,
|
||||
targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
|
||||
wallets: decision.wallets,
|
||||
);
|
||||
} else {
|
||||
destinationWalletInfo = decision.wallets.first;
|
||||
}
|
||||
|
||||
if (destinationWalletInfo == null || !pageContext.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(pageContext).pop(destinationWalletInfo);
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _switchWallet(
|
||||
BuildContext context,
|
||||
WalletInfo walletInfo,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
if (request.isLightning) {
|
||||
_enterLightningMode();
|
||||
}
|
||||
|
||||
_isSwitchingWallet = true;
|
||||
BuildContext? loadingSheetContext;
|
||||
bool completedFlow = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted || completedFlow) {
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (sheetContext) {
|
||||
if (completedFlow) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (Navigator.canPop(sheetContext)) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
}
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
loadingSheetContext = sheetContext;
|
||||
return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
bool success = false;
|
||||
try {
|
||||
success = await anyPayService.switchWalletForPayment(
|
||||
walletInfo,
|
||||
chainId: decision.targetChainId,
|
||||
);
|
||||
} finally {
|
||||
_isSwitchingWallet = false;
|
||||
completedFlow = true;
|
||||
if (loadingSheetContext != null &&
|
||||
loadingSheetContext!.mounted &&
|
||||
Navigator.canPop(loadingSheetContext!)) {
|
||||
Navigator.of(loadingSheetContext!).pop();
|
||||
}
|
||||
loadingSheetContext = null;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
_showError(context, S.of(context).network_switch_failed);
|
||||
return null;
|
||||
}
|
||||
|
||||
return AnyPayFormFill(
|
||||
request: request,
|
||||
token: decision.token,
|
||||
fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
|
||||
amountOverride: decision.amountOverride,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openSwap(
|
||||
BuildContext presentContext,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
) async {
|
||||
if (!presentContext.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.hasContract && decision.token == null) {
|
||||
_showError(presentContext, S.of(presentContext).unsupported_token_requested);
|
||||
return;
|
||||
}
|
||||
|
||||
final intent = anyPayService.buildSwapIntent(
|
||||
request,
|
||||
targetWalletType: decision.targetWalletType,
|
||||
targetChainId: decision.targetChainId,
|
||||
token: decision.token,
|
||||
);
|
||||
|
||||
final page = NewSwapPage(
|
||||
getIt.get<ExchangeViewModel>(),
|
||||
authService,
|
||||
getIt.get<AddressResolverService>(),
|
||||
null,
|
||||
walletSwitcherViewModel: walletSwitcherViewModel,
|
||||
fromSend: SwapFromSendArgs.fromIntent(intent),
|
||||
balanceViewModel: sendViewModel.balanceViewModel,
|
||||
);
|
||||
await Navigator.of(presentContext).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(bottom: false, child: page),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showError(BuildContext context, String message) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: message,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enterLightningMode() {
|
||||
sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
|
||||
sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
|
||||
}
|
||||
|
||||
int _currentEvmChainId() {
|
||||
final wallet = sendViewModel.wallet;
|
||||
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class EvmAddressDetectedSheet extends StatelessWidget {
|
||||
const EvmAddressDetectedSheet({super.key, required this.networks});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => EvmAddressDetectedSheet(networks: networks),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
StackedNetworkIcons(iconPaths: networks.map((n) => n.iconPath).toList()),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).evm_address_detected,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_for_tx,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NetworkListCard(
|
||||
rows: [
|
||||
for (final network in networks)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
imageOverride: network.chainBadgeIconPath,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StackedNetworkIcons extends StatelessWidget {
|
||||
const StackedNetworkIcons({
|
||||
required this.iconPaths,
|
||||
this.size = 50,
|
||||
this.step = 34,
|
||||
});
|
||||
|
||||
final List<String> iconPaths;
|
||||
final double size;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (iconPaths.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final width = size + (iconPaths.length - 1) * step;
|
||||
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: width,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = 0; i < iconPaths.length; i++)
|
||||
Positioned(
|
||||
left: i * step,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colors.surfaceContainer,
|
||||
border: Border.all(color: colors.surface, width: 2),
|
||||
),
|
||||
child: HashImageWidget(imageUrl: iconPaths[i], fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class NetworkDecisionPage extends StatelessWidget {
|
||||
const NetworkDecisionPage({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.destinationIconPath,
|
||||
required this.primaryText,
|
||||
required this.onPrimary,
|
||||
required this.secondaryText,
|
||||
this.currentIconPath,
|
||||
this.primaryIconPath,
|
||||
this.secondaryIconPath,
|
||||
this.onSecondary,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String destinationIconPath;
|
||||
final String? currentIconPath;
|
||||
final String primaryText;
|
||||
final VoidCallback onPrimary;
|
||||
final String? primaryIconPath;
|
||||
final String secondaryText;
|
||||
final String? secondaryIconPath;
|
||||
final VoidCallback? onSecondary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final primaryIcon = primaryIconPath;
|
||||
final secondaryIcon = secondaryIconPath;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: _DecisionHeader(
|
||||
destinationIconPath: destinationIconPath,
|
||||
currentIconPath: currentIconPath,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NewPrimaryButton(
|
||||
onPressed: onPrimary,
|
||||
image: primaryIcon != null
|
||||
? HashImageWidget(
|
||||
imageUrl: primaryIcon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
|
||||
)
|
||||
: null,
|
||||
text: primaryText,
|
||||
color: colors.primary,
|
||||
textColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NewPrimaryButton(
|
||||
onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
|
||||
image: secondaryIcon != null
|
||||
? HashImageWidget(
|
||||
imageUrl: secondaryIcon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
|
||||
)
|
||||
: null,
|
||||
text: secondaryText,
|
||||
color: colors.surfaceContainer,
|
||||
textColor: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DecisionHeader extends StatelessWidget {
|
||||
const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
|
||||
|
||||
final String destinationIconPath;
|
||||
final String? currentIconPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final current = currentIconPath;
|
||||
|
||||
if (current == null) {
|
||||
return HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: current,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(current) ? colors.primary : null,
|
||||
),
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
|
||||
HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkSelector extends StatelessWidget {
|
||||
const RecipientNetworkSelector({
|
||||
required this.networkName,
|
||||
required this.networkIconPath,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String networkIconPath;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: "${S.of(context).network_prefix_on} $networkName",
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).network_prefix_on,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
HashImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.07,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkItem {
|
||||
const RecipientNetworkItem({
|
||||
required this.chainId,
|
||||
required this.name,
|
||||
required this.iconPath,
|
||||
this.chainBadgeIconPath,
|
||||
});
|
||||
|
||||
final int chainId;
|
||||
final String name;
|
||||
final String iconPath;
|
||||
final String? chainBadgeIconPath;
|
||||
}
|
||||
|
||||
class SelectRecipientNetworkSheet extends StatelessWidget {
|
||||
const SelectRecipientNetworkSheet({
|
||||
super.key,
|
||||
required this.networks,
|
||||
required this.currentChainId,
|
||||
});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
final int currentChainId;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
required int currentChainId,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => SelectRecipientNetworkSheet(
|
||||
networks: networks,
|
||||
currentChainId: currentChainId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
RecipientNetworkItem? current;
|
||||
final compatible = <RecipientNetworkItem>[];
|
||||
for (final network in networks) {
|
||||
if (network.chainId == currentChainId && current == null) {
|
||||
current = network;
|
||||
} else {
|
||||
compatible.add(network);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/network_cube.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_recipient_network,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (current != null) ...[
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
RecipientNetworkListRow(
|
||||
item: current,
|
||||
subtitle: S.of(context).current_network,
|
||||
onTap: () => Navigator.of(context).pop(current!.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (compatible.isNotEmpty) ...[
|
||||
Text(
|
||||
S.of(context).compatible_networks,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.06),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
for (final network in compatible)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkListCard extends StatelessWidget {
|
||||
const NetworkListCard({required this.rows});
|
||||
|
||||
final List<Widget> rows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < rows.length; i++) ...[
|
||||
rows[i],
|
||||
if (i != rows.length - 1)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colors.surfaceContainerHigh,
|
||||
indent: 48,
|
||||
endIndent: 12,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecipientNetworkListRow extends StatelessWidget {
|
||||
const RecipientNetworkListRow(
|
||||
{super.key, required this.item, required this.onTap, this.subtitle, this.imageOverride});
|
||||
|
||||
final String? subtitle;
|
||||
final String? imageOverride;
|
||||
final VoidCallback onTap;
|
||||
final RecipientNetworkItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: imageOverride ?? item.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(imageOverride ?? item.iconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w400, letterSpacing: -0.07),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant, letterSpacing: -0.06, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SwitchNetworkWalletPage extends StatelessWidget {
|
||||
const SwitchNetworkWalletPage({
|
||||
required this.wallets,
|
||||
required this.networkName,
|
||||
required this.destinationIconPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String destinationIconPath;
|
||||
final List<WalletInfo> wallets;
|
||||
|
||||
static Future<WalletInfo?> push({
|
||||
required BuildContext context,
|
||||
required String networkName,
|
||||
required String targetIconPath,
|
||||
required List<WalletInfo> wallets,
|
||||
}) =>
|
||||
Navigator.of(context).push<WalletInfo>(
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => SwitchNetworkWalletPage(
|
||||
networkName: networkName,
|
||||
destinationIconPath: targetIconPath,
|
||||
wallets: wallets,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 124),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 32),
|
||||
const SizedBox(width: 8),
|
||||
HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).switch_to_x_wallet(networkName),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_compatible_wallet,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: wallets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (_, index) {
|
||||
final wallet = wallets[index];
|
||||
return WalletRow(
|
||||
currencyIconPath: getCryptoCurrencyIconForWalletListItem(wallet.type),
|
||||
walletName: wallet.name,
|
||||
onTap: () => Navigator.of(context).pop(wallet),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -60,67 +61,78 @@ 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: HashImageWidget(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: HashImageWidget(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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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!,
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,10 +140,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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BorderedSvgIcon extends StatelessWidget {
|
||||
const BorderedSvgIcon({
|
||||
required this.iconPath,
|
||||
required this.iconSize,
|
||||
Color? iconColor,
|
||||
Color? borderColor,
|
||||
this.borderSize = 6,
|
||||
super.key,
|
||||
}) : _iconColor = iconColor,
|
||||
_borderColor = borderColor;
|
||||
|
||||
final String iconPath;
|
||||
final Color? _iconColor;
|
||||
final double iconSize;
|
||||
final Color? _borderColor;
|
||||
final double borderSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = _iconColor ?? Theme.of(context).colorScheme.onSurface;
|
||||
final borderColor = _borderColor ?? Theme.of(context).colorScheme.surface;
|
||||
|
||||
return SizedBox(
|
||||
width: iconSize + borderSize * 2,
|
||||
height: iconSize + borderSize * 2,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: iconSize + borderSize * 2,
|
||||
height: iconSize + borderSize * 2,
|
||||
colorFilter: ColorFilter.mode(
|
||||
borderColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
colorFilter: ColorFilter.mode(
|
||||
iconColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,8 @@ class TransferHistoryRow extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${sourceChainName?.toLowerCase()}.webp',
|
||||
imageUrl:
|
||||
'assets/new-ui/crypto_full_icons/${sourceChainName?.toLowerCase()}.svg',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
@@ -96,7 +97,8 @@ class TransferHistoryRow extends StatelessWidget {
|
||||
top: 14,
|
||||
left: 12,
|
||||
child: HashImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${destChainName?.toLowerCase()}.webp',
|
||||
imageUrl:
|
||||
'assets/new-ui/crypto_full_icons/${destChainName?.toLowerCase()}.svg',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BuySellSelectorModal extends StatelessWidget {
|
||||
const BuySellSelectorModal({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
Text(
|
||||
S.of(context).buy_or_sell,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
S.of(context).buy_or_sell_desc,
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
BuySellSelectorModalButton(
|
||||
title: S.of(context).buy_crypto,
|
||||
description: S.of(context).buy_crypto_desc,
|
||||
iconPath: "assets/new-ui/plus.svg",
|
||||
onTap: () => openBuySellPage(context, BuySellPageMode.buy),
|
||||
),
|
||||
BuySellSelectorModalButton(
|
||||
title: S.of(context).sell_crypto,
|
||||
description: S.of(context).sell_crypto_desc,
|
||||
iconPath: "assets/new-ui/sell.svg",
|
||||
onTap: () => openBuySellPage(context, BuySellPageMode.sell),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
|
||||
Navigator.of(context).pop();
|
||||
final page = getIt.get<NewBuySellAmountPage>(param1: NewBuySellParams(mode: mode));
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (modalContext) => ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: modalContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BuySellSelectorModalButton extends StatelessWidget {
|
||||
const BuySellSelectorModalButton({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.iconPath,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String iconPath;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 55,
|
||||
height: 55,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/wallet_type_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -40,15 +41,16 @@ class _ChangelogModalState extends State<ChangelogModal> {
|
||||
}
|
||||
|
||||
void loadChangelog() async {
|
||||
final fileName = isMoneroOnly ? "monerocom_changelog" : "changelog";
|
||||
String lang = WidgetsBinding.instance.platformDispatcher.locale.languageCode;
|
||||
if (lang == "und" ||
|
||||
lang.isEmpty ||
|
||||
!(await File("$changelogTextLocation/changelog_$lang.json").exists())) {
|
||||
!(await File("$changelogTextLocation/${fileName}_$lang.json").exists())) {
|
||||
lang = "en";
|
||||
}
|
||||
|
||||
final List<dynamic> changelog =
|
||||
jsonDecode(await rootBundle.loadString("$changelogTextLocation/changelog_$lang.json"))
|
||||
jsonDecode(await rootBundle.loadString("$changelogTextLocation/${fileName}_$lang.json"))
|
||||
as List<dynamic>;
|
||||
|
||||
for (final item in changelog) {
|
||||
@@ -170,34 +172,42 @@ class ChangelogItemWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.iconFilename.isNotEmpty)
|
||||
HashImageWidget(
|
||||
imageUrl: "$changelogIconLocation/${item.iconFilename}.svg",
|
||||
height: 36,
|
||||
width: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (item.title.isNotEmpty)
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (item.description.isNotEmpty)
|
||||
Text(item.description,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.iconFilename.isNotEmpty)
|
||||
HashImageWidget(
|
||||
imageUrl: "$changelogIconLocation/${item.iconFilename}.svg",
|
||||
height: 36,
|
||||
width: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (item.title.isNotEmpty)
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (item.description.isNotEmpty)
|
||||
Text(
|
||||
item.description,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/change_pill.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_footer.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class ChartsAssetGrid extends StatelessWidget {
|
||||
const ChartsAssetGrid({required this.bloc, super.key});
|
||||
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
if (state is ChartsStateWithData) {
|
||||
final currencies = state.currencies;
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: currencies.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
mainAxisExtent: 105,),
|
||||
itemBuilder: (context, index) {
|
||||
final curr = currencies[index];
|
||||
return ChartsAssetCard(
|
||||
bloc: bloc,
|
||||
currency: curr,
|
||||
price: state.priceFor(curr)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
|
||||
ticker: state.fiatTicker,
|
||||
changeData: state is ChartsLoaded ? state.changeDataFor(curr) : null,
|
||||
favorite: curr == state.pinnedCurrency,
|
||||
isSingleCurrency: state.hasSingleCurrency,
|
||||
);
|
||||
},);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class ChartsAssetCard extends StatelessWidget {
|
||||
const ChartsAssetCard(
|
||||
{required this.bloc, super.key,
|
||||
required this.currency,
|
||||
required this.price,
|
||||
required this.ticker,
|
||||
this.changeData,
|
||||
required this.favorite,
|
||||
required this.isSingleCurrency,});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String price;
|
||||
final String ticker;
|
||||
final PriceChangeData? changeData;
|
||||
final bool favorite;
|
||||
final bool isSingleCurrency;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
String get displayPrice {
|
||||
try {
|
||||
final priceDouble = double.parse(price);
|
||||
if (priceDouble > 10000) {
|
||||
return priceDouble.toStringAsFixed(0);
|
||||
} else {
|
||||
return priceDouble.toStringAsFixed(2);
|
||||
}
|
||||
} catch (_) {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LongPressPopupBuilder(
|
||||
popup: LongPressMenu(items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).favorite,
|
||||
iconPath: "assets/new-ui/favorite.svg",
|
||||
onSelected: () {
|
||||
bloc.add(CurrencyPinned(currency: currency));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
color: favorite ? Theme.of(context).colorScheme.error : null,),
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).remove,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
if (!isSingleCurrency) {
|
||||
bloc.add(CurrencyRemoved(currency: currency));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
color: isSingleCurrency ? Theme.of(context).colorScheme.onSurfaceVariant : null,),
|
||||
],),
|
||||
footer:
|
||||
isSingleCurrency ? LongPressFooter(text: S.of(context).cannot_remove_last_asset) : null,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
final res = await showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChartModal(
|
||||
bloc: bloc,
|
||||
currency: currency,
|
||||
isFavorite: favorite,
|
||||
),);
|
||||
if (res != null && res is bool && res) {
|
||||
bloc.add(CurrencyPinned(currency: currency));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
width: 1, color: Theme.of(context).colorScheme.surfaceContainerHighest,),
|
||||
gradient: LinearGradient(colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
], begin: Alignment.topCenter, end: Alignment.bottomCenter,),),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 5,
|
||||
children: [
|
||||
if (changeData != null)
|
||||
RotatedBox(
|
||||
quarterTurns:
|
||||
changeData!.direction == PriceChangeDirection.up ? 0 : 2,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/price_change_arrow.svg",
|
||||
width: 8,
|
||||
height: 8,
|
||||
colorFilter: ColorFilter.mode(
|
||||
changeData!.direction.color, BlendMode.srcIn,),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
currency.title.toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
currency.fullName ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (changeData != null)
|
||||
ChangePill(
|
||||
changePercentage: changeData!.percentage,
|
||||
direction: changeData!.direction,),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
alignment: Alignment.centerRight,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
displayPrice,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
Text(
|
||||
ticker,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class ChartsAssetGridHeader extends StatelessWidget {
|
||||
const ChartsAssetGridHeader({
|
||||
required this.bloc, super.key,
|
||||
});
|
||||
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).followed_assets,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
size: 36,
|
||||
iconSize: 16,
|
||||
svgPath: "assets/new-ui/add.svg",
|
||||
onPressed: () => _showCurrencyPicker(context, bloc),
|
||||
),
|
||||
LongPressPopupBuilder(
|
||||
showOnTap: true,
|
||||
popup: LongPressMenu(
|
||||
items: PriceDataSortCriterium.all.map((item) {
|
||||
final isSelected =
|
||||
state is ChartsStateWithData && state.sortCriterium == item;
|
||||
|
||||
return LongPressMenuItem(
|
||||
label: item.name,
|
||||
iconPath: item.iconPath,
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
onSelected: () {
|
||||
bloc.add(SortingCriteriumChanged(newCriterium: item));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
child: ModernButton.svg(
|
||||
size: 36,
|
||||
iconSize: 12,
|
||||
svgPath: "assets/new-ui/sort.svg",
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _showCurrencyPicker(BuildContext context, ChartsBloc bloc) {
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: CryptoCurrency.all,
|
||||
onSelected: (item) {
|
||||
bloc.add(CurrencyAdded(currency: item));
|
||||
},
|
||||
showStablesHeader: false,
|
||||
symbolResolver: (item) => item.symbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:hash_wallet/new-ui/model/charts/util/price_change_data.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/charts_page/change_pill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChangeDisplay extends StatelessWidget {
|
||||
const ChangeDisplay({super.key, required this.changeData, required this.ticker});
|
||||
|
||||
final String ticker;
|
||||
final PriceChangeData changeData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
"${changeData.direction.symbol}${ticker} ${changeData.amount.toStringWithPrecision(fractionalDigits: 2)}",
|
||||
style: TextStyle(fontSize: 16, color: changeData.direction.color),
|
||||
),
|
||||
ChangePill(changePercentage: changeData.percentage, direction: changeData.direction)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChangePill extends StatelessWidget {
|
||||
const ChangePill({super.key, required this.changePercentage, required this.direction});
|
||||
|
||||
final String changePercentage;
|
||||
final PriceChangeDirection direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: direction.color.withAlpha(52), borderRadius: BorderRadius.circular(999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2.5, bottom: 2.5, left: 4, right: 8),
|
||||
child: Text(
|
||||
"${direction.symbol} $changePercentage%",
|
||||
style: TextStyle(color: direction.color),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/change_display.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_view.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/coin_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/price_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/range_selector.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/amount/money_double.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:intl/intl.dart";
|
||||
|
||||
class ChartHeader extends StatefulWidget {
|
||||
const ChartHeader({
|
||||
required this.bloc, super.key,
|
||||
required this.currency,
|
||||
required this.chartHeight,
|
||||
required this.chartPadding,
|
||||
required this.centered,
|
||||
required this.favorite,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final double chartHeight;
|
||||
final double chartPadding;
|
||||
final bool centered;
|
||||
final bool favorite;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
State<ChartHeader> createState() => _ChartHeaderState();
|
||||
}
|
||||
|
||||
class _ChartHeaderState extends State<ChartHeader> {
|
||||
String? _viewedPrice;
|
||||
String? _viewedTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: widget.bloc,
|
||||
builder: (context, state) {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (!widget.favorite)
|
||||
HashImageWidget(
|
||||
imageUrl: widget.currency.iconPath ?? "",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
ChartViewCoinHeader(currency: widget.currency, isFavorite: widget.favorite),
|
||||
ChartViewPriceHeader(
|
||||
price: _viewedPrice ?? s.priceFor(widget.currency)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
|
||||
ticker: s.fiatTicker,
|
||||
highlight: _viewedPrice != null,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// has to be constant-size, otherwise will jump around when loading
|
||||
height: 36,
|
||||
alignment: widget.centered ? Alignment.center : Alignment.centerLeft,
|
||||
child: (s is ChartsLoaded)
|
||||
? (_viewedPrice != null && _viewedTime != null)
|
||||
? Text(
|
||||
_viewedTime!,
|
||||
style: TextStyle(
|
||||
fontFamily: "IBM Plex Mono",
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
)
|
||||
: ChangeDisplay(
|
||||
changeData: s.changeDataFor(widget.currency),
|
||||
ticker: s.fiatTicker,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
if (s is ChartsLoaded)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: widget.chartPadding),
|
||||
child: PriceChart(
|
||||
height: widget.chartHeight,
|
||||
prices: s.dataFor(widget.currency),
|
||||
direction: s.changeDataFor(widget.currency).direction,
|
||||
touchCallback: (event, response) {
|
||||
if (!event.isInterestedForInteractions) {
|
||||
setState(() {
|
||||
_viewedPrice = null;
|
||||
_viewedTime = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final newPrice =
|
||||
response?.lineBarSpots?.firstOrNull?.y.toStringAsFixed(2);
|
||||
final newTime = DateFormat("M/d/y HH:mm").format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
response?.lineBarSpots?.firstOrNull?.x.toInt() ?? 0,
|
||||
),
|
||||
);
|
||||
if (_viewedPrice != newPrice) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() {
|
||||
_viewedPrice = newPrice;
|
||||
});
|
||||
}
|
||||
if (_viewedTime != newTime) {
|
||||
setState(() {
|
||||
_viewedTime = newTime;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: widget.chartHeight + widget.chartPadding * 2,
|
||||
child: const Center(child: CupertinoActivityIndicator()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(128),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ChartRangeSelector(
|
||||
selectedRange: s.range,
|
||||
onRangeSelected: (range) =>
|
||||
widget.bloc.add(RangeChanged(newRange: range)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const CupertinoActivityIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/swap_page.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/action_row/coin_action_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/buy/buy_sell_page.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/store/app_store.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class ChartModal extends StatelessWidget {
|
||||
const ChartModal({required this.bloc, required this.currency, required this.isFavorite, super.key});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isFavorite;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
padding: const EdgeInsets.only(top: 18, left: 18),
|
||||
),
|
||||
ChartHeader(
|
||||
bloc: bloc,
|
||||
currency: currency,
|
||||
chartHeight: 220,
|
||||
chartPadding: 32,
|
||||
centered: true,
|
||||
favorite: false,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: MediaQuery.of(context).size.width * 0.05,
|
||||
children: [
|
||||
if (canOpenBuySell) ...[
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/plus.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).buy,
|
||||
action: () => openBuySellPage(context, BuySellPageMode.buy),
|
||||
),
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/sell.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).sell,
|
||||
action: () => openBuySellPage(context, BuySellPageMode.sell),
|
||||
),
|
||||
],
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/exchange.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).swap,
|
||||
action: () {
|
||||
final page = getIt.get<NewSwapPage>(param2: currency);
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/favorite.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
isFavorite
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
gradientColors:
|
||||
isFavorite ? [const Color(0xFFDF2626), const Color(0xFF980F0F)] : null,
|
||||
label: S.of(context).favorite,
|
||||
action: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
|
||||
Navigator.of(context).pop();
|
||||
final page = getIt.get<NewBuySellAmountPage>(
|
||||
param1: NewBuySellParams(mode: mode, initialCurrency: currency));
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (modalContext) => ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: modalContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get canOpenBuySell => getIt.get<AppStore>().wallet!.balance.containsKey(currency);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:fl_chart/fl_chart.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class PriceChart extends StatelessWidget {
|
||||
const PriceChart({
|
||||
super.key,
|
||||
required this.height,
|
||||
required this.prices,
|
||||
required this.direction,
|
||||
required this.touchCallback,
|
||||
});
|
||||
|
||||
final List<PriceData> prices;
|
||||
final double height;
|
||||
final PriceChangeDirection direction;
|
||||
final Function(FlTouchEvent, LineTouchResponse?) touchCallback;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chartPoints = prices.map((entry) {
|
||||
final x = entry.time.millisecondsSinceEpoch.toDouble();
|
||||
final y = double.parse(entry.quote.toString());
|
||||
return FlSpot(x, y);
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: chartPoints,
|
||||
gradient: LinearGradient(colors: [direction.color.withAlpha(25), direction.color]),
|
||||
barWidth: 1.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
lineTouchData: LineTouchData(
|
||||
enabled: true,
|
||||
getTouchLineStart: (barData, spotIndex) => -double.infinity,
|
||||
getTouchLineEnd: (barData, spotIndex) => double.infinity,
|
||||
getTouchedSpotIndicator: (barData, spotIndexes) => spotIndexes
|
||||
.map(
|
||||
(index) => TouchedSpotIndicatorData(
|
||||
FlLine(
|
||||
strokeWidth: 1,
|
||||
color: direction.color.withAlpha(80),
|
||||
dashArray: [4, 4],
|
||||
),
|
||||
const FlDotData(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (touchedSpots) => touchedSpots.map((spot) => null).toList(),
|
||||
),
|
||||
touchCallback: touchCallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartViewCoinHeader extends StatelessWidget {
|
||||
const ChartViewCoinHeader({required this.currency, required this.isFavorite, super.key});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isFavorite;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
mainAxisAlignment: isFavorite ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (isFavorite)
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath ?? "",
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName ?? currency.title,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
currency.title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isFavorite)
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/favorite.svg",
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartViewPriceHeader extends StatelessWidget {
|
||||
const ChartViewPriceHeader({
|
||||
required this.price,
|
||||
required this.ticker,
|
||||
required this.highlight,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String price;
|
||||
final String ticker;
|
||||
final bool highlight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
price,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
color: highlight
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
ticker,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
color: highlight
|
||||
? Theme.of(context).colorScheme.primary.withAlpha(128)
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartRangeSelector extends StatelessWidget {
|
||||
const ChartRangeSelector({required this.selectedRange, required this.onRangeSelected, super.key});
|
||||
|
||||
final ChartRange selectedRange;
|
||||
final Function(ChartRange) onRangeSelected;
|
||||
|
||||
static const double optionSize = 36;
|
||||
static const double optionPadding = 24;
|
||||
static const Duration switchDuration = Duration(milliseconds: 250);
|
||||
|
||||
double pillPosition(int selectedIndex) => selectedIndex * (optionSize + optionPadding);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedIndex = ChartRange.ranges.indexOf(selectedRange);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
curve: Curves.easeOutCubic,
|
||||
left: pillPosition(selectedIndex),
|
||||
duration: switchDuration,
|
||||
child: Container(
|
||||
height: optionSize,
|
||||
width: optionSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: ChartRange.ranges.map((item) {
|
||||
final selected = selectedRange == item;
|
||||
final isFirst = ChartRange.ranges.indexOf(item) == 0;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (!selected) {
|
||||
onRangeSelected(item);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: optionPadding / 2,
|
||||
left: isFirst ? 0 : optionPadding / 2,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: optionSize,
|
||||
height: optionSize,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: switchDuration,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w400 : FontWeight.w500,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Center(child: Text(item.displayText)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class CoinControlListItem extends StatelessWidget {
|
||||
required this.isLast,
|
||||
required this.isLoading,
|
||||
this.onCheckBoxTap,
|
||||
required this.hasCheckbox,
|
||||
});
|
||||
|
||||
final String note;
|
||||
@@ -31,6 +32,7 @@ class CoinControlListItem extends StatelessWidget {
|
||||
final bool isFirst;
|
||||
final bool isLast;
|
||||
final bool isLoading;
|
||||
final bool hasCheckbox;
|
||||
final Function()? onCheckBoxTap;
|
||||
|
||||
@override
|
||||
@@ -133,6 +135,10 @@ class CoinControlListItem extends StatelessWidget {
|
||||
return HashImageWidget(imageUrl: "assets/new-ui/frozen.svg");
|
||||
}
|
||||
|
||||
if (!hasCheckbox) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return NewSimpleCheckbox(
|
||||
value: isSending,
|
||||
onChanged: (value) {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class AccountsPromo extends StatelessWidget {
|
||||
const AccountsPromo({
|
||||
required this.settingsStore,
|
||||
required this.walletName,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
final String walletName;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static bool supportsWallet(WalletType walletType) => walletType == WalletType.bitcoin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (settingsStore.accountsHomePromoDismissed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 64),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/accounts.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_title(walletName),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_description,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => settingsStore.accountsHomePromoDismissed = true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import 'dart:math';
|
||||
import "dart:math";
|
||||
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class CoinActionButton extends StatelessWidget {
|
||||
const CoinActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.action,
|
||||
super.key,
|
||||
this.gradientColors,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final String label;
|
||||
final VoidCallback action;
|
||||
final List<Color>? gradientColors;
|
||||
|
||||
static const sizeFactor = 0.16;
|
||||
|
||||
@@ -24,47 +25,58 @@ 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.onSurface,
|
||||
// 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: gradientColors ??
|
||||
[
|
||||
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),
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
label,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,13 @@ import 'package:hash_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
import '../../../pages/receive_page.dart';
|
||||
import '../../../pages/scan_page.dart';
|
||||
import 'coin_action_button.dart';
|
||||
|
||||
class CoinActionRow extends StatelessWidget {
|
||||
@@ -45,6 +44,7 @@ class CoinActionRow extends StatelessWidget {
|
||||
spacing: MediaQuery.of(context).size.width * 0.05,
|
||||
children: [
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_send_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/send.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -54,7 +54,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
),
|
||||
label: S.of(context).send,
|
||||
action: () {
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
unspentCoinType:
|
||||
@@ -75,14 +74,10 @@ class CoinActionRow extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Map<String, dynamic>? args;
|
||||
if (lightningMode) args = {'coinTypeToSpendFrom': UnspentCoinType.lightning};
|
||||
Navigator.of(context).pushNamed(Routes.send, arguments: args);
|
||||
}
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_receive_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/receive.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -92,7 +87,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
),
|
||||
label: S.of(context).receive,
|
||||
action: () async {
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
final page = getIt.get<NewReceivePage>(param1: lightningMode);
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
@@ -101,21 +95,11 @@ class CoinActionRow extends StatelessWidget {
|
||||
return Material(child: ModalNavigator(parentContext: context, rootPage: page));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// ToDo: (Konsti) refactor as part of the derivation PR (I hate myself for it)
|
||||
if (lightningMode) {
|
||||
await getIt<WalletAddressListViewModel>().setAddressType(
|
||||
bitcoin!.getOptionToType(bitcoin!.getBitcoinLightningReceivePageOption()));
|
||||
} else {
|
||||
await getIt<WalletAddressListViewModel>().setAddressType(
|
||||
bitcoin!.getOptionToType(bitcoin!.getBitcoinSegwitPageOption()));
|
||||
}
|
||||
Navigator.of(context).pushNamed(Routes.addressPage);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (showSwap)
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_swap_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/exchange.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -127,7 +111,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
action: () {
|
||||
final page =
|
||||
getIt.get<NewSwapPage>(param2: lightningMode ? CryptoCurrency.btcln : null);
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
@@ -137,12 +120,10 @@ class CoinActionRow extends StatelessWidget {
|
||||
parentContext: context,
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(Routes.exchange);
|
||||
}
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_scan_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/scan.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -159,71 +140,63 @@ class CoinActionRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _onPressedScan(BuildContext context) async {
|
||||
if (false && FeatureFlag.hasNewUiExtraPages) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FractionallySizedBox(
|
||||
heightFactor: 0.9,
|
||||
child: ScanPage(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final code = await presentQRScanner(context);
|
||||
final code = await presentQRScanner(context, showHelp: true);
|
||||
|
||||
if (code == null || code.isEmpty) return;
|
||||
if (code == null || code.isEmpty) return;
|
||||
|
||||
late final PaymentRequest req;
|
||||
var unspentCoinType = UnspentCoinType.any;
|
||||
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0));
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (SendViewModelBase.isLnurlInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = CryptoCurrency.btcln
|
||||
.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0));
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
|
||||
req = PaymentRequest(code, "", "", "", "");
|
||||
} else if (Uri.tryParse(code)?.scheme == "wc") {
|
||||
if (!isEVMCompatibleChain(walletType)) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "WalletConnect",
|
||||
alertContent: S.of(context).switchToEVMCompatibleWallet,
|
||||
buttonText: "OK",
|
||||
buttonAction: Navigator.of(context).pop));
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
|
||||
PaymentRequest? req;
|
||||
String? rawInput;
|
||||
var unspentCoinType = UnspentCoinType.any;
|
||||
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = getBolt11Amount(code)?.toString() ?? "0";
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (SendViewModelBase.isLnurlInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (Uri.tryParse(code)?.scheme == "wc") {
|
||||
if (!isWalletConnectCompatibleChain(walletType)) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "WalletConnect",
|
||||
alertContent:
|
||||
S.of(context).switchToWCCompatibleWallet(walletConnectCompatibleChainsLabel()),
|
||||
buttonText: "OK",
|
||||
buttonAction: Navigator.of(context).pop));
|
||||
return;
|
||||
} else {
|
||||
final uri = Uri.tryParse(code);
|
||||
if (uri == null) return;
|
||||
req = PaymentRequest.fromUri(uri);
|
||||
}
|
||||
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: req,
|
||||
unspentCoinType: unspentCoinType,
|
||||
),
|
||||
);
|
||||
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: sendPage,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
|
||||
return;
|
||||
} else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
|
||||
["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
|
||||
Navigator.of(context).pushNamed(Routes.newNode,
|
||||
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
|
||||
return;
|
||||
} else {
|
||||
rawInput = code;
|
||||
}
|
||||
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: req,
|
||||
initialRawInput: rawInput,
|
||||
unspentCoinType: unspentCoinType,
|
||||
),
|
||||
);
|
||||
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: sendPage,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:hash_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/receive_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/swap_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
@@ -61,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),
|
||||
),
|
||||
@@ -77,7 +79,10 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
if (iconPath.isNotEmpty)
|
||||
HashImageWidget(imageUrl: iconPath, width: 75, height: 75)
|
||||
TokenImageWidget(
|
||||
imageUrl: iconPath,
|
||||
size: 75,
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 75,
|
||||
@@ -352,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))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssetTile extends StatelessWidget {
|
||||
@@ -37,147 +37,140 @@ class AssetTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconPath = _getIconPath();
|
||||
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: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Stack(
|
||||
children: [
|
||||
if ((iconPath).isNotEmpty)
|
||||
HashImageWidget(imageUrl: iconPath)
|
||||
else
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
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,
|
||||
borderRadius: BorderRadius.circular(99999)),
|
||||
child: Center(
|
||||
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),
|
||||
)),
|
||||
),
|
||||
// if (chainIconPath.isNotEmpty)
|
||||
// Align(
|
||||
// alignment: Alignment.bottomRight,
|
||||
// child: Container(
|
||||
// decoration: ShapeDecoration(
|
||||
// shape: RoundedSuperellipseBorder(
|
||||
// borderRadius: BorderRadius.circular(5),side: BorderSide(color: Colors.black)),
|
||||
// color: Colors.white),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(2.0),
|
||||
// child: HashImageWidget(
|
||||
// imageUrl: chainIconPath,
|
||||
// width: 12,
|
||||
// height: 12,
|
||||
// colorFilter:
|
||||
// ColorFilter.mode(Colors.black, BlendMode.srcIn),
|
||||
// ),
|
||||
// )))
|
||||
],
|
||||
)),
|
||||
SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
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,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -185,17 +178,5 @@ class AssetTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _getChainTitle() {
|
||||
try {
|
||||
return CryptoCurrency.fromString(wallet.currency.tag ?? wallet.currency.title).fullName ?? "";
|
||||
} catch (e) {
|
||||
return wallet.currency.fullName ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
String _getIconPath() {
|
||||
if (balance.asset == CryptoCurrency.baseEth) return "assets/new-ui/crypto_full_icons/base.svg";
|
||||
|
||||
return balance.asset.iconPath ?? "";
|
||||
}
|
||||
String _getChainTitle() => walletTypeToDisplayName(wallet.type);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_top_bar.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'assets_section.dart';
|
||||
import 'history_section.dart';
|
||||
|
||||
class AssetsHistorySectionActionButton {
|
||||
final String title;
|
||||
final String iconPath;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AssetsHistorySectionActionButton(this.title, this.iconPath, this.onPressed);
|
||||
}
|
||||
|
||||
class AssetsHistorySectionTab {
|
||||
final String title;
|
||||
final Widget content;
|
||||
final AssetsHistorySectionActionButton? actionButton;
|
||||
|
||||
AssetsHistorySectionTab(this.title, this.content);
|
||||
const AssetsHistorySectionTab(this.title, this.content, this.actionButton);
|
||||
}
|
||||
|
||||
class AssetsHistorySection extends StatefulWidget {
|
||||
@@ -32,22 +47,40 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
|
||||
void reloadTabs() {
|
||||
final oldTabLength = tabs.length;
|
||||
final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled);
|
||||
final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
|
||||
final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId);
|
||||
tabs = [
|
||||
if (widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled))
|
||||
if (hasAssetsTab)
|
||||
AssetsHistorySectionTab(
|
||||
S.current.assets,
|
||||
AssetsSection(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
)),
|
||||
),
|
||||
hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
|
||||
() {Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: widget.dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
}): null),
|
||||
AssetsHistorySectionTab(
|
||||
S.current.history,
|
||||
HistorySection(
|
||||
detailsAsPage: false,
|
||||
roundedTopSection: hasAssetsTab || hasNftTab,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
)),
|
||||
if (isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId))
|
||||
AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel))
|
||||
short: true,
|
||||
),
|
||||
AssetsHistorySectionActionButton(
|
||||
S.current.all_pascal_case, "assets/new-ui/arrow_right.svg", () {
|
||||
openHistoryModal(context);
|
||||
})),
|
||||
if (hasNftTab)
|
||||
AssetsHistorySectionTab(
|
||||
S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel), null)
|
||||
];
|
||||
if (oldTabLength != tabs.length) {
|
||||
setState(() {
|
||||
@@ -71,19 +104,39 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
Widget build(BuildContext context) {
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
// if(tabs.length>1)
|
||||
AssetsTopBar(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
tabs: tabs.map((item) => item.title).toList(),
|
||||
onTabChange: (index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
selectedTab: _selectedTab,
|
||||
),
|
||||
if (tabs.length > 1)
|
||||
AssetsTopBar(
|
||||
onTransactionHistoryOpened: () => openHistoryModal(context),
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
tabs: tabs,
|
||||
onTabChange: (index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
selectedTab: _selectedTab,
|
||||
),
|
||||
if (tabs.length == 1)
|
||||
Observer(
|
||||
builder: (_) => HistoryTopBar(
|
||||
onTap: () => openHistoryModal(context),
|
||||
roundedBottom: widget.dashboardViewModel.itemsShort.isEmpty,
|
||||
),
|
||||
),
|
||||
tabs[_selectedTab].content
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openHistoryModal(BuildContext context) async {
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ModalNavigator(
|
||||
rootPage: Material(
|
||||
color: Colors.transparent,
|
||||
child: HistoryModal(dashboardViewModel: widget.dashboardViewModel)),
|
||||
parentContext: context,
|
||||
));
|
||||
widget.dashboardViewModel.changeAllFilterItems(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/line_tab_switcher.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/filter_widget.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -11,78 +8,99 @@ class AssetsTopBar extends StatelessWidget {
|
||||
const AssetsTopBar({
|
||||
super.key,
|
||||
required this.onTabChange,
|
||||
required this.onTransactionHistoryOpened,
|
||||
required this.selectedTab,
|
||||
required this.tabs,
|
||||
required this.dashboardViewModel,
|
||||
});
|
||||
|
||||
final void Function(int) onTabChange;
|
||||
final VoidCallback onTransactionHistoryOpened;
|
||||
final int selectedTab;
|
||||
final List<String> tabs;
|
||||
final List<AssetsHistorySectionTab> tabs;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsButtonText = _getSettingsButtonText();
|
||||
final hasTokenSettingsButton = settingsButtonText != null;
|
||||
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),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 0,
|
||||
left: 12,
|
||||
right: 18,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (tabs.length > 1)
|
||||
LineTabSwitcher(
|
||||
tabs: tabs,
|
||||
onTabChange: onTabChange,
|
||||
selectedTab: selectedTab,
|
||||
)
|
||||
else
|
||||
SizedBox.shrink(),
|
||||
Opacity(
|
||||
opacity: hasTokenSettingsButton ? 1 : 0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (tabs[selectedTab] == S.of(context).assets) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
} else if (tabs[selectedTab] == S.of(context).history) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FilterWidget(filterItems: dashboardViewModel.filterItems),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/options_slider.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
|
||||
Text(
|
||||
settingsButtonText ?? "",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
LineTabSwitcher(
|
||||
tabs: tabs.map((item) => item.title).toList(),
|
||||
onTabChange: onTabChange,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey(selectedTab),
|
||||
spacing: 8,
|
||||
children: [
|
||||
// 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(
|
||||
key: const ValueKey("assets_history_action_button_key"),
|
||||
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: HashImageWidget(
|
||||
imageUrl: actionButton.iconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -90,16 +108,4 @@ class AssetsTopBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _getSettingsButtonText() {
|
||||
if (tabs[selectedTab] == S.current.assets &&
|
||||
dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) {
|
||||
return S.current.tokens;
|
||||
}
|
||||
|
||||
if (tabs[selectedTab] == S.current.history) {
|
||||
return S.current.filters;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/select_deselect_all.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/filter_item.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';
|
||||
|
||||
class HistoryFiltersPage extends StatelessWidget {
|
||||
const HistoryFiltersPage({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
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(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SelectDeselectAllBar(
|
||||
title: S.of(context).type,
|
||||
onSelected: dashboardViewModel.changeAllFilterItems),
|
||||
),
|
||||
),
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": dashboardViewModel.filterItems.map((item) {
|
||||
if (item is SwapFilterItem) {
|
||||
final String subtitle;
|
||||
final Color subtitleColor;
|
||||
if (dashboardViewModel.tradeFilterStore.displayAllTrades) {
|
||||
subtitle = S.of(context).manage_providers;
|
||||
subtitleColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
} else if (dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount ==
|
||||
0) {
|
||||
subtitle = S.of(context).no_providers_selected;
|
||||
subtitleColor = Color(0xFFFFB84E);
|
||||
} else {
|
||||
subtitle = "${item.enabledProviders()} ${S.of(context).providers}";
|
||||
subtitleColor = Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
return ListItemCheckbox(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => HistorySwapProvidersPage(
|
||||
dashboardViewModel: dashboardViewModel)));
|
||||
},
|
||||
keyValue: item.caption,
|
||||
label: S.of(context).swap,
|
||||
value: item.value(),
|
||||
onChanged: (val) {
|
||||
if ((val &&
|
||||
dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount ==
|
||||
0) ||
|
||||
(!val &&
|
||||
dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount >
|
||||
0)) {
|
||||
dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(ExchangeProviderDescription.all);
|
||||
}
|
||||
},
|
||||
subtitle: subtitle,
|
||||
subtitleColor: subtitleColor,
|
||||
showArrow: true);
|
||||
}
|
||||
|
||||
return ListItemCheckbox(
|
||||
keyValue: item.caption,
|
||||
label: item.caption,
|
||||
value: item.value(),
|
||||
onChanged: (val) => item.onChanged());
|
||||
}).toList()
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: () => dashboardViewModel.changeAllFilterItems(true),
|
||||
text: S.of(context).reset_filters,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:hash_wallet/core/csv_export_service.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_filters_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_section.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class HistoryModal extends StatelessWidget {
|
||||
const HistoryModal({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: const ValueKey("history_modal_key"),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).history,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/tx_export.svg",
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).export_csv,
|
||||
onTrailingPressed: () =>
|
||||
getIt.get<CsvExportService>().exportToCsv(dashboardViewModel.items, context),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
child: CustomScrollView(physics: ClampingScrollPhysics(),
|
||||
controller: ModalScrollController.of(context),
|
||||
slivers: [
|
||||
HistorySection(
|
||||
detailsAsPage: true,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
short: false,
|
||||
roundedTopSection: true)
|
||||
])),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).viewPadding.bottom + 168,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: <Color>[
|
||||
Theme.of(context).colorScheme.surface.withAlpha(200),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(175),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(150),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(100),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(50),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(25),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: MediaQuery.of(context).viewPadding.bottom + 24,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
HistoryFiltersPage(dashboardViewModel: dashboardViewModel)));
|
||||
},
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary, width: 1)),
|
||||
height: 56,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/filter_options.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).filters,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart';
|
||||
@@ -7,7 +8,6 @@ import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_tra
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/utils/date_formatter.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
|
||||
@@ -15,176 +15,223 @@ import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class HistorySection extends StatelessWidget {
|
||||
const HistorySection({super.key, required this.dashboardViewModel});
|
||||
const HistorySection(
|
||||
{super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.short,
|
||||
required this.roundedTopSection,
|
||||
required this.detailsAsPage});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool short;
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: EdgeInsets.only(left: 16.0, right: 16, top: short && roundedTopSection ? 18 : 0),
|
||||
sliver: Observer(
|
||||
builder: (_) => (dashboardViewModel.items.isEmpty)
|
||||
? SliverPadding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: (dashboardViewModel.status is SyncingSyncStatus)
|
||||
? SizedBox.shrink()
|
||||
: Center(
|
||||
child: Text(S.of(context).transactions_will_appear_here,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: dashboardViewModel.items.length,
|
||||
(context, index) => Observer(builder: (_) {
|
||||
final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1];
|
||||
final topPadding = index == 0 ? 0.0 : 18.0;
|
||||
final item = dashboardViewModel.items[index];
|
||||
final nextItem = index == dashboardViewModel.items.length - 1
|
||||
? null
|
||||
: dashboardViewModel.items[index + 1];
|
||||
builder: (_) {
|
||||
final localeName = Localizations.localeOf(context).toString();
|
||||
final items = short ? dashboardViewModel.itemsShort : dashboardViewModel.items;
|
||||
|
||||
final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
|
||||
final roundedTop = (prevItem == null || prevItem is DateSectionItem);
|
||||
return (items.isEmpty)
|
||||
? SliverPadding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: (dashboardViewModel.status is SyncingSyncStatus)
|
||||
? SizedBox.shrink()
|
||||
: Center(
|
||||
child: Text(S.of(context).transactions_will_appear_here,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: short ? 0 : 144),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: items.length,
|
||||
(context, index) => Observer(builder: (_) {
|
||||
final prevItem = index == 0 ? null : items[index - 1];
|
||||
final topPadding = index == 0 ? 0.0 : 18.0;
|
||||
final item = items[index];
|
||||
final nextItem = index == items.length - 1 ? null : items[index + 1];
|
||||
|
||||
if (item is TransactionListItem) {
|
||||
final transaction = item.transaction;
|
||||
final transactionType = dashboardViewModel.getTransactionType(transaction);
|
||||
final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
|
||||
final roundedTop = roundedTopSection &&
|
||||
(prevItem == null || prevItem is DateSectionItem);
|
||||
|
||||
if (item.hasTokens && item.assetOfTransaction == null) {
|
||||
return Container();
|
||||
}
|
||||
if (item is TransactionListItem) {
|
||||
final transaction = item.transaction;
|
||||
final transactionType =
|
||||
dashboardViewModel.getTransactionType(transaction);
|
||||
|
||||
CryptoCurrency? asset;
|
||||
if (transaction.additionalInfo["isLightning"] == true)
|
||||
asset = CryptoCurrency.btcln;
|
||||
else
|
||||
asset = item.assetOfTransaction;
|
||||
if (item.hasTokens && item.assetOfTransaction == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final page = getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => page);
|
||||
},
|
||||
child: HistoryTile(
|
||||
title: item.formattedTitle + transactionType,
|
||||
date: DateFormat('HH:mm').format(transaction.date),
|
||||
amount: item.formattedCryptoAmount,
|
||||
amountFiat: item.formattedFiatAmount,
|
||||
hasTokens: item.hasTokens,
|
||||
chainIconPath: _getChainIconPath(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
direction: item.transaction.direction,
|
||||
pending: item.transaction.isPending,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
} else if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
final tradeFrom = trade.from;
|
||||
final tradeTo = trade.to;
|
||||
if (tradeFrom == null || tradeTo == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
CryptoCurrency? asset;
|
||||
if (transaction.additionalInfo["isLightning"] == true)
|
||||
asset = CryptoCurrency.btcln;
|
||||
else
|
||||
asset = item.assetOfTransaction;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
child: HistoryTradeTile(
|
||||
from: tradeFrom,
|
||||
to: tradeTo,
|
||||
provider: trade.provider,
|
||||
date:
|
||||
DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()),
|
||||
amount: trade.amountFormatted(),
|
||||
receiveAmount: trade.receiveAmountFormatted(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
swapState: trade.state,
|
||||
),
|
||||
);
|
||||
} else if (item is DateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(DateFormatter.convertDateTimeToReadableString(item.date),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is OrderListItem) {
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: item.order),
|
||||
child: HistoryOrderTile(
|
||||
date: DateFormat('HH:mm').format(item.order.createdAt),
|
||||
amount: item.orderFormattedAmount,
|
||||
amountFiat: "USD 0.00",
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
),
|
||||
);
|
||||
} else if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
child: PayjoinHistoryTile(
|
||||
createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
|
||||
amount: dashboardViewModel.appStore.amountParsingProxy
|
||||
.getDisplayCryptoString(
|
||||
session.amount.toInt(), CryptoCurrency.btc),
|
||||
currency: item.transaction?.from ?? "BTC",
|
||||
state: item.status,
|
||||
isSending: session.isSenderSession,
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom),
|
||||
);
|
||||
} else if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo),
|
||||
child: AnonpayHistoryTile(
|
||||
provider: transactionInfo.provider,
|
||||
createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
|
||||
amount: transactionInfo.fiatAmount?.toString() ??
|
||||
(transactionInfo.amountTo?.toString() ?? ''),
|
||||
currency: transactionInfo.fiatAmount != null
|
||||
? transactionInfo.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
|
||||
.name
|
||||
.toUpperCase(),
|
||||
roundedTop: roundedTop,
|
||||
return _historyRow(
|
||||
onTap: () {
|
||||
final page =
|
||||
getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
if (detailsAsPage) {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => Material(child: page)));
|
||||
} else {
|
||||
showMaterialModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FractionallySizedBox(heightFactor: 0.9, child: page));
|
||||
}
|
||||
},
|
||||
child: HistoryTile(
|
||||
key: ValueKey("home_page_transaction_${transaction.id}_key"),
|
||||
title: item.formattedTitle + transactionType,
|
||||
date: _formatTransactionDate(item.date, localeName),
|
||||
amount: item.formattedCryptoAmount,
|
||||
amountFiat: item.formattedFiatAmount,
|
||||
hasTokens: item.hasTokens,
|
||||
chainIconPath: _getChainIconPath(),
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom));
|
||||
} else
|
||||
return Text(item.runtimeType.toString());
|
||||
}),
|
||||
),
|
||||
),
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
direction: item.transaction.direction,
|
||||
pending: item.transaction.isPending,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
} else if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
final tradeFrom = trade.from;
|
||||
final tradeTo = trade.to;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
child: HistoryTradeTile(
|
||||
from: tradeFrom,
|
||||
to: tradeTo,
|
||||
provider: trade.provider,
|
||||
date: _formatTransactionDate(
|
||||
item.trade.createdAt ?? DateTime.now(), localeName),
|
||||
amount: dashboardViewModel.balanceDisplayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? "---"
|
||||
: trade.amountFormatted(),
|
||||
receiveAmount: dashboardViewModel.balanceDisplayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? "---"
|
||||
: trade.receiveAmountFormatted(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
swapState: trade.state,
|
||||
),
|
||||
);
|
||||
} else if (item is SpecificDateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(item.text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is DateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(DateFormat("MMMM yyyy", localeName).format(item.date),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is OrderListItem) {
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: item.order),
|
||||
child: HistoryOrderTile(
|
||||
date: _formatTransactionDate(item.order.createdAt, localeName),
|
||||
amount: item.orderFormattedAmount,
|
||||
amountFiat: "",
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
),
|
||||
);
|
||||
} else if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
child: PayjoinHistoryTile(
|
||||
createdAt:
|
||||
_formatTransactionDate(session.inProgressSince!, localeName),
|
||||
amount: dashboardViewModel.appStore.amountParsingProxy
|
||||
.asDisplayString(Money(session.amount, CryptoCurrency.btc)),
|
||||
currency: item.transaction?.from ?? "BTC",
|
||||
state: item.status,
|
||||
isSending: session.isSenderSession,
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom),
|
||||
);
|
||||
} else if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.anonPayDetailsPage,
|
||||
arguments: transactionInfo),
|
||||
child: AnonpayHistoryTile(
|
||||
provider: transactionInfo.provider,
|
||||
createdAt: _formatTransactionDate(
|
||||
transactionInfo.createdAt, localeName),
|
||||
amount: transactionInfo.fiatAmount?.toString() ??
|
||||
(transactionInfo.amountTo?.toString() ?? ''),
|
||||
currency: transactionInfo.fiatAmount != null
|
||||
? transactionInfo.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
|
||||
.name
|
||||
.toUpperCase(),
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom));
|
||||
} else
|
||||
return Text(item.runtimeType.toString());
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -197,4 +244,36 @@ class HistorySection extends StatelessWidget {
|
||||
return dashboardViewModel.wallet.currency.chainIconPath ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTransactionDate(DateTime date, String localeName) {
|
||||
final time = DateFormat.Hm(localeName);
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final thatDay = DateTime(date.year, date.month, date.day);
|
||||
final daysAgo = today.difference(thatDay).inDays;
|
||||
|
||||
final timeStr = time.format(date);
|
||||
|
||||
if (daysAgo == 0) {
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
if (daysAgo == 1) {
|
||||
return "${S.current.yesterday}, $timeStr";
|
||||
}
|
||||
|
||||
if (daysAgo < 7) {
|
||||
final weekday = DateFormat.EEEE(localeName).format(date);
|
||||
return "$weekday, $timeStr";
|
||||
}
|
||||
|
||||
if (date.year == now.year) {
|
||||
final dayMonth = DateFormat("d MMMM", localeName).format(date);
|
||||
return "$dayMonth, $timeStr";
|
||||
}
|
||||
|
||||
final full = DateFormat("d MMM yyyy", localeName).format(date);
|
||||
return "$full, $timeStr";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/select_deselect_all.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class HistorySwapProvidersPage extends StatelessWidget {
|
||||
const HistorySwapProvidersPage({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
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(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SelectDeselectAllBar(
|
||||
title: S.of(context).swap_providers,
|
||||
onSelected: (val) {
|
||||
if ((val &&
|
||||
dashboardViewModel.tradeFilterStore.enabledProvidersCount ==
|
||||
0) ||
|
||||
(!val &&
|
||||
dashboardViewModel.tradeFilterStore.enabledProvidersCount >
|
||||
0)) {
|
||||
dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(ExchangeProviderDescription.all);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
NewListSections(sections: {
|
||||
"": dashboardViewModel.exchangeFilterItems
|
||||
.whereType<SwapProviderFilterItem>()
|
||||
.map((item) => ListItemCheckbox(
|
||||
iconPath: item.providerDescription.image,
|
||||
keyValue: item.caption,
|
||||
label: item.caption,
|
||||
value: item.value(),
|
||||
onChanged: (val) => dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(item.providerDescription)))
|
||||
.toList()
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
@@ -80,13 +81,13 @@ class HistoryTile extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle),
|
||||
child: HashImageWidget(
|
||||
imageUrl: _getDirectionIconToken(),
|
||||
height: 14,
|
||||
width: 14,
|
||||
colorFilter: ColorFilter.mode(
|
||||
direction == TransactionDirection.outgoing
|
||||
? Theme.of(context).colorScheme.inverseSurface.withAlpha(175)
|
||||
: Colors.green,
|
||||
BlendMode.srcIn),
|
||||
width: 14,
|
||||
height: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -99,10 +100,9 @@ class HistoryTile extends StatelessWidget {
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: pending ? 0.5 : 1,
|
||||
child: HashImageWidget(
|
||||
child: TokenImageWidget(
|
||||
imageUrl: asset?.iconPath ?? "",
|
||||
width: 34,
|
||||
height: 34,
|
||||
size: 34,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
@@ -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,
|
||||
|
||||
@@ -87,7 +87,7 @@ class HistoryTileBase extends StatelessWidget {
|
||||
bottomLeft: Radius.circular(roundedBottom ? 22 : 0),
|
||||
bottomRight: Radius.circular(roundedBottom ? 22 : 0),
|
||||
)),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -153,7 +153,7 @@ class HistoryTileBase extends StatelessWidget {
|
||||
),
|
||||
if (bottomSeparator)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 56, right: 16),
|
||||
child: Container(
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HistoryTopBar extends StatelessWidget {
|
||||
const HistoryTopBar({super.key, required this.onTap, required this.roundedBottom});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final bool roundedBottom;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).history,
|
||||
onTap: onTap,
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
key: const ValueKey("history_top_bar_key"),
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
|
||||
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: [
|
||||
SizedBox.shrink(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).history),
|
||||
HashImageWidget(
|
||||
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)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
required this.roundedTop,
|
||||
required this.roundedBottom,
|
||||
required this.bottomSeparator,
|
||||
required this.from,
|
||||
required this.to,
|
||||
this.from,
|
||||
this.to,
|
||||
required this.swapState,
|
||||
required this.provider});
|
||||
|
||||
final CryptoCurrency from;
|
||||
final CryptoCurrency to;
|
||||
final CryptoCurrency? from;
|
||||
final CryptoCurrency? to;
|
||||
final ExchangeProviderDescription provider;
|
||||
final String date;
|
||||
final String amount;
|
||||
@@ -32,6 +32,10 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
final TradeState swapState;
|
||||
|
||||
Widget _getLeadingStack(BuildContext context) {
|
||||
if (from == null || to == null) {
|
||||
return SizedBox(width: 50, height: 50);
|
||||
}
|
||||
|
||||
double currencyIconSize = 22.0;
|
||||
|
||||
return SizedBox(
|
||||
@@ -40,7 +44,7 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: _getIconPath(from), width: currencyIconSize, height: currencyIconSize),
|
||||
imageUrl: _getIconPath(from!), width: currencyIconSize, height: currencyIconSize),
|
||||
Positioned(
|
||||
top: currencyIconSize / 2,
|
||||
left: currencyIconSize / 2,
|
||||
@@ -50,7 +54,7 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
Border.all(width: 2, color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
shape: BoxShape.circle),
|
||||
child: HashImageWidget(
|
||||
imageUrl: _getIconPath(to),
|
||||
imageUrl: _getIconPath(to!),
|
||||
width: currencyIconSize,
|
||||
height: currencyIconSize,
|
||||
),
|
||||
@@ -85,8 +89,10 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500)),
|
||||
Text(from.title, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
if (fromChainIcon.isNotEmpty)
|
||||
if (from?.title != null)
|
||||
Text(from!.title,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
if (fromChainIcon?.isNotEmpty ?? false)
|
||||
HashImageWidget(
|
||||
imageUrl: fromChainIcon,
|
||||
width: 12,
|
||||
@@ -106,10 +112,17 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
receiveAmount.withMaxDecimals(8),
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
to.title,
|
||||
),
|
||||
if (toChainIcon.isNotEmpty) HashImageWidget(imageUrl: toChainIcon, width: 12, height: 12)
|
||||
if (to != null)
|
||||
Text(
|
||||
to!.title,
|
||||
),
|
||||
if (toChainIcon?.isNotEmpty ?? false)
|
||||
HashImageWidget(
|
||||
imageUrl: toChainIcon,
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
],
|
||||
),
|
||||
leadingIcon: _getLeadingStack(context),
|
||||
@@ -137,7 +150,8 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
return "";
|
||||
}
|
||||
|
||||
String _getChainIcon(CryptoCurrency currency) {
|
||||
String? _getChainIcon(CryptoCurrency? currency) {
|
||||
if (currency == null) return null;
|
||||
try {
|
||||
if (currency.title.isNotEmpty) {
|
||||
final parsedCurrency =
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/address_list_item.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/utils/address_formatter.dart';
|
||||
import 'package:hash_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:hash_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/copy_wrapper.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/address_list_item.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/utils/address_formatter.dart";
|
||||
import "package:hash_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});
|
||||
const TransactionDetailsModal(
|
||||
{
|
||||
required this.transactionDetailsViewModel,
|
||||
this.highlightNoteField = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TransactionDetailsViewModel transactionDetailsViewModel;
|
||||
final bool highlightNoteField;
|
||||
|
||||
@override
|
||||
State<TransactionDetailsModal> createState() => _TransactionDetailsModalState();
|
||||
@@ -37,191 +45,237 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
|
||||
widget.transactionDetailsViewModel.updateNote(noteController.text);
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.highlightNoteField) {
|
||||
noteFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.25,
|
||||
maxChildSize: 0.9,
|
||||
snap: true,
|
||||
snapSizes: const [0.9],
|
||||
builder: (context, controller) => 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: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: widget.transactionDetailsViewModel.transactionAsset
|
||||
.iconPath ??
|
||||
"",
|
||||
width: 64,
|
||||
height: 64),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedCryptoAmount,
|
||||
style: TextStyle(fontSize: 28),
|
||||
),
|
||||
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;
|
||||
|
||||
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(
|
||||
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(
|
||||
hintText: S.of(context).add_a_note,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
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
|
||||
]);
|
||||
})
|
||||
]
|
||||
}),
|
||||
)
|
||||
],
|
||||
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: 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),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BalanceCardAction {
|
||||
final String label;
|
||||
@@ -23,7 +24,7 @@ class BalanceCard extends StatelessWidget {
|
||||
this.borderRadius = 20,
|
||||
this.selected = false,
|
||||
this.accountName = "",
|
||||
this.accountBalance = "",
|
||||
this.accountBalance,
|
||||
this.balance = "",
|
||||
this.fiatBalance = "",
|
||||
this.assetName = "",
|
||||
@@ -38,7 +39,7 @@ class BalanceCard extends StatelessWidget {
|
||||
final double width;
|
||||
final double borderRadius;
|
||||
final Gradient? gradient;
|
||||
final String accountBalance;
|
||||
final Money? accountBalance;
|
||||
final String accountName;
|
||||
final String balance;
|
||||
final String fiatBalance;
|
||||
@@ -63,7 +64,7 @@ class BalanceCard extends StatelessWidget {
|
||||
|
||||
final leadText = fiatFirst ? S.of(context).wallet_balance : accountName;
|
||||
|
||||
final bool showText = accountBalance.isNotEmpty ||
|
||||
final bool showText = accountBalance != null ||
|
||||
leadText.isNotEmpty ||
|
||||
balance.isNotEmpty ||
|
||||
fiatBalance.isNotEmpty ||
|
||||
@@ -92,15 +93,18 @@ class BalanceCard extends StatelessWidget {
|
||||
? ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
key: ValueKey(design.imagePath),
|
||||
child: HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
// Purely decorative card artwork.
|
||||
child: ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgFullOff'),
|
||||
key: ValueKey("svgFullOff"),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -115,7 +119,7 @@ class BalanceCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (leadText.isNotEmpty || accountBalance.isNotEmpty)
|
||||
if (leadText.isNotEmpty || accountBalance != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -147,9 +151,16 @@ 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: MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -157,40 +168,51 @@ 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: !fiatFirst && balance.isEmpty
|
||||
? MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
)
|
||||
: 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -203,17 +225,19 @@ class BalanceCard extends StatelessWidget {
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
final currentKey = currentChild?.key;
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
for (final child in previousChildren)
|
||||
if (child.key != currentKey) child,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
key: ValueKey(fiatFirst ? balance : fiatBalance),
|
||||
fiatFirst ? "$assetName $balance" : "$fiatCurrencyTitle $fiatBalance",
|
||||
fiatFirst ? "$assetName $balance" : fiatBalance,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -251,18 +275,12 @@ class BalanceCard extends StatelessWidget {
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
|
||||
? HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
key: const ValueKey('svgIcon'),
|
||||
height: iconWidth,
|
||||
width: iconWidth,
|
||||
colorFilter: ColorFilter.mode(
|
||||
design.colors.backgroundImageColor.withAlpha(80),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
// Purely decorative card artwork.
|
||||
? ExcludeSemantics(
|
||||
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgIconOff'),
|
||||
key: ValueKey("svgIconOff"),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -276,18 +294,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: HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
alignment: Alignment.topRight,
|
||||
colorFilter:
|
||||
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -299,28 +327,58 @@ 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CornerSvgIcon extends StatelessWidget {
|
||||
const _CornerSvgIcon({required this.design, required this.iconWidth});
|
||||
|
||||
final CardDesign design;
|
||||
final double iconWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
key: ValueKey(design.imagePath),
|
||||
height: iconWidth,
|
||||
width: iconWidth,
|
||||
colorFilter: design.preColoredIcon
|
||||
? null
|
||||
: ColorFilter.mode(
|
||||
design.colors.backgroundImageColor.withValues(alpha: 0.33),
|
||||
BlendMode.dstIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'package:hash_wallet/core/utilities.dart';
|
||||
import 'package:cw_core/balance_card_layout.dart';
|
||||
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
@@ -8,11 +10,12 @@ import 'package:hash_wallet/entities/provider_types.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/payment_request.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
@@ -29,14 +32,14 @@ import 'balance_card.dart';
|
||||
class CardsView extends StatefulWidget {
|
||||
const CardsView(
|
||||
{super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.accountListViewModel,
|
||||
required this.lightningMode,
|
||||
required this.onCompactModeBackgroundCardsTapped,
|
||||
required this.onCustomizeTapped});
|
||||
required this.dashboardViewModel,
|
||||
required this.accountListViewModel,
|
||||
required this.lightningMode,
|
||||
required this.onCompactModeBackgroundCardsTapped,
|
||||
required this.onCustomizeTapped});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final MoneroAccountListViewModel? accountListViewModel;
|
||||
final WalletAccountListViewModel? accountListViewModel;
|
||||
final VoidCallback onCompactModeBackgroundCardsTapped;
|
||||
final VoidCallback onCustomizeTapped;
|
||||
final bool lightningMode;
|
||||
@@ -48,22 +51,39 @@ class CardsView extends StatefulWidget {
|
||||
class _CardsViewState extends State<CardsView> {
|
||||
late int _selectedIndex;
|
||||
bool isFirstBuild = true;
|
||||
ReactionDisposer? _cardOrderReaction;
|
||||
|
||||
int get _fallbackSelectedIndex =>
|
||||
max(0, widget.dashboardViewModel.cardOrder.length - 1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
|
||||
reaction(
|
||||
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
|
||||
(_) => setState(() {
|
||||
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
|
||||
}));
|
||||
_selectedIndex = _fallbackSelectedIndex;
|
||||
_cardOrderReaction = reaction(
|
||||
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
|
||||
(_) {
|
||||
if (mounted) setState(() => _selectedIndex = _fallbackSelectedIndex);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cardOrderReaction?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const Duration animDuration = Duration(milliseconds: 200);
|
||||
static const int compactModeTreshold = 4;
|
||||
static const int maxCards = 5;
|
||||
|
||||
/// No account name on the card: Lightning card, or Bitcoin with multi-accounts off.
|
||||
bool get _hideAccountInfo =>
|
||||
widget.lightningMode ||
|
||||
(widget.dashboardViewModel.wallet.type == WalletType.bitcoin &&
|
||||
!widget.dashboardViewModel.isMultiAccountsEnabled);
|
||||
|
||||
Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth,
|
||||
Map<int, int> order, bool compactMode, double overlapAmount) {
|
||||
final baseTop = overlapAmount * (numCards - 1);
|
||||
@@ -74,11 +94,32 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
final top = baseTop - (howFarBehind * overlapAmount);
|
||||
|
||||
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
|
||||
final double effectiveCardWidth = min(cardWidth, 768);
|
||||
|
||||
final left = (parentWidth - effectiveCardWidth) / 2.0;
|
||||
|
||||
final isSelected = _selectedIndex == visualIndex;
|
||||
final accounts = widget.accountListViewModel?.accounts;
|
||||
final account = accounts?.firstWhereOrNull((account) => account.id == realIndex);
|
||||
final cardLabel = !_hideAccountInfo && account != null ? account.label : S.of(context).balance;
|
||||
|
||||
Future<void> onCardTap() async {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
if (account != null && !_hideAccountInfo) {
|
||||
await widget.accountListViewModel!.select(account);
|
||||
}
|
||||
if (mounted) setState(() => _selectedIndex = visualIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void onCardLongPress() {
|
||||
if (_selectedIndex == visualIndex) {
|
||||
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
|
||||
return AnimatedPositioned(
|
||||
key: ValueKey("$visualIndex $realIndex"),
|
||||
duration: animDuration,
|
||||
@@ -89,125 +130,117 @@ 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 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: (_) {
|
||||
final account = widget.accountListViewModel?.accounts
|
||||
.firstWhereOrNull((account) => account.id == 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);
|
||||
// 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 = '●●●●●●';
|
||||
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?.combinedAvailableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
|
||||
walletBalance = "";
|
||||
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalanceRaw ?? "0.00";
|
||||
}
|
||||
|
||||
// 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 card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final designIndex = widget.dashboardViewModel.cardAccountIndices.indexOf(
|
||||
widget.lightningMode ? -2 : realIndex,
|
||||
);
|
||||
final cardDesign = designIndex < 0 ||
|
||||
designIndex >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[designIndex];
|
||||
|
||||
final String accountName;
|
||||
final String accountBalance;
|
||||
if (account == null) {
|
||||
accountName = "";
|
||||
accountBalance = "";
|
||||
} else {
|
||||
accountName = account.label;
|
||||
accountBalance = account.balance ?? "0.00";
|
||||
}
|
||||
final showAccount = account != null && !_hideAccountInfo;
|
||||
final accountName = showAccount ? account.label : "";
|
||||
final accountBalance = showAccount ? account.balance : null;
|
||||
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
|
||||
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 &&
|
||||
ProvidersHelper.getAvailableBuyProviderTypes().isNotEmpty)
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.buy,
|
||||
icon: Icons.arrow_forward_ios_rounded,
|
||||
iconSize: 12,
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
|
||||
)
|
||||
]
|
||||
: [];
|
||||
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 &&
|
||||
ProvidersHelper.getAvailableBuyProviderTypes().isNotEmpty)
|
||||
? [
|
||||
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?.fiatCurrencyTicker ??
|
||||
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,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -234,45 +267,46 @@ class _CardsViewState extends State<CardsView> {
|
||||
}
|
||||
}
|
||||
|
||||
double get effectiveCardWidth => min(MediaQuery.of(context).size.width * 0.878,
|
||||
responsiveLayoutUtil.shouldRenderMobileUI ? 768 : 512);
|
||||
|
||||
double _getBoxHeight(int numCards, double overlapAmount) {
|
||||
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
|
||||
final double effectiveCardWidth = min(cardWidth, 768);
|
||||
return
|
||||
/* height of initial card */
|
||||
(2 / 3.2) * (effectiveCardWidth) +
|
||||
/* height of bg card * amount of bg cards */
|
||||
overlapAmount * ((numCards) - 1);
|
||||
/* height of initial card */
|
||||
(2 / 3.2) * (effectiveCardWidth) +
|
||||
/* height of bg card * amount of bg cards */
|
||||
overlapAmount * ((numCards) - 1);
|
||||
}
|
||||
|
||||
|
||||
int? _visualIndexForSelectedAccount(Map<int, int> order) {
|
||||
final vm = widget.accountListViewModel;
|
||||
if (vm == null || _hideAccountInfo) return null;
|
||||
|
||||
final selectedId = vm.selectedAccount?.id;
|
||||
for (final entry in order.entries) {
|
||||
if (entry.value == selectedId) return entry.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (context) {
|
||||
Widget build(BuildContext context) => Observer(builder: (context) {
|
||||
final parentWidth = MediaQuery.of(context).size.width;
|
||||
final children = <Widget>[];
|
||||
|
||||
int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin
|
||||
? 1
|
||||
: widget.dashboardViewModel.cardDesigns.length;
|
||||
if (numCards == 0) numCards = 1;
|
||||
|
||||
if (_selectedIndex >= (numCards)) {
|
||||
_selectedIndex = 0;
|
||||
final stack = _hideAccountInfo
|
||||
? const [0]
|
||||
: BalanceCardLayout.stackFromOrder(widget.dashboardViewModel.cardOrder);
|
||||
if (stack.isEmpty) return const SizedBox.shrink();
|
||||
final numCards = stack.length;
|
||||
if (_selectedIndex < 0 || _selectedIndex >= numCards) {
|
||||
_selectedIndex = numCards - 1;
|
||||
}
|
||||
final order = {for (int i = 0; i < stack.length; i++) i: stack[i]};
|
||||
|
||||
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
|
||||
? Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
)
|
||||
: widget.dashboardViewModel.cardOrder;
|
||||
|
||||
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
|
||||
int visualIndex = (_selectedIndex - i + numCards) % numCards;
|
||||
if (order[visualIndex] == null) {
|
||||
order = Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
);
|
||||
}
|
||||
}
|
||||
final followIndex = _visualIndexForSelectedAccount(order);
|
||||
if (followIndex != null) _selectedIndex = followIndex;
|
||||
|
||||
final bool compactMode = numCards >= compactModeTreshold;
|
||||
final double overlapAmount = compactMode ? 5.0 : 46.0;
|
||||
@@ -281,14 +315,6 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
int realIndex = order[visualIndex]!;
|
||||
|
||||
if (visualIndex == _selectedIndex &&
|
||||
widget.accountListViewModel != null &&
|
||||
realIndex < widget.accountListViewModel!.accounts.length &&
|
||||
widget.accountListViewModel?.selected.label !=
|
||||
widget.accountListViewModel?.accounts[realIndex].label) {
|
||||
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
}
|
||||
|
||||
children.add(_buildCard(
|
||||
visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount));
|
||||
}
|
||||
@@ -306,7 +332,6 @@ class _CardsViewState extends State<CardsView> {
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> depositToL2() async {
|
||||
PaymentRequest? paymentRequest = null;
|
||||
@@ -318,19 +343,19 @@ class _CardsViewState extends State<CardsView> {
|
||||
}
|
||||
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final depositAddress =
|
||||
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
|
||||
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
|
||||
if ((depositAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
|
||||
}
|
||||
}
|
||||
|
||||
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final page = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: UnspentCoinType.nonMweb,
|
||||
mode: SendPageModes.lightningDeposit,
|
||||
));
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: UnspentCoinType.nonMweb,
|
||||
mode: SendPageModes.lightningDeposit,
|
||||
));
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(128),
|
||||
@@ -371,13 +396,13 @@ class _CardsViewState extends State<CardsView> {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
}
|
||||
|
||||
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final page = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: unspentCoinType,
|
||||
mode: SendPageModes.lightningWithdrawal,
|
||||
));
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: unspentCoinType,
|
||||
mode: SendPageModes.lightningWithdrawal,
|
||||
));
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(128),
|
||||
|
||||
@@ -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: [
|
||||
HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class SeedBackupReminderCard extends StatelessWidget {
|
||||
const SeedBackupReminderCard({required this.dashboardViewModel, required this.onTap, super.key});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (!dashboardViewModel.shouldShowSeedBackupReminder) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18, top: 24),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/key.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).back_up_recovery_phrase,
|
||||
style: textTheme.labelLarge?.copyWith(
|
||||
color: colors.onSurface,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).back_up_recovery_phrase_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum _IconShape {
|
||||
alreadyCircular,
|
||||
clipOnly,
|
||||
hollowNeedsBackdrop,
|
||||
}
|
||||
|
||||
class TokenImageWidget extends StatefulWidget {
|
||||
const TokenImageWidget({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
required this.size,
|
||||
this.errorWidget,
|
||||
this.semanticsLabel,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final double size;
|
||||
final Widget? errorWidget;
|
||||
|
||||
/// Accessible name for the token artwork. Leave `null` (the default) when the
|
||||
/// token is already named by adjacent text, so the image stays decorative.
|
||||
final String? semanticsLabel;
|
||||
|
||||
@override
|
||||
State<TokenImageWidget> createState() => _TokenImageWidgetState();
|
||||
}
|
||||
|
||||
class _TokenImageWidgetState extends State<TokenImageWidget> {
|
||||
static final Map<String, _IconShape> _shapeCache = {};
|
||||
|
||||
_IconShape _shape = _IconShape.clipOnly;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resolveShape();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TokenImageWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||
_resolveShape();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolveShape() async {
|
||||
final url = widget.imageUrl;
|
||||
|
||||
final cached = _shapeCache[url];
|
||||
if (cached != null) {
|
||||
if (mounted && _shape != cached) {
|
||||
setState(() => _shape = cached);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final shape = await _analyzeShape(url);
|
||||
_shapeCache[url] = shape;
|
||||
if (mounted) {
|
||||
setState(() => _shape = shape);
|
||||
}
|
||||
}
|
||||
|
||||
Future<_IconShape> _analyzeShape(String url) async {
|
||||
if (url.toLowerCase().endsWith('.svg')) return _IconShape.alreadyCircular;
|
||||
|
||||
final ImageProvider provider;
|
||||
if (url.startsWith('assets/')) {
|
||||
provider = AssetImage(url);
|
||||
} else if (url.startsWith('http')) {
|
||||
provider = NetworkImage(url);
|
||||
} else {
|
||||
return _IconShape.clipOnly;
|
||||
}
|
||||
|
||||
try {
|
||||
final completer = Completer<ui.Image>();
|
||||
final stream = provider.resolve(ImageConfiguration.empty);
|
||||
late final ImageStreamListener listener;
|
||||
listener = ImageStreamListener(
|
||||
(info, _) {
|
||||
if (!completer.isCompleted) completer.complete(info.image);
|
||||
},
|
||||
onError: (error, _) {
|
||||
if (!completer.isCompleted) completer.completeError(error);
|
||||
},
|
||||
);
|
||||
stream.addListener(listener);
|
||||
|
||||
final image = await completer.future;
|
||||
stream.removeListener(listener);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
if (byteData == null) return _IconShape.clipOnly;
|
||||
|
||||
final totalPixels = image.width * image.height;
|
||||
var transparentPixels = 0;
|
||||
for (var i = 0; i < totalPixels; i++) {
|
||||
final alpha = byteData.getUint8(i * 4 + 3);
|
||||
if (alpha < 128) transparentPixels++;
|
||||
}
|
||||
|
||||
final ratio = transparentPixels / totalPixels;
|
||||
|
||||
if (ratio > 0.15 && ratio < 0.30) {
|
||||
return _IconShape.alreadyCircular;
|
||||
}
|
||||
|
||||
if (ratio >= 0.30) {
|
||||
return _IconShape.hollowNeedsBackdrop;
|
||||
}
|
||||
|
||||
return _IconShape.clipOnly;
|
||||
} catch (e) {
|
||||
printV('TokenImageWidget: failed to analyze $url: $e');
|
||||
return _IconShape.clipOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: HashImageWidget(
|
||||
imageUrl: widget.imageUrl,
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
errorWidget: widget.errorWidget,
|
||||
semanticsLabel: widget.semanticsLabel,
|
||||
),
|
||||
);
|
||||
|
||||
switch (_shape) {
|
||||
case _IconShape.alreadyCircular:
|
||||
return image;
|
||||
case _IconShape.clipOnly:
|
||||
return ClipOval(child: image);
|
||||
case _IconShape.hollowNeedsBackdrop:
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,28 +10,36 @@ class ChainIcon extends StatelessWidget {
|
||||
{super.key,
|
||||
required this.iconPath,
|
||||
required this.dashboardViewModel,
|
||||
required this.isSyncHeavy});
|
||||
required this.isSyncHeavy,
|
||||
required this.showSyncedMessage});
|
||||
|
||||
final String iconPath;
|
||||
final bool isSyncHeavy;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool showSyncedMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final progress = dashboardViewModel.status.progress();
|
||||
final done = !isSyncHeavy || progress >= 1;
|
||||
final done = !showSyncedMessage && (!isSyncHeavy || progress >= 1);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
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 || showSyncedMessage,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
color: showSyncedMessage ? Color(0xFFFF12A439) : Color(0xFFFFB84E),
|
||||
strokeWidth: 2,
|
||||
semanticsLabel: S.of(context).synchronizing,
|
||||
semanticsValue: "${(progress * 100).round()}%",
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedScale(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -12,47 +13,50 @@ 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,
|
||||
// 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: 63,
|
||||
height: 36,
|
||||
padding: const EdgeInsets.all(4.5),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
alignment: Alignment.centerRight,
|
||||
margin: EdgeInsets.only(left: lightningMode ? 27 : 0),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: 27,
|
||||
height: 27,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: HashImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
|
||||
imageUrl: "assets/new-ui/switcher-bitcoin.svg",
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 27,
|
||||
height: 27,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
@@ -62,14 +66,14 @@ class LightningSwitcher extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: HashImageWidget(
|
||||
imageUrl: 'assets/new-ui/switcher-lightning.svg',
|
||||
imageUrl: "assets/new-ui/switcher-lightning.svg",
|
||||
key: ValueKey(lightningMode),
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 27,
|
||||
height: 27,
|
||||
colorFilter: ColorFilter.mode(
|
||||
lightningMode
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
@@ -80,8 +84,8 @@ class LightningSwitcher extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import 'package:hash_wallet/core/sync_status_title.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/settings/manage_nodes_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import "package:flutter/cupertino.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';
|
||||
|
||||
class SyncBar extends StatelessWidget {
|
||||
SyncBar({super.key, required this.dashboardViewModel, required this.isSyncHeavy});
|
||||
const SyncBar({
|
||||
required this.dashboardViewModel,
|
||||
required this.isSyncHeavy,
|
||||
required this.showSyncedMessage,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool isSyncHeavy;
|
||||
final bool showSyncedMessage;
|
||||
|
||||
static const failStatuses = [
|
||||
FailedSyncStatus,
|
||||
@@ -36,87 +43,170 @@ class SyncBar extends StatelessWidget {
|
||||
ConnectedSyncStatus,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final status = dashboardViewModel.status;
|
||||
final Widget? icon = _getIcon(context, status.runtimeType);
|
||||
static const syncedColor = Color(0xFF12A439);
|
||||
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
if (!_showFullBar())
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (dashboardViewModel.isTorEnabled)
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/tor.svg",
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
if (_showDot()) PulsingDot(),
|
||||
],
|
||||
),
|
||||
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),
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
final status = dashboardViewModel.status;
|
||||
if (!_showFullBar) {
|
||||
return _buildCompactBar(context);
|
||||
}
|
||||
|
||||
final Widget? icon = _getIcon(context, status.runtimeType);
|
||||
final statusTitle = _statusTitle(context, status);
|
||||
final silentPaymentsProgress = _silentPaymentsProgress(status);
|
||||
final barHeight = silentPaymentsProgress == null ? 36.0 : 40.0;
|
||||
|
||||
// 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.
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: _statusSemanticsLabel(context, status, statusTitle),
|
||||
value: silentPaymentsProgress,
|
||||
hint: S.of(context).manage_nodes,
|
||||
onTap: () => _openNodeManagement(context),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: () => _openNodeManagement(context),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: OverflowBox(
|
||||
key: ValueKey(status.runtimeType),
|
||||
alignment: Alignment.center,
|
||||
minHeight: barHeight,
|
||||
maxHeight: barHeight,
|
||||
child: Container(
|
||||
key: ValueKey(status.runtimeType),
|
||||
height: 36,
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999),
|
||||
border: _getBorder(context, status.runtimeType),
|
||||
color: _getBackgroundColor(context, status.runtimeType),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
spacing: 8,
|
||||
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),
|
||||
if (silentPaymentsProgress != null) ...[
|
||||
Text(
|
||||
silentPaymentsProgress,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"·",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
statusTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _getTextStyle(context, status.runtimeType),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (dashboardViewModel.isTorEnabled)
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/tor.svg",
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
if (_showDot())
|
||||
CupertinoActivityIndicator(
|
||||
key: ValueKey(dashboardViewModel.status.runtimeType),
|
||||
radius: 8,
|
||||
),
|
||||
if (_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
|
||||
],
|
||||
);
|
||||
|
||||
final label = _joinLabels([
|
||||
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
|
||||
if (_showDot()) S.of(context).synchronizing,
|
||||
if (_showLightSyncCheck()) S.of(context).sync_synced,
|
||||
]);
|
||||
|
||||
if (label.isEmpty) {
|
||||
return row;
|
||||
}
|
||||
|
||||
return Semantics(label: label, child: ExcludeSemantics(child: row));
|
||||
}
|
||||
|
||||
bool get _isShowingSyncedMessage =>
|
||||
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
|
||||
|
||||
String _statusTitle(BuildContext context, SyncStatus status) {
|
||||
final title = syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode);
|
||||
|
||||
return title.isNotEmpty ? title : S.of(context).synchronizing;
|
||||
}
|
||||
|
||||
String _statusSemanticsLabel(BuildContext context, SyncStatus status, String statusTitle) {
|
||||
final isFailure = failStatuses.contains(status.runtimeType);
|
||||
|
||||
return _joinLabels([
|
||||
statusTitle,
|
||||
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(", ");
|
||||
|
||||
String? _silentPaymentsProgress(SyncStatus status) {
|
||||
if (!dashboardViewModel.hasSilentPayments ||
|
||||
!dashboardViewModel.silentPaymentsScanningActive ||
|
||||
status is! SyncingSyncStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "${(status.progress() * 100).toInt()}%";
|
||||
}
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context, Type status) {
|
||||
if (failStatuses.contains(status)) {
|
||||
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
|
||||
@@ -126,7 +216,7 @@ class SyncBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
Border? _getBorder(BuildContext context, Type status) {
|
||||
if (progressStatuses.contains(status)) {
|
||||
if (progressStatuses.contains(status) || _isShowingSyncedMessage) {
|
||||
return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1);
|
||||
}
|
||||
|
||||
@@ -134,19 +224,24 @@ class SyncBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
TextStyle? _getTextStyle(BuildContext context, Type status) {
|
||||
final Color color;
|
||||
if (failStatuses.contains(status)) {
|
||||
return TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.error);
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
} else if (status == SyncedSyncStatus) {
|
||||
color = syncedColor;
|
||||
} else {
|
||||
return TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant);
|
||||
color = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
|
||||
return TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: color);
|
||||
}
|
||||
|
||||
Widget? _getIcon(BuildContext context, Type status) {
|
||||
if (status == LostConnectionSyncStatus || status == FailedSyncStatus) {
|
||||
if (status == SyncedSyncStatus) {
|
||||
return const Icon(Icons.check, color: syncedColor, size: 12);
|
||||
}
|
||||
|
||||
if (status == LostConnectionSyncStatus) {
|
||||
return HashImageWidget(
|
||||
imageUrl: "assets/new-ui/offline.svg",
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
|
||||
@@ -163,22 +258,35 @@ class SyncBar extends StatelessWidget {
|
||||
final List<Widget> children = [];
|
||||
|
||||
if (dashboardViewModel.isTorEnabled) {
|
||||
children.add(HashImageWidget(
|
||||
children.add(
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/tor_sync.svg",
|
||||
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn)));
|
||||
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (dashboardViewModel.hasMweb) {
|
||||
children.add(HashImageWidget(
|
||||
imageUrl: "assets/new-ui/mweb_sync.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
));
|
||||
children.add(
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/mweb_sync.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (dashboardViewModel.hasSilentPayments) {
|
||||
children.add(HashImageWidget(
|
||||
imageUrl: "assets/new-ui/silent_sync.svg",
|
||||
colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn),
|
||||
));
|
||||
children.add(
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/silent_sync.svg",
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(context.customColors.warningOutlineColor, BlendMode.srcIn),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (children.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Row(
|
||||
@@ -187,12 +295,16 @@ class SyncBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
bool _showFullBar() {
|
||||
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) return false;
|
||||
bool get _showFullBar {
|
||||
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) {
|
||||
return isSyncHeavy && _isShowingSyncedMessage;
|
||||
}
|
||||
|
||||
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
|
||||
}
|
||||
|
||||
bool _showDot() {
|
||||
return !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
|
||||
}
|
||||
bool _showDot() =>
|
||||
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
|
||||
|
||||
bool _showLightSyncCheck() => !isSyncHeavy && _isShowingSyncedMessage;
|
||||
}
|
||||
|
||||
@@ -1,73 +1,193 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/wallet_info.dart";
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class TopBar extends StatelessWidget {
|
||||
class TopBar extends StatefulWidget {
|
||||
const TopBar({
|
||||
super.key,
|
||||
required this.lightningMode,
|
||||
required this.onLightningSwitchPress,
|
||||
required this.dashboardViewModel,
|
||||
required this.onSettingsButtonPress,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool lightningMode;
|
||||
final VoidCallback onLightningSwitchPress;
|
||||
final VoidCallback onSettingsButtonPress;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
|
||||
child: Observer(
|
||||
builder: (_) => Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
(dashboardViewModel.hasLightning)
|
||||
? LightningSwitcher(
|
||||
lightningMode: lightningMode,
|
||||
onLightningSwitchPress: onLightningSwitchPress,
|
||||
)
|
||||
: ChainIcon(
|
||||
iconPath: dashboardViewModel.wallet.currency.flatIconPath ?? "",
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isSyncHeavy: dashboardViewModel.isSyncHeavy),
|
||||
SyncBar(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isSyncHeavy: dashboardViewModel.isSyncHeavy,
|
||||
),
|
||||
ModernButton.svg(
|
||||
// Hash Bags: was theme.colorScheme.primary (dark forest green)
|
||||
// which rendered the gear icon nearly invisible on the dark
|
||||
// dashboard backdrop. onSurfaceVariant is the de-emphasized-
|
||||
// foreground Material token.
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 36,
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onSettingsButtonPress();
|
||||
},
|
||||
svgPath: "assets/new-ui/top-settings.svg",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
State<TopBar> createState() => _TopBarState();
|
||||
}
|
||||
|
||||
class _TopBarState extends State<TopBar> {
|
||||
static const syncedMessageDuration = Duration(seconds: 3);
|
||||
|
||||
bool showSyncedMessage = false;
|
||||
Timer? syncedMessageTimer;
|
||||
ReactionDisposer? _statusReactionDisposer;
|
||||
|
||||
bool get replacesWalletName {
|
||||
final status = widget.dashboardViewModel.status.runtimeType;
|
||||
if (status == SyncedSyncStatus) {
|
||||
return showSyncedMessage;
|
||||
}
|
||||
|
||||
return widget.dashboardViewModel.isSyncHeavy ||
|
||||
SyncBar.progressStatuses.contains(status) ||
|
||||
SyncBar.failStatuses.contains(status);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bindStatusReaction();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TopBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (identical(oldWidget.dashboardViewModel, widget.dashboardViewModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncedMessageTimer?.cancel();
|
||||
syncedMessageTimer = null;
|
||||
showSyncedMessage = false;
|
||||
_statusReactionDisposer?.call();
|
||||
_bindStatusReaction();
|
||||
}
|
||||
|
||||
void _bindStatusReaction() {
|
||||
_statusReactionDisposer = reaction(
|
||||
(_) => widget.dashboardViewModel.status.runtimeType,
|
||||
(status) {
|
||||
syncedMessageTimer?.cancel();
|
||||
syncedMessageTimer = null;
|
||||
|
||||
if (status == SyncedSyncStatus) {
|
||||
if (mounted) {
|
||||
setState(() => showSyncedMessage = true);
|
||||
}
|
||||
syncedMessageTimer = Timer(syncedMessageDuration, () {
|
||||
syncedMessageTimer = null;
|
||||
if (mounted) {
|
||||
setState(() => showSyncedMessage = false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => showSyncedMessage = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
syncedMessageTimer?.cancel();
|
||||
syncedMessageTimer = null;
|
||||
_statusReactionDisposer?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
|
||||
child: Observer(
|
||||
builder: (_) => Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
widget.dashboardViewModel.hasLightning
|
||||
? LightningSwitcher(
|
||||
lightningMode: widget.dashboardViewModel.lightningMode,
|
||||
onLightningSwitchPress: widget.onLightningSwitchPress,
|
||||
)
|
||||
: ChainIcon(
|
||||
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
|
||||
showSyncedMessage: showSyncedMessage,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
reverseDuration: Duration.zero,
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
child: replacesWalletName
|
||||
? SyncBar(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
|
||||
showSyncedMessage: showSyncedMessage,
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: WalletInfoBar(
|
||||
name: widget.dashboardViewModel.wallet.name,
|
||||
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
|
||||
),
|
||||
),
|
||||
if (widget.dashboardViewModel.isTorEnabled) ...[
|
||||
const SizedBox(width: 6),
|
||||
SyncBar(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
|
||||
showSyncedMessage: showSyncedMessage,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ModernButton.svg(
|
||||
key: const ValueKey("home_page_settings_button_key"),
|
||||
// Hash Bags: was theme.colorScheme.primary (dark forest green)
|
||||
// which rendered the gear icon nearly invisible on the dark
|
||||
// dashboard backdrop. onSurfaceVariant is the de-emphasized-
|
||||
// foreground Material token.
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 36,
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
widget.onSettingsButtonPress();
|
||||
},
|
||||
svgPath: "assets/new-ui/top-settings.svg",
|
||||
semanticLabel: S.of(context).settings_title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
//FIXME remove after this gets fixed flutter-side
|
||||
double _additionalTopPadding(BuildContext context) {
|
||||
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) return 24;
|
||||
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) {
|
||||
return 24;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_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,89 +23,94 @@ 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),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: show
|
||||
? Column(
|
||||
children: [
|
||||
const SizedBox(height: 12, width: double.infinity),
|
||||
Observer(builder: (context) {
|
||||
final balance =
|
||||
dashboardViewModel.balanceViewModel.additionalBalance(currency);
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: show
|
||||
? Column(
|
||||
children: [
|
||||
const SizedBox(height: 12, width: double.infinity),
|
||||
Observer(builder: (context) {
|
||||
final balance = dashboardViewModel.balanceViewModel.additionalBalance(currency);
|
||||
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width * 0.87,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showMaterialModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return UnconfirmedBalanceModal(
|
||||
balance: "${balance} ${currency.title}",
|
||||
currencyIconPath: currency.iconPath ?? "",
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
value: dashboardViewModel.confirmationProgress,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
"$balance ${currency.title}",
|
||||
style: TextStyle(
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width * 0.87,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Material(
|
||||
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) => modal);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
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,
|
||||
value: dashboardViewModel.confirmationProgress,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
"$balance ${currency.title}",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(S.of(context).confirming),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
)
|
||||
: const SizedBox(
|
||||
width: double.infinity,
|
||||
));
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
)
|
||||
: const SizedBox(width: double.infinity),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -129,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),
|
||||
@@ -175,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,84 +1,64 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class WalletInfoBar extends StatelessWidget {
|
||||
const WalletInfoBar(
|
||||
{super.key,
|
||||
required this.lightningMode,
|
||||
required this.name,
|
||||
required this.hardwareWalletType,
|
||||
required this.onCustomizeButtonTap,
|
||||
required this.hasCustomize});
|
||||
const WalletInfoBar({required this.name, required this.hardwareWalletType, super.key});
|
||||
|
||||
final bool lightningMode;
|
||||
final String name;
|
||||
final HardwareWalletType? hardwareWalletType;
|
||||
final bool hasCustomize;
|
||||
final VoidCallback onCustomizeButtonTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (hasCustomize) {
|
||||
onCustomizeButtonTap();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return SizeTransition(
|
||||
final semanticsLabel =
|
||||
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
|
||||
|
||||
return Semantics(
|
||||
label: semanticsLabel,
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) => SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: animation,
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: hardwareWalletIcon == null
|
||||
? const SizedBox.shrink(key: ValueKey("empty"))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: HashImageWidget(
|
||||
imageUrl: hardwareWalletIcon!,
|
||||
key: ValueKey("hardware_wallet_icon"),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
child: hardwareWalletIcon == null
|
||||
? const SizedBox.shrink(key: ValueKey("empty"))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: HashImageWidget(
|
||||
imageUrl: hardwareWalletIcon!,
|
||||
key: const ValueKey("hardware_wallet_icon"),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
if (hasCustomize) ...[
|
||||
SizedBox(width: 8),
|
||||
ModernButton.svg(
|
||||
size: 24,
|
||||
onPressed: () {
|
||||
if (hasCustomize) {
|
||||
onCustomizeButtonTap();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
},
|
||||
svgPath: "assets/new-ui/icon-accounts.svg",
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
key: const ValueKey("home_page_wallet_name_text_key"),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -88,11 +68,11 @@ class WalletInfoBar extends StatelessWidget {
|
||||
case null:
|
||||
return null;
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/images/hardware_wallet/device_bitbox.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_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: HashImageWidget(
|
||||
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 HashImageWidget(
|
||||
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://github.com/Such-Software/hash-wallet"));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConfirmSwiper extends StatefulWidget {
|
||||
final VoidCallback onConfirmed;
|
||||
final String swiperText;
|
||||
|
||||
const ConfirmSwiper({super.key, required this.onConfirmed, required this.swiperText});
|
||||
/// Label of the plain button rendered instead of the swiper when the platform
|
||||
/// reports accessible navigation (VoiceOver / TalkBack), which intercepts the
|
||||
/// horizontal drag the swiper depends on. Falls back to [swiperText].
|
||||
final String? accessibleNavigationModeButtonText;
|
||||
|
||||
const ConfirmSwiper({
|
||||
super.key,
|
||||
required this.onConfirmed,
|
||||
required this.swiperText,
|
||||
this.accessibleNavigationModeButtonText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConfirmSwiper> createState() => _ConfirmSwiperState();
|
||||
@@ -30,6 +41,15 @@ class _ConfirmSwiperState extends State<ConfirmSwiper> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (MediaQuery.of(context).accessibleNavigation) {
|
||||
return NewPrimaryButton(
|
||||
onPressed: widget.onConfirmed,
|
||||
text: widget.accessibleNavigationModeButtonText ?? widget.swiperText,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
|
||||
final radius = (pillSize + pillHorizontalPadding * 2) / 2;
|
||||
|
||||
return LayoutBuilder(
|
||||
@@ -41,7 +61,7 @@ class _ConfirmSwiperState extends State<ConfirmSwiper> {
|
||||
final maxDrag = areaWidth - pillSize - pillHorizontalPadding;
|
||||
final triggerAt = maxDrag - _triggerThreshold;
|
||||
|
||||
return GestureDetector(
|
||||
final swiper = GestureDetector(
|
||||
onHorizontalDragUpdate: (d) {
|
||||
setState(() {
|
||||
drag = max(pillHorizontalPadding, min(maxDrag, drag + d.delta.dx));
|
||||
@@ -98,6 +118,17 @@ class _ConfirmSwiperState extends State<ConfirmSwiper> {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// A single accessibility node for the whole control: the flowing label and
|
||||
// the arrow knob are decorative and must not become separate stops. No
|
||||
// semantics action is exposed here; a screen reader gets the button
|
||||
// variant above instead.
|
||||
return Semantics(
|
||||
container: true,
|
||||
excludeSemantics: true,
|
||||
label: widget.swiperText,
|
||||
child: swiper,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_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 {
|
||||
const CopyWrapper(
|
||||
{super.key,
|
||||
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 Widget Function(BuildContext, bool) builder;
|
||||
final bool requireLongPress;
|
||||
|
||||
/// 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
|
||||
@@ -28,6 +44,12 @@ class _CopyWrapperState extends State<CopyWrapper> {
|
||||
void handleCopy() async {
|
||||
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, () {
|
||||
@@ -38,10 +60,41 @@ class _CopyWrapperState extends State<CopyWrapper> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChainChipStrip extends StatelessWidget {
|
||||
const ChainChipStrip({
|
||||
super.key,
|
||||
required this.walletTypes,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<WalletType> walletTypes;
|
||||
final WalletType? selected;
|
||||
final ValueChanged<WalletType?> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: 48,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
children: [
|
||||
_ChainChip(
|
||||
label: S.of(context).picker_chip_all,
|
||||
isSelected: selected == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
for (final type in walletTypes)
|
||||
_ChainChip(
|
||||
label: walletTypeToString(type),
|
||||
isSelected: selected == type,
|
||||
onTap: () => onSelected(type),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChainChip extends StatelessWidget {
|
||||
const _ChainChip({
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import "package:hash_wallet/evm/evm.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/currency_groups.dart";
|
||||
import "package:cw_core/erc20_token.dart";
|
||||
import "package:cw_core/spl_token.dart";
|
||||
import "package:cw_core/tron_token.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
String chainNameForCurrency(CryptoCurrency c) {
|
||||
if (c == CryptoCurrency.btcln) {
|
||||
return "Lightning";
|
||||
}
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
return wt != null ? walletTypeToString(wt) : (c.tag ?? "");
|
||||
}
|
||||
|
||||
final Set<String> _stablecoinSymbols = {
|
||||
for (final c in CryptoCurrency.all)
|
||||
if (c.groups.contains(CurrencyGroups.stablecoin)) c.title.toUpperCase(),
|
||||
};
|
||||
|
||||
bool isTrustedStablecoin(CryptoCurrency c) =>
|
||||
(c.groups.contains(CurrencyGroups.stablecoin) ||
|
||||
_stablecoinSymbols.contains(c.title.toUpperCase())) &&
|
||||
!c.isPotentialScam;
|
||||
|
||||
bool hasBuiltInStablecoinSymbol(CryptoCurrency c) =>
|
||||
_stablecoinSymbols.contains(c.title.toUpperCase());
|
||||
|
||||
const _kEvmDefaultTokenNatives = <CryptoCurrency>[
|
||||
CryptoCurrency.baseEth,
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.maticpoly,
|
||||
];
|
||||
|
||||
void appendEvmDefaultTokens(List<CryptoCurrency> into) {
|
||||
if (evm == null) {
|
||||
return;
|
||||
}
|
||||
String keyFor(CryptoCurrency c) {
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
final chain = wt?.toString() ?? (c.tag ?? "").toUpperCase();
|
||||
return "${c.title.toUpperCase()}|$chain";
|
||||
}
|
||||
|
||||
final seen = <String>{for (final c in into) keyFor(c)};
|
||||
for (final native in _kEvmDefaultTokenNatives) {
|
||||
final chainId = getChainIdByCryptoCurrency(native);
|
||||
if (chainId == null) {
|
||||
continue;
|
||||
}
|
||||
for (final t in evm!.getDefaultTokensByChainId(chainId)) {
|
||||
if (seen.add(keyFor(t))) {
|
||||
into.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CurrencyPickerBalance {
|
||||
const CurrencyPickerBalance({required this.amount, this.fiat, this.fiatValue});
|
||||
|
||||
final String amount;
|
||||
final String? fiat;
|
||||
final double? fiatValue;
|
||||
}
|
||||
|
||||
String? _assetAddressKey(CryptoCurrency c) {
|
||||
if (c is Erc20Token) {
|
||||
return c.contractAddress.toLowerCase();
|
||||
}
|
||||
if (c is TronToken) {
|
||||
return c.contractAddress.toLowerCase();
|
||||
}
|
||||
if (c is SPLToken) {
|
||||
return c.mintAddress.toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CurrencyPickerBalance? balanceForAsset(
|
||||
Map<CryptoCurrency, CurrencyPickerBalance>? balances,
|
||||
CryptoCurrency asset,
|
||||
) {
|
||||
if (balances == null || balances.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final direct = balances[asset];
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
final addressKey = _assetAddressKey(asset);
|
||||
if (addressKey != null) {
|
||||
for (final entry in balances.entries) {
|
||||
if (_assetAddressKey(entry.key) == addressKey) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final title = asset.title.toUpperCase();
|
||||
CurrencyPickerBalance? byTitle;
|
||||
for (final entry in balances.entries) {
|
||||
if (entry.key.title.toUpperCase() == title) {
|
||||
if (byTitle != null) {
|
||||
return null;
|
||||
}
|
||||
byTitle = entry.value;
|
||||
}
|
||||
}
|
||||
return byTitle;
|
||||
}
|
||||
|
||||
enum RecentsSource { none, trades, orders }
|
||||
|
||||
class CurrencyPickerArgs {
|
||||
const CurrencyPickerArgs({
|
||||
this.selected,
|
||||
required this.items,
|
||||
this.showStablesHeader = true,
|
||||
this.balanceByAsset,
|
||||
this.filterByNetwork,
|
||||
required this.onSelected,
|
||||
required this.symbolResolver,
|
||||
this.recentsSource = RecentsSource.none,
|
||||
this.useSingleNetworkLayout = false,
|
||||
});
|
||||
|
||||
final CryptoCurrency? selected;
|
||||
final List<CryptoCurrency> items;
|
||||
final WalletType? filterByNetwork;
|
||||
final bool showStablesHeader;
|
||||
final void Function(CryptoCurrency) onSelected;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset;
|
||||
final RecentsSource recentsSource;
|
||||
final bool useSingleNetworkLayout;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerListContainer extends StatelessWidget {
|
||||
const CurrencyPickerListContainer({super.key, required this.rows});
|
||||
|
||||
final List<Widget> rows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
children.add(rows[i]);
|
||||
if (i != rows.length - 1) {
|
||||
children.add(Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colors.surfaceContainerHigh,
|
||||
indent: 56,
|
||||
endIndent: 24,
|
||||
));
|
||||
}
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: children),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerRow extends StatelessWidget {
|
||||
const CurrencyPickerRow({
|
||||
super.key,
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.trailing,
|
||||
required this.onTap,
|
||||
this.chainPillLabel,
|
||||
this.chainBadgePath,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final String? chainPillLabel;
|
||||
final String? chainBadgePath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
trailing,
|
||||
const SizedBox(width: 8),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: isSelected ? colors.primary : colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconWithBadge extends StatelessWidget {
|
||||
const _IconWithBadge({required this.currency, required this.badgePath});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String? badgePath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = TokenImageWidget(
|
||||
imageUrl: currency.iconPath ?? '',
|
||||
size: 32,
|
||||
errorWidget: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.length >= 2 ? currency.title.substring(0, 2) : currency.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (badgePath == null) return icon;
|
||||
return SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 2,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: HashImageWidget(
|
||||
imageUrl: badgePath,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 13,
|
||||
height: 13,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerSearchField extends StatelessWidget {
|
||||
const CurrencyPickerSearchField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
border: Border.all(color: colors.surfaceContainer, width: 1),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(child: Icon(Icons.search, size: 20, color: colors.primary)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
// 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)
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool currencyMatchesQuery(CryptoCurrency c, String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final q = query.toLowerCase();
|
||||
if (c.title.toLowerCase().contains(q)) return true;
|
||||
if (c.name.toLowerCase().contains(q)) return true;
|
||||
if (c.fullName?.toLowerCase().contains(q) ?? false) return true;
|
||||
if (c.tag?.toLowerCase().contains(q) ?? false) return true;
|
||||
if (chainNameForCurrency(c).toLowerCase().contains(q)) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/multi_network_currency_picker.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/single_network_currency_picker.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrencyPickerSheet extends StatelessWidget {
|
||||
const CurrencyPickerSheet({super.key, required this.args});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required CurrencyPickerArgs args,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => CurrencyPickerSheet(args: args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).select_asset,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: args.useSingleNetworkLayout
|
||||
? SingleNetworkCurrencyPicker(args: args)
|
||||
: MultiNetworkCurrencyPicker(args: args),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_search_result.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencyPickerSheet extends StatefulWidget {
|
||||
const FiatCurrencyPickerSheet({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
this.cryptoOption,
|
||||
this.onCryptoSelected,
|
||||
});
|
||||
|
||||
final Object? selected;
|
||||
final ValueChanged<FiatCurrency> onSelected;
|
||||
final CryptoCurrency? cryptoOption;
|
||||
final ValueChanged<CryptoCurrency>? onCryptoSelected;
|
||||
|
||||
static const _popularOrder = <FiatCurrency>[
|
||||
FiatCurrency.usd,
|
||||
FiatCurrency.eur,
|
||||
FiatCurrency.gbp,
|
||||
FiatCurrency.jpy,
|
||||
FiatCurrency.cny,
|
||||
];
|
||||
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required Object? selected,
|
||||
required ValueChanged<FiatCurrency> onSelected,
|
||||
CryptoCurrency? cryptoOption,
|
||||
ValueChanged<CryptoCurrency>? onCryptoSelected,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => FiatCurrencyPickerSheet(
|
||||
selected: selected,
|
||||
onSelected: onSelected,
|
||||
cryptoOption: cryptoOption,
|
||||
onCryptoSelected: onCryptoSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<FiatCurrencyPickerSheet> createState() => _FiatCurrencyPickerSheetState();
|
||||
}
|
||||
|
||||
class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late final List<FiatCurrency> _allSorted = [...FiatCurrency.all]..sort(
|
||||
(a, b) => a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase()),
|
||||
);
|
||||
|
||||
bool get _isSearching => _searchController.text.trim().isNotEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _matches(FiatCurrency c, String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final q = query.toLowerCase();
|
||||
return c.fullName.toLowerCase().contains(q) || c.title.toLowerCase().contains(q);
|
||||
}
|
||||
|
||||
void _selectFiatCurrency(FiatCurrency c) {
|
||||
widget.onSelected(c);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
void _selectCryptoCurrency(CryptoCurrency c) {
|
||||
widget.onCryptoSelected?.call(c);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final selected = widget.selected;
|
||||
final selectedFiat = selected is FiatCurrency ? selected : null;
|
||||
final popular = FiatCurrencyPickerSheet._popularOrder
|
||||
.where((c) => c != selectedFiat)
|
||||
.toList(growable: false);
|
||||
final query = _searchController.text.trim();
|
||||
final filteredAll = _allSorted.where((c) => _matches(c, query)).toList(growable: false);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
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(
|
||||
child: _isSearching
|
||||
? FiatCurrencySearchResults(
|
||||
items: filteredAll,
|
||||
onSelected: _selectFiatCurrency,
|
||||
selected: selectedFiat,
|
||||
)
|
||||
: FiatCurrencyPickerBody(
|
||||
selected: selected,
|
||||
cryptoOption: widget.cryptoOption,
|
||||
popular: popular,
|
||||
all: filteredAll,
|
||||
onSelectedFiat: _selectFiatCurrency,
|
||||
onSelectedCrypto: _selectCryptoCurrency,
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FiatCurrencyPickerBody extends StatelessWidget {
|
||||
const FiatCurrencyPickerBody({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.cryptoOption,
|
||||
required this.popular,
|
||||
required this.all,
|
||||
required this.onSelectedFiat,
|
||||
required this.onSelectedCrypto,
|
||||
});
|
||||
|
||||
final Object? selected;
|
||||
final CryptoCurrency? cryptoOption;
|
||||
final List<FiatCurrency> popular;
|
||||
final List<FiatCurrency> all;
|
||||
final void Function(FiatCurrency) onSelectedFiat;
|
||||
final void Function(CryptoCurrency) onSelectedCrypto;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedFiat = selected is FiatCurrency ? selected as FiatCurrency : null;
|
||||
final selectedCrypto = selected is CryptoCurrency ? selected as CryptoCurrency : null;
|
||||
final showCryptoSection = cryptoOption != null && selectedCrypto == null;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (selectedFiat != null || selectedCrypto != null) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_selected),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
if (selectedFiat != null)
|
||||
FiatCurrencyRow(
|
||||
currency: selectedFiat,
|
||||
isSelected: true,
|
||||
onTap: () => onSelectedFiat(selectedFiat),
|
||||
),
|
||||
if (selectedCrypto != null)
|
||||
_CryptoOptionRow(
|
||||
currency: selectedCrypto,
|
||||
isSelected: true,
|
||||
onTap: () => onSelectedCrypto(selectedCrypto),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (showCryptoSection) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_crypto),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
_CryptoOptionRow(
|
||||
currency: cryptoOption!,
|
||||
isSelected: false,
|
||||
onTap: () => onSelectedCrypto(cryptoOption!),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (popular.isNotEmpty) ...[
|
||||
_SectionHeader(label: S.of(context).picker_section_popular),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in popular)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: false,
|
||||
onTap: () => onSelectedFiat(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_SectionHeader(label: S.of(context).picker_section_all_currencies),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in all)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: c == selectedFiat,
|
||||
onTap: () => onSelectedFiat(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CryptoOptionRow extends StatelessWidget {
|
||||
const _CryptoOptionRow({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@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: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName ?? currency.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currency.title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check, size: 20, color: colors.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencyRow extends StatelessWidget {
|
||||
const FiatCurrencyRow({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final FiatCurrency currency;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@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: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: HashImageWidget(
|
||||
imageUrl: 'assets/images/flags/${currency.countryCode}.png',
|
||||
width: 28,
|
||||
height: 20,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: Container(
|
||||
width: 28,
|
||||
height: 20,
|
||||
color: colors.surfaceContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currency.title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check, size: 20, color: colors.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FiatCurrencySearchResults extends StatelessWidget {
|
||||
const FiatCurrencySearchResults({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
required this.selected,
|
||||
});
|
||||
|
||||
final List<FiatCurrency> items;
|
||||
final void Function(FiatCurrency) onSelected;
|
||||
final FiatCurrency? selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final c in items)
|
||||
FiatCurrencyRow(
|
||||
currency: c,
|
||||
isSelected: c == selected,
|
||||
onTap: () => onSelected(c),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/chain_chip_strip.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/picker_recents_loader.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/picker_section_header.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/pill_grid.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/select_network_page.dart';
|
||||
import 'package:hash_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultiNetworkCurrencyPicker extends StatefulWidget {
|
||||
const MultiNetworkCurrencyPicker({super.key, required this.args});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
@override
|
||||
State<MultiNetworkCurrencyPicker> createState() => _MultiNetworkCurrencyPickerState();
|
||||
}
|
||||
|
||||
class _MultiNetworkCurrencyPickerState extends State<MultiNetworkCurrencyPicker> {
|
||||
bool _recentsLoaded = false;
|
||||
WalletType? _selectedNetwork;
|
||||
List<CryptoCurrency> _recents = const [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
bool get _isSearching => _searchController.text.trim().isNotEmpty;
|
||||
|
||||
late final List<WalletType> _networks = _computeNetworks();
|
||||
|
||||
List<WalletType> _computeNetworks() {
|
||||
final counts = <WalletType, int>{};
|
||||
for (final currency in widget.args.items) {
|
||||
final walletType = cryptoCurrencyOrTokenToWalletType(currency);
|
||||
if (walletType == null) continue;
|
||||
counts[walletType] = (counts[walletType] ?? 0) + 1;
|
||||
}
|
||||
return counts.entries.where((e) => e.value >= 2).map((e) => e.key).toList(growable: false);
|
||||
}
|
||||
|
||||
Set<CryptoCurrency> get _natives =>
|
||||
{for (final walletType in availableWalletTypes) walletTypeToCryptoCurrency(walletType)};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
_loadRecents();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadRecents() async {
|
||||
try {
|
||||
final recents = await PickerRecentsLoader.load(
|
||||
source: widget.args.recentsSource,
|
||||
visibleItems: widget.args.items,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_recents = recents;
|
||||
_recentsLoaded = true;
|
||||
});
|
||||
} catch (e, s) {
|
||||
printV('load picker recents failed: $e\n$s');
|
||||
if (!mounted) return;
|
||||
setState(() => _recentsLoaded = true);
|
||||
}
|
||||
}
|
||||
|
||||
bool _matchesNetwork(CryptoCurrency c) {
|
||||
if (_selectedNetwork == null) return true;
|
||||
return cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork;
|
||||
}
|
||||
|
||||
List<CryptoCurrency> get _visibleItems {
|
||||
final query = _searchController.text.trim();
|
||||
return widget.args.items
|
||||
.where(_hasSupportedChain)
|
||||
.where((c) => currencyMatchesQuery(c, query) && _matchesNetwork(c))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _hasSupportedChain(CryptoCurrency c) {
|
||||
if (c.tag == null) return true;
|
||||
return cryptoCurrencyOrTokenToWalletType(c) != null;
|
||||
}
|
||||
|
||||
void _selectCurrency(CryptoCurrency currency) {
|
||||
widget.args.onSelected(currency);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
void _onStablecoinPillTapped(CryptoCurrency tapped) {
|
||||
var variants = widget.args.items
|
||||
.where((c) =>
|
||||
isTrustedStablecoin(c) &&
|
||||
c.title.toUpperCase() == tapped.title.toUpperCase() &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) != null)
|
||||
.toList();
|
||||
|
||||
if (_selectedNetwork != null) {
|
||||
final filtered = variants
|
||||
.where((c) => cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork)
|
||||
.toList(growable: false);
|
||||
if (filtered.isNotEmpty) variants = filtered;
|
||||
}
|
||||
|
||||
if (variants.length <= 1) {
|
||||
_selectCurrency(variants.isNotEmpty ? variants.first : tapped);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder<void>(
|
||||
opaque: true,
|
||||
transitionDuration: const Duration(milliseconds: 250),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (_, __, ___) => SelectNetworkPage(
|
||||
assetTitle: tapped.title,
|
||||
assetFullName: tapped.fullName,
|
||||
assetIconPath: tapped.iconPath,
|
||||
variants: variants,
|
||||
onSelected: widget.args.onSelected,
|
||||
),
|
||||
transitionsBuilder: (_, animation, __, child) => SlideTransition(
|
||||
position: animation.drive(
|
||||
Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero)
|
||||
.chain(CurveTween(curve: Curves.easeInOut)),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (_networks.length > 1)
|
||||
ChainChipStrip(
|
||||
walletTypes: _networks,
|
||||
selected: _selectedNetwork,
|
||||
onSelected: (network) => setState(() => _selectedNetwork = network),
|
||||
),
|
||||
Expanded(
|
||||
child: _MultiNetworkPickerBody(
|
||||
items: _visibleItems,
|
||||
isSearching: _isSearching,
|
||||
recents: _recents,
|
||||
recentsLoaded: _recentsLoaded,
|
||||
natives: _natives,
|
||||
selected: widget.args.selected,
|
||||
symbolResolver: widget.args.symbolResolver,
|
||||
showStablesHeader: widget.args.showStablesHeader,
|
||||
onSelect: _selectCurrency,
|
||||
onStablecoinTap: _onStablecoinPillTapped,
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MultiNetworkPickerBody extends StatefulWidget {
|
||||
const _MultiNetworkPickerBody({
|
||||
required this.items,
|
||||
required this.isSearching,
|
||||
required this.recents,
|
||||
required this.recentsLoaded,
|
||||
required this.natives,
|
||||
required this.selected,
|
||||
required this.symbolResolver,
|
||||
required this.onSelect,
|
||||
required this.onStablecoinTap,
|
||||
required this.showStablesHeader,
|
||||
});
|
||||
|
||||
final bool isSearching;
|
||||
final bool recentsLoaded;
|
||||
final bool showStablesHeader;
|
||||
final CryptoCurrency? selected;
|
||||
final List<CryptoCurrency> items;
|
||||
final List<CryptoCurrency> recents;
|
||||
final Set<CryptoCurrency> natives;
|
||||
final void Function(CryptoCurrency) onSelect;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final void Function(CryptoCurrency) onStablecoinTap;
|
||||
|
||||
@override
|
||||
State<_MultiNetworkPickerBody> createState() => _MultiNetworkPickerBodyState();
|
||||
}
|
||||
|
||||
class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> {
|
||||
static const int _previewCount = 3;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _moreCryptosSectionExpanded = false;
|
||||
bool _xstocksSectionExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final selected = widget.selected;
|
||||
if (selected == null) return;
|
||||
|
||||
final moreCryptos = _computeMoreCryptosSection(widget.items);
|
||||
final xstocks = _computeXstocksSection(widget.items);
|
||||
|
||||
if (moreCryptos.indexOf(selected) >= _previewCount) {
|
||||
_moreCryptosSectionExpanded = true;
|
||||
}
|
||||
|
||||
if (xstocks.indexOf(selected) >= _previewCount) {
|
||||
_xstocksSectionExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<CryptoCurrency> _computeMoreCryptosSection(List<CryptoCurrency> from) {
|
||||
final list = from
|
||||
.where((c) =>
|
||||
!widget.natives.contains(c) &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) == null &&
|
||||
c.tag == null &&
|
||||
!c.groups.contains(CurrencyGroups.stablecoin) &&
|
||||
!c.groups.contains(CurrencyGroups.tokenizedStock))
|
||||
.toList();
|
||||
|
||||
list.sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo(
|
||||
(b.fullName ?? b.title).toLowerCase(),
|
||||
));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
List<CryptoCurrency> _computeXstocksSection(List<CryptoCurrency> from) {
|
||||
return from
|
||||
.where((c) => c.groups.contains(CurrencyGroups.tokenizedStock))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = widget.items;
|
||||
final isSearching = widget.isSearching;
|
||||
final recents = widget.recents;
|
||||
final recentsLoaded = widget.recentsLoaded;
|
||||
final natives = widget.natives;
|
||||
final selected = widget.selected;
|
||||
final symbolResolver = widget.symbolResolver;
|
||||
final onSelect = widget.onSelect;
|
||||
final onStablecoinTap = widget.onStablecoinTap;
|
||||
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isSearching) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
primary: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in items)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: selected != null && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final visibleRecents = recents.where(items.contains).toList(growable: false);
|
||||
|
||||
final isSingleNetwork = items.map(cryptoCurrencyOrTokenToWalletType).toSet().length == 1;
|
||||
final seenStablecoinTitles = <String>{};
|
||||
final stablecoins = items
|
||||
.where((c) =>
|
||||
isTrustedStablecoin(c) &&
|
||||
(isSingleNetwork || hasBuiltInStablecoinSymbol(c)) &&
|
||||
seenStablecoinTitles.add(c.title.toUpperCase()))
|
||||
.toList(growable: false);
|
||||
|
||||
final cryptocurrencies = items.where(natives.contains).toList(growable: false);
|
||||
final moreCryptos = _computeMoreCryptosSection(items);
|
||||
final xstocks = _computeXstocksSection(items);
|
||||
final allAssets = [...items]..sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo(
|
||||
(b.fullName ?? b.title).toLowerCase(),
|
||||
));
|
||||
|
||||
final section = _selectedSection(
|
||||
recents: visibleRecents,
|
||||
stablecoins: stablecoins,
|
||||
cryptocurrencies: cryptocurrencies,
|
||||
moreCryptos: moreCryptos,
|
||||
xstocks: xstocks,
|
||||
);
|
||||
|
||||
final moreCryptosVisible = _moreCryptosSectionExpanded
|
||||
? moreCryptos
|
||||
: moreCryptos.take(_previewCount).toList(growable: false);
|
||||
final xstocksVisible =
|
||||
_xstocksSectionExpanded ? xstocks : xstocks.take(_previewCount).toList(growable: false);
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
primary: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (recentsLoaded && visibleRecents.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_recents,
|
||||
child: _RecentsRow(
|
||||
items: visibleRecents,
|
||||
selected: section == _SelSection.recents ? selected : null,
|
||||
symbolResolver: symbolResolver,
|
||||
onTap: onSelect,
|
||||
),
|
||||
),
|
||||
if (stablecoins.isNotEmpty && widget.showStablesHeader)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_stablecoins,
|
||||
child: PillGrid(
|
||||
items: stablecoins,
|
||||
selected: section == _SelSection.stablecoins ? selected : null,
|
||||
onTap: onStablecoinTap,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
),
|
||||
if (cryptocurrencies.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_cryptocurrencies,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in cryptocurrencies)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.cryptocurrencies && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (moreCryptos.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_more_cryptocurrencies,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in moreCryptosVisible)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.moreCryptocurrencies && selected == item,
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
if (moreCryptos.length > _previewCount)
|
||||
_SeeAllRow(
|
||||
expanded: _moreCryptosSectionExpanded,
|
||||
onTap: () => setState(
|
||||
() => _moreCryptosSectionExpanded = !_moreCryptosSectionExpanded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (xstocks.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_tokenized_stocks,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in xstocksVisible)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.xstocks && selected == item,
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
),
|
||||
if (xstocks.length > _previewCount)
|
||||
_SeeAllRow(
|
||||
expanded: _xstocksSectionExpanded,
|
||||
onTap: () =>
|
||||
setState(() => _xstocksSectionExpanded = !_xstocksSectionExpanded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (allAssets.isNotEmpty)
|
||||
_PickerSection(
|
||||
title: S.of(context).picker_section_all_assets,
|
||||
child: CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final item in allAssets)
|
||||
CurrencyPickerRow(
|
||||
currency: item,
|
||||
isSelected: section == _SelSection.allAssets && selected == item,
|
||||
chainPillLabel: _chainPillLabelFor(item),
|
||||
chainBadgePath: _chainBadgePathFor(item),
|
||||
trailing: _SymbolTrailing(
|
||||
currency: item,
|
||||
symbolResolver: symbolResolver,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _chainPillLabelFor(CryptoCurrency c) {
|
||||
if (c == CryptoCurrency.btcln) return _shortChainLabel(c);
|
||||
if (_isL2NativeEth(c)) return _shortChainLabel(c);
|
||||
if (widget.natives.contains(c)) return null;
|
||||
if (cryptoCurrencyOrTokenToWalletType(c) == null) return null;
|
||||
return _shortChainLabel(c);
|
||||
}
|
||||
|
||||
String _shortChainLabel(CryptoCurrency c) {
|
||||
if (cryptoCurrencyOrTokenToWalletType(c) == WalletType.bsc) return 'BSC';
|
||||
return chainNameForCurrency(c);
|
||||
}
|
||||
|
||||
String? _chainBadgePathFor(CryptoCurrency c) {
|
||||
if (_isL2NativeEth(c)) return c.chainIconPath;
|
||||
if (widget.natives.contains(c)) return null;
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
if (wt == null) return null;
|
||||
return c.chainIconPath ?? walletTypeToCryptoCurrency(wt).chainIconPath;
|
||||
}
|
||||
|
||||
bool _isL2NativeEth(CryptoCurrency c) =>
|
||||
c == CryptoCurrency.arbEth || c == CryptoCurrency.baseEth || c == CryptoCurrency.robEth;
|
||||
|
||||
_SelSection? _selectedSection({
|
||||
required List<CryptoCurrency> recents,
|
||||
required List<CryptoCurrency> stablecoins,
|
||||
required List<CryptoCurrency> cryptocurrencies,
|
||||
required List<CryptoCurrency> moreCryptos,
|
||||
required List<CryptoCurrency> xstocks,
|
||||
}) {
|
||||
final s = widget.selected;
|
||||
if (s == null) return null;
|
||||
if (recents.contains(s)) return _SelSection.recents;
|
||||
if (stablecoins.any((c) => c.title.toUpperCase() == s.title.toUpperCase())) {
|
||||
return _SelSection.stablecoins;
|
||||
}
|
||||
if (cryptocurrencies.contains(s)) return _SelSection.cryptocurrencies;
|
||||
if (moreCryptos.contains(s)) return _SelSection.moreCryptocurrencies;
|
||||
if (xstocks.contains(s)) return _SelSection.xstocks;
|
||||
return _SelSection.allAssets;
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerSection extends StatelessWidget {
|
||||
const _PickerSection({required this.title, required this.child});
|
||||
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PickerSectionHeader(title: title),
|
||||
child,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SymbolTrailing extends StatelessWidget {
|
||||
const _SymbolTrailing({
|
||||
required this.currency,
|
||||
required this.symbolResolver,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
symbolResolver(currency),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeeAllRow extends StatelessWidget {
|
||||
const _SeeAllRow({
|
||||
required this.expanded,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool expanded;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final label = expanded ? S.of(context).picker_show_less : S.of(context).picker_see_all;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentsRow extends StatelessWidget {
|
||||
const _RecentsRow({
|
||||
required this.items,
|
||||
required this.selected,
|
||||
required this.symbolResolver,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final List<CryptoCurrency> items;
|
||||
final CryptoCurrency? selected;
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final void Function(CryptoCurrency) onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final item = items[i];
|
||||
return _RecentPill(
|
||||
currency: item,
|
||||
label: symbolResolver(item),
|
||||
isSelected: selected != null && selected == item,
|
||||
onTap: () => onTap(item),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentPill extends StatelessWidget {
|
||||
const _RecentPill({
|
||||
required this.currency,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
border: isSelected ? Border.all(color: colors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl: currency.iconPath ?? '',
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _SelSection {
|
||||
recents,
|
||||
stablecoins,
|
||||
cryptocurrencies,
|
||||
moreCryptocurrencies,
|
||||
xstocks,
|
||||
allAssets,
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:hash_wallet/order/order.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
class PickerRecentsLoader {
|
||||
const PickerRecentsLoader._();
|
||||
|
||||
static const int defaultLimit = 6;
|
||||
|
||||
static final Set<TradeState> _executedTradeStates = {
|
||||
TradeState.complete,
|
||||
TradeState.completed,
|
||||
TradeState.finished,
|
||||
TradeState.success,
|
||||
TradeState.settled,
|
||||
TradeState.traded,
|
||||
};
|
||||
|
||||
static Future<List<CryptoCurrency>> load({
|
||||
required RecentsSource source,
|
||||
required List<CryptoCurrency> visibleItems,
|
||||
int limit = defaultLimit,
|
||||
}) async {
|
||||
switch (source) {
|
||||
case RecentsSource.none:
|
||||
return const [];
|
||||
case RecentsSource.trades:
|
||||
return _fromTrades(visibleItems, limit);
|
||||
case RecentsSource.orders:
|
||||
return _fromOrders(visibleItems, limit);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> _fromTrades(
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) async {
|
||||
final trades = await Trade.getAll();
|
||||
final ordered = trades.where((t) => _executedTradeStates.contains(t.state)).toList()
|
||||
..sort((a, b) => (b.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.compareTo(a.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0)));
|
||||
|
||||
return _collect(
|
||||
ordered.expand((t) => [t.from, t.to]),
|
||||
visibleItems,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> _fromOrders(
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) async {
|
||||
final orders = Hive.box<Order>(Order.boxName).values.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
return _collect(
|
||||
orders.expand((o) => [_tryResolve(o.from), _tryResolve(o.to)]),
|
||||
visibleItems,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
static List<CryptoCurrency> _collect(
|
||||
Iterable<CryptoCurrency?> candidates,
|
||||
List<CryptoCurrency> visibleItems,
|
||||
int limit,
|
||||
) {
|
||||
final visibleSet = visibleItems.toSet();
|
||||
final seen = <CryptoCurrency>{};
|
||||
final collected = <CryptoCurrency>[];
|
||||
|
||||
for (final candidate in candidates) {
|
||||
if (candidate == null) continue;
|
||||
if (!visibleSet.contains(candidate)) continue;
|
||||
if (!seen.add(candidate)) continue;
|
||||
collected.add(candidate);
|
||||
if (collected.length >= limit) break;
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
static CryptoCurrency? _tryResolve(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
try {
|
||||
return CryptoCurrency.fromString(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PickerSectionHeader extends StatelessWidget {
|
||||
const PickerSectionHeader({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PillGrid extends StatelessWidget {
|
||||
const PillGrid({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.symbolResolver,
|
||||
});
|
||||
|
||||
final List<CryptoCurrency> items;
|
||||
final CryptoCurrency? selected;
|
||||
final ValueChanged<CryptoCurrency> onTap;
|
||||
final String Function(CryptoCurrency c) symbolResolver;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const gap = 8.0;
|
||||
final cardWidth = (constraints.maxWidth - gap) / 2;
|
||||
return Wrap(
|
||||
spacing: gap,
|
||||
runSpacing: gap,
|
||||
children: [
|
||||
for (final item in items)
|
||||
SizedBox(
|
||||
width: cardWidth,
|
||||
child: _PillCard(
|
||||
currency: item,
|
||||
isSelected: selected != null && selected == item,
|
||||
label: symbolResolver(item),
|
||||
onTap: () => onTap(item),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PillCard extends StatelessWidget {
|
||||
const _PillCard({
|
||||
required this.currency,
|
||||
required this.isSelected,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isSelected;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
border: isSelected ? Border.all(color: colors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl: currency.iconPath ?? '',
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SelectNetworkPage extends StatelessWidget {
|
||||
const SelectNetworkPage({
|
||||
super.key,
|
||||
required this.assetTitle,
|
||||
required this.assetFullName,
|
||||
required this.assetIconPath,
|
||||
required this.variants,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final String assetTitle;
|
||||
final String? assetFullName;
|
||||
final String? assetIconPath;
|
||||
final List<CryptoCurrency> variants;
|
||||
final ValueChanged<CryptoCurrency> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
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),
|
||||
// Illustration of the asset named by the title below it.
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: assetIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
assetTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_network_subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
children: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final variant in variants)
|
||||
_NetworkRow(
|
||||
variant: variant,
|
||||
onTap: () {
|
||||
final nav = Navigator.of(context);
|
||||
onSelected(variant);
|
||||
if (nav.canPop()) nav.pop();
|
||||
if (nav.canPop()) nav.pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkRow extends StatelessWidget {
|
||||
const _NetworkRow({required this.variant, required this.onTap});
|
||||
|
||||
final CryptoCurrency variant;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(variant);
|
||||
final networkIconPath =
|
||||
wt != null ? getCryptoCurrencyIconForWalletListItem(wt) : variant.iconPath;
|
||||
final networkName = chainNameForCurrency(variant);
|
||||
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: HashImageWidget(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/picker_section_header.dart";
|
||||
import "package:hash_wallet/reactions/wallet_utils.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SingleNetworkCurrencyPicker extends StatefulWidget {
|
||||
const SingleNetworkCurrencyPicker({
|
||||
required this.args,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
@override
|
||||
State<SingleNetworkCurrencyPicker> createState() => _SingleNetworkCurrencyPickerState();
|
||||
}
|
||||
|
||||
class _SingleNetworkCurrencyPickerState extends State<SingleNetworkCurrencyPicker> {
|
||||
CurrencyPickerArgs get _args => widget.args;
|
||||
WalletType get _network => _args.filterByNetwork!;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _selectCurrency(CryptoCurrency currency) {
|
||||
_args.onSelected(currency);
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
CurrencyPickerBalance? _balanceFor(CryptoCurrency c) => balanceForAsset(_args.balanceByAsset, c);
|
||||
|
||||
double _fiatValueFor(CryptoCurrency c) => _balanceFor(c)?.fiatValue ?? 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final native = walletTypeToCryptoCurrency(_network);
|
||||
final query = _searchController.text.trim();
|
||||
final tokens = _args.items
|
||||
.where(
|
||||
(c) =>
|
||||
c != native &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) == _network &&
|
||||
currencyMatchesQuery(c, query),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final insertOrder = {for (var i = 0; i < tokens.length; i++) tokens[i]: i};
|
||||
tokens.sort((a, b) {
|
||||
final av = _fiatValueFor(a);
|
||||
final bv = _fiatValueFor(b);
|
||||
if (bv != av) {
|
||||
return bv.compareTo(av);
|
||||
}
|
||||
return (insertOrder[a] ?? 0).compareTo(insertOrder[b] ?? 0);
|
||||
});
|
||||
final nativeMatches = currencyMatchesQuery(native, query);
|
||||
|
||||
final showSectionHeaders = _network != WalletType.bitcoin;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: (nativeMatches || tokens.isNotEmpty)
|
||||
? ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: showSectionHeaders
|
||||
? [
|
||||
if (nativeMatches) ...[
|
||||
PickerSectionHeader(title: S.of(context).picker_section_gas_token),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(native)),
|
||||
onTap: () => _selectCurrency(native),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (tokens.isNotEmpty) ...[
|
||||
PickerSectionHeader(
|
||||
title: S
|
||||
.of(context)
|
||||
.picker_section_tokens_standard(tokenStandardFor(_network)),
|
||||
),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(t)),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
]
|
||||
: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
if (nativeMatches)
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(native)),
|
||||
onTap: () => _selectCurrency(native),
|
||||
),
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(t)),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
S.of(context).picker_no_matches,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CurrencyPickerSearchField(
|
||||
controller: _searchController,
|
||||
hintText: S.of(context).search,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BalanceTrailing extends StatelessWidget {
|
||||
const _BalanceTrailing({required this.balance});
|
||||
|
||||
final CurrencyPickerBalance? balance;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final amount = balance?.amount ?? "—";
|
||||
final fiat = balance?.fiat;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (fiat != null && fiat.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
fiat,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class DigitInputController {
|
||||
String _text = "";
|
||||
|
||||
String get text => _text;
|
||||
|
||||
set text(String newText) {
|
||||
_text = newText;
|
||||
for (final listener in _listeners) {
|
||||
listener.call();
|
||||
}
|
||||
}
|
||||
|
||||
final List<void Function()> _listeners = [];
|
||||
|
||||
void addListener(void Function() listener) => _listeners.add(listener);
|
||||
}
|
||||
|
||||
class DigitInputPill extends StatelessWidget {
|
||||
const DigitInputPill({required this.highlighted, super.key, this.digit});
|
||||
|
||||
final String? digit;
|
||||
final bool highlighted;
|
||||
|
||||
@override
|
||||
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({
|
||||
required this.controller,
|
||||
required this.desiredLength,
|
||||
this.breakAt = 3,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DigitInputController controller;
|
||||
final int desiredLength;
|
||||
final int breakAt;
|
||||
|
||||
@override
|
||||
State<DigitInput> createState() => _DigitInputState();
|
||||
}
|
||||
|
||||
class _DigitInputState extends State<DigitInput> implements TextInputClient {
|
||||
TextInputConnection? _textInputConnection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_openVirtualKeyboard();
|
||||
}
|
||||
|
||||
void _openVirtualKeyboard() {
|
||||
if (_textInputConnection == null || !_textInputConnection!.attached) {
|
||||
_textInputConnection = TextInput.attach(
|
||||
this,
|
||||
TextInputConfiguration(
|
||||
inputType: TextInputType.number,
|
||||
inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_textInputConnection!.setEditingState(
|
||||
TextEditingValue(
|
||||
text: widget.controller.text,
|
||||
selection: TextSelection.collapsed(offset: widget.controller.text.length),
|
||||
),
|
||||
);
|
||||
_textInputConnection!.show();
|
||||
}
|
||||
|
||||
void _closeVirtualKeyboard() => _textInputConnection?.close();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_closeVirtualKeyboard();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
|
||||
|
||||
if (numberRegExp.hasMatch(value.text)) {
|
||||
setState(() => widget.controller.text = value.text);
|
||||
} else {
|
||||
// flutter keeps the text editing state regardless of whether you actually used it or not.
|
||||
// so, if validation fails, we gotta set it back to what it was explicitly
|
||||
_textInputConnection?.setEditingState(
|
||||
TextEditingValue(
|
||||
text: widget.controller.text,
|
||||
selection: TextSelection.collapsed(offset: widget.controller.text.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performAction(TextInputAction action) {}
|
||||
|
||||
@override
|
||||
TextEditingValue? get currentTextEditingValue => TextEditingValue(text: widget.controller.text);
|
||||
|
||||
@override
|
||||
void connectionClosed() => _textInputConnection = null;
|
||||
|
||||
@override
|
||||
AutofillScope? get currentAutofillScope => null;
|
||||
|
||||
@override
|
||||
void performPrivateCommand(String action, Map<String, dynamic> data) {}
|
||||
|
||||
@override
|
||||
void showAutocorrectionPromptRect(int start, int end) {}
|
||||
|
||||
@override
|
||||
void updateFloatingCursor(RawFloatingCursorPoint point) {}
|
||||
|
||||
@override
|
||||
void insertTextPlaceholder(Size size) {}
|
||||
|
||||
@override
|
||||
void removeTextPlaceholder() {}
|
||||
|
||||
@override
|
||||
void showToolbar() {}
|
||||
|
||||
@override
|
||||
void performSelector(String selectorName) {}
|
||||
|
||||
@override
|
||||
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
|
||||
|
||||
@override
|
||||
void insertContent(KeyboardInsertedContent content) {}
|
||||
}
|
||||
@@ -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: HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import "package:hash_wallet/new-ui/widgets/money/currency_symbol_text.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class FloatingAmountInput extends StatefulWidget {
|
||||
const FloatingAmountInput({
|
||||
required this.currency,
|
||||
required this.controller,
|
||||
super.key,
|
||||
this.focusNode,
|
||||
this.inputFormatters,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final Currency currency;
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final Function(String)? onChanged;
|
||||
final FormFieldValidator<String>? validator;
|
||||
|
||||
@override
|
||||
State<FloatingAmountInput> createState() => _FloatingAmountInputState();
|
||||
}
|
||||
|
||||
class _FloatingAmountInputState extends State<FloatingAmountInput> {
|
||||
bool _amountFocused = false;
|
||||
late FocusNode focusNode = widget.focusNode ?? FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode.addListener(() => setState(() => _amountFocused = focusNode.hasFocus));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (focusNode != widget.focusNode) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
IntrinsicWidth(
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
focusNode: focusNode,
|
||||
maxLines: 1,
|
||||
onChanged: widget.onChanged,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: widget.validator,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r"^\d*[.,]?\d*$"),
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
hintText: _amountFocused || widget.controller.text.isNotEmpty ? null : "0.00",
|
||||
hintStyle: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 45,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CurrencySymbolText(
|
||||
widget.currency,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 45,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HardwareWalletProceedOnDeviceMessage extends StatelessWidget {
|
||||
const HardwareWalletProceedOnDeviceMessage({
|
||||
super.key,
|
||||
required this.hardwareWalletType,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
/// Optional hint about what the device is going to ask
|
||||
final String? note;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(spacing: 8, children: [
|
||||
if (hardwareWalletIcon != null)
|
||||
HashImageWidget(
|
||||
imageUrl: hardwareWalletIcon!,
|
||||
width: 36,
|
||||
height: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).proceed_on_device,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (note != null)
|
||||
Text(
|
||||
note!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (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:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
|
||||
import "package:hash_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/bordered_svg.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/digit_input.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/send_page/directional_switcher.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:hash_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class HardwareWalletProceedOnDeviceSheet extends StatefulWidget {
|
||||
const HardwareWalletProceedOnDeviceSheet({
|
||||
required this.hardwareWalletType,
|
||||
required this.trezorConnectVM,
|
||||
required this.onRetry,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
final TrezorConnectViewModelBase trezorConnectVM;
|
||||
final void Function() onRetry;
|
||||
|
||||
@override
|
||||
State<HardwareWalletProceedOnDeviceSheet> createState() =>
|
||||
_HardwareWalletProceedOnDeviceSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProceedOnDeviceSheet> {
|
||||
TrezorParingState _paringState = TrezorParingState.initial;
|
||||
|
||||
final DigitInputController _controller = DigitInputController();
|
||||
|
||||
static const pinOpenDuration = Duration(milliseconds: 300);
|
||||
|
||||
// you should probably check if there can be other pin lengths
|
||||
static const pinLength = 6;
|
||||
|
||||
ReactionDisposer? paringStateReaction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() => setState(() {}));
|
||||
|
||||
_paringState = widget.trezorConnectVM.paringState;
|
||||
paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) {
|
||||
if (paringState is SuccessTrezorParingState) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState(() => _paringState = paringState);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
paringStateReaction?.reaction.dispose();
|
||||
}
|
||||
|
||||
bool get _isAwaitingPin => _paringState == TrezorParingState.enterPin;
|
||||
|
||||
@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: "",
|
||||
leadingWidget: AnimatedSwitcher(
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
duration: pinOpenDuration,
|
||||
child: Text(
|
||||
key: ValueKey(pageTitle),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
pageTitle,
|
||||
),
|
||||
),
|
||||
trailingIcon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).are_you_sure_exit,
|
||||
alertContent: S.of(context).hww_exit_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: DirectionalAnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(),
|
||||
AnimatedOpacity(
|
||||
curve: Curves.easeOutQuad,
|
||||
opacity: hasFullPin ? 1 : 0,
|
||||
duration: pinOpenDuration,
|
||||
child: ModernButton(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
iconColor: Theme.of(context).colorScheme.onPrimary,
|
||||
size: 36,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
semanticLabel: S.of(context).confirm,
|
||||
onPressed: () =>
|
||||
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get content => switch (_paringState) {
|
||||
InitialTrezorParingState() || EnterPinTrezorParingState() => PinEntryWidget(
|
||||
title: S.of(context).proceed_on_device,
|
||||
description: S.of(context).proceed_on_device_description,
|
||||
pinOpenDuration: pinOpenDuration,
|
||||
pinLength: pinLength,
|
||||
isAwaitingPin: _isAwaitingPin,
|
||||
controller: _controller,
|
||||
iconPath: hardwareWalletIcon ?? "",
|
||||
key: const ValueKey(0),
|
||||
),
|
||||
VerifyingPinTrezorParingState() => const VerifyingProgressIndicator(
|
||||
key: ValueKey(1),
|
||||
),
|
||||
FailTrezorParingState(:final message) => ConnectionErrorWidget(
|
||||
error: message,
|
||||
onRetryPressed: retry,
|
||||
key: const ValueKey(2),
|
||||
),
|
||||
AwaitingSettingsTrezorParingState(:final isAutoPairingAvailable) => WalletOptionsScreen(
|
||||
trezorConnectVM: widget.trezorConnectVM,
|
||||
iconPath: hardwareWalletIcon ?? "",
|
||||
isAutoPairingAvailable: isAutoPairingAvailable,
|
||||
key: const ValueKey(3),
|
||||
),
|
||||
AwaitingPassphraseTrezorParingState() => PinEntryWidget(
|
||||
title: S.of(context).proceed_on_device,
|
||||
description: S.of(context).proceed_on_device_description,
|
||||
pinOpenDuration: pinOpenDuration,
|
||||
pinLength: pinLength,
|
||||
isAwaitingPin: false,
|
||||
controller: _controller,
|
||||
iconPath: hardwareWalletIcon ?? "",
|
||||
key: const ValueKey(4),
|
||||
),
|
||||
_ => const SizedBox.shrink(
|
||||
key: ValueKey(5),
|
||||
),
|
||||
};
|
||||
|
||||
void retry() {
|
||||
_controller.text = "";
|
||||
widget.onRetry();
|
||||
}
|
||||
|
||||
String get pageTitle {
|
||||
if (_paringState is InitialTrezorParingState) {
|
||||
return S.of(context).device_confirmation;
|
||||
}
|
||||
|
||||
if (_isAwaitingPin) {
|
||||
return S.of(context).pairing_code;
|
||||
}
|
||||
|
||||
if (_paringState is AwaitingSettingsTrezorParingState) {
|
||||
return S.of(context).other_device_settings;
|
||||
}
|
||||
|
||||
if (_paringState is AwaitingPassphraseTrezorParingState) {
|
||||
return S.of(context).passphrase_entry;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin;
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (widget.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:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionErrorWidget extends StatelessWidget {
|
||||
const ConnectionErrorWidget({required this.error, required this.onRetryPressed, super.key});
|
||||
|
||||
final VoidCallback onRetryPressed;
|
||||
final String error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
key: key,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(
|
||||
S.of(context).pairing_error,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
error,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Spacer(),
|
||||
NewPrimaryButton(
|
||||
onPressed: onRetryPressed,
|
||||
text: S.of(context).try_again,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class VerifyingProgressIndicator extends StatelessWidget {
|
||||
const VerifyingProgressIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
key: key,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CupertinoActivityIndicator(radius: 36),
|
||||
const SizedBox(),
|
||||
Text(
|
||||
"${S.of(context).verifying_code}...",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
Text(
|
||||
S.of(context).this_can_take_few_seconds,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PinEntryWidget extends StatelessWidget {
|
||||
const PinEntryWidget({
|
||||
required this.pinOpenDuration,
|
||||
required this.pinLength,
|
||||
required this.controller,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.iconPath,
|
||||
required this.isAwaitingPin,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DigitInputController controller;
|
||||
final String iconPath;
|
||||
final bool isAwaitingPin;
|
||||
final int pinLength;
|
||||
final Duration pinOpenDuration;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
key: key,
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: pinOpenDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: isAwaitingPin ? 40 : 100,
|
||||
height: isAwaitingPin ? 40 : 100,
|
||||
child: HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: pinOpenDuration,
|
||||
switchInCurve: Curves.easeInCubic,
|
||||
switchOutCurve: Curves.easeOutCubic,
|
||||
child: isAwaitingPin
|
||||
? Column(
|
||||
spacing: 24,
|
||||
key: const ValueKey(1),
|
||||
children: [
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
"${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}",
|
||||
),
|
||||
DigitInput(
|
||||
controller: controller,
|
||||
desiredLength: pinLength,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
key: const ValueKey(0),
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class WalletOptionsScreen extends StatefulWidget {
|
||||
const WalletOptionsScreen({
|
||||
required this.trezorConnectVM,
|
||||
required this.isAutoPairingAvailable,
|
||||
required this.iconPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final TrezorConnectViewModelBase trezorConnectVM;
|
||||
final bool isAutoPairingAvailable;
|
||||
|
||||
@override
|
||||
State<WalletOptionsScreen> createState() => _WalletOptionsScreenState();
|
||||
}
|
||||
|
||||
class _WalletOptionsScreenState extends State<WalletOptionsScreen> {
|
||||
bool _autoConnect = true;
|
||||
bool _usePassphrase = false;
|
||||
bool _setPassphraseOnDevice = false;
|
||||
final _passphraseController = TextEditingController();
|
||||
|
||||
bool get anythingSelected => _autoConnect || _usePassphrase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 32,
|
||||
children: [
|
||||
Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 116,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: HashImageWidget(
|
||||
imageUrl: widget.iconPath,
|
||||
width: 100,
|
||||
height: 100,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: BorderedSvgIcon(
|
||||
iconPath: "assets/new-ui/cog.svg",
|
||||
iconSize: 36,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
Text(
|
||||
S.of(context).almost_ready,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
S.of(context).hww_options_desc,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
NewListSections(
|
||||
sections: {
|
||||
if (widget.isAutoPairingAvailable)
|
||||
"auto": [
|
||||
ListItemToggle(
|
||||
value: _autoConnect,
|
||||
onChanged: (val) => setState(() => _autoConnect = val),
|
||||
keyValue: "autoconnect",
|
||||
label: S.of(context).auto_connect,
|
||||
),
|
||||
],
|
||||
"pass": [
|
||||
ListItemToggle(
|
||||
value: _usePassphrase,
|
||||
onChanged: (val) => setState(() => _usePassphrase = val),
|
||||
keyValue: "passphrase",
|
||||
label: S.of(context).passphrase,
|
||||
subtitle: S.of(context).wallet_has_passphrase,
|
||||
),
|
||||
if (_usePassphrase) ...[
|
||||
ListItemToggle(
|
||||
value: _setPassphraseOnDevice,
|
||||
onChanged: (val) => setState(() => _setPassphraseOnDevice = val),
|
||||
keyValue: "passphrase on device",
|
||||
label: S.of(context).enter_passphrase_on_device,
|
||||
),
|
||||
if (!_setPassphraseOnDevice)
|
||||
ListItemRegularRow(
|
||||
keyValue: "passphrase on app",
|
||||
label: S.of(context).passphrase_raw,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => MoneroHardwareWalletPassphraseInputModal(
|
||||
controller: _passphraseController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
NewPrimaryButton(
|
||||
onPressed: () => widget.trezorConnectVM.setDeviceSettings(
|
||||
TrezorDeviceSettings(
|
||||
enableAutoParing: _autoConnect,
|
||||
passphraseOnDevice: _usePassphrase && _setPassphraseOnDevice,
|
||||
passphrase: _usePassphrase && !_setPassphraseOnDevice ? _passphraseController.text : null,
|
||||
),
|
||||
),
|
||||
text: anythingSelected ? S.of(context).continue_text : S.of(context).skip,
|
||||
color: anythingSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
textColor: anythingSelected
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/send_page/directional_switcher.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/connect_device/connect_device_page.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:hash_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/primary_button.dart";
|
||||
import "package:hash_wallet/store/app_store.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_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: [
|
||||
HashImageWidget(
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HardwareWalletTrezorParingSheet extends StatefulWidget {
|
||||
const HardwareWalletTrezorParingSheet({super.key});
|
||||
|
||||
@override
|
||||
State<HardwareWalletTrezorParingSheet> createState() => _HardwareWalletTrezorParingSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorParingSheet> {
|
||||
final TextEditingController _paringCodeController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "Paring code",
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {},
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 75,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
textAlign: TextAlign.left,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
controller: _paringCodeController,
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(signed: false, decimal: false),
|
||||
decoration: InputDecoration(
|
||||
hint: Text(
|
||||
"000 000",
|
||||
textAlign: TextAlign.center,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(),
|
||||
NewPrimaryButton(
|
||||
text: S.of(context).continue_text,
|
||||
onPressed: () => Navigator.of(context).pop(_paringCodeController.text),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
@@ -29,7 +30,7 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
|
||||
void didUpdateWidget(covariant LineTabSwitcher oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.tabs.length != widget.tabs.length) {
|
||||
if (!listEquals(oldWidget.tabs, widget.tabs)) {
|
||||
textWidgetKeys = List.generate(widget.tabs.length, (_) => GlobalKey());
|
||||
textWidgetSizes = [];
|
||||
totalWidth = 0;
|
||||
@@ -92,58 +93,70 @@ 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(
|
||||
key: ValueKey("line_tab_switcher_${index}_key"),
|
||||
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())),
|
||||
Container(
|
||||
width: totalWidth,
|
||||
height: 2,
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
curve: Curves.easeOut,
|
||||
left: _calcBarLeft(),
|
||||
bottom: 0,
|
||||
duration: Duration(milliseconds: 150),
|
||||
child: AnimatedSize(
|
||||
if (widget.tabs.length > 1)
|
||||
Container(
|
||||
width: totalWidth,
|
||||
height: 2,
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
curve: Curves.easeOut,
|
||||
left: _calcBarLeft(),
|
||||
bottom: 0,
|
||||
duration: Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
height: 2,
|
||||
width: textWidgetSizes.isEmpty ? 0 : textWidgetSizes[widget.selectedTab].width,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
child: AnimatedSize(
|
||||
duration: Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
height: 2,
|
||||
width:
|
||||
textWidgetSizes.isEmpty ? 0 : textWidgetSizes[widget.selectedTab].width,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class LongPressMenuItem {
|
||||
final String label;
|
||||
final String iconPath;
|
||||
final VoidCallback onSelected;
|
||||
final Color? color;
|
||||
|
||||
LongPressMenuItem(
|
||||
{required this.label, required this.iconPath, required this.onSelected, this.color});
|
||||
}
|
||||
|
||||
class LongPressMenu extends StatefulWidget {
|
||||
const LongPressMenu({super.key, required this.items});
|
||||
|
||||
final List<LongPressMenuItem> items;
|
||||
|
||||
@override
|
||||
State<LongPressMenu> createState() => _LongPressMenuState();
|
||||
}
|
||||
|
||||
class _LongPressMenuState extends State<LongPressMenu> {
|
||||
bool _isVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_isVisible = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: _isVisible ? null : 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(60),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.items.map((item) {
|
||||
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: [
|
||||
HashImageWidget(
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class LongPressFooter extends StatelessWidget {
|
||||
const LongPressFooter({required this.text, super.key});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(60),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/info.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import "dart:ui";
|
||||
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class LongPressMenuItem {
|
||||
LongPressMenuItem({
|
||||
required this.label,
|
||||
required this.iconPath,
|
||||
required this.onSelected,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String iconPath;
|
||||
final VoidCallback onSelected;
|
||||
final Color? color;
|
||||
}
|
||||
|
||||
class LongPressMenu extends StatefulWidget {
|
||||
const LongPressMenu({required this.items, super.key});
|
||||
|
||||
final List<LongPressMenuItem> items;
|
||||
|
||||
@override
|
||||
State<LongPressMenu> createState() => _LongPressMenuState();
|
||||
}
|
||||
|
||||
class _LongPressMenuState extends State<LongPressMenu> {
|
||||
bool _isVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_isVisible = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: _isVisible ? null : 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(60),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.items.map((item) {
|
||||
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: item.onSelected,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import "dart:ui";
|
||||
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class LongPressPopupBuilder extends StatelessWidget {
|
||||
const LongPressPopupBuilder({
|
||||
required this.child,
|
||||
required this.popup,
|
||||
super.key,
|
||||
this.spacing = 8,
|
||||
this.showOnTap = false,
|
||||
this.footer,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Widget popup;
|
||||
final Widget? footer;
|
||||
final double spacing;
|
||||
final bool showOnTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: () => _showMenu(context),
|
||||
onTap: showOnTap ? () => _showMenu(context) : null,
|
||||
child: IgnorePointer(ignoring: showOnTap, child: child),
|
||||
);
|
||||
|
||||
void _showMenu(BuildContext context) {
|
||||
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final size = renderBox.size;
|
||||
|
||||
showPopUp(
|
||||
context: context,
|
||||
builder: (context) => _buildPopup(context, offset, size),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final bool isOnRightHalf = offset.dx + (size.width / 2) > screenWidth / 2;
|
||||
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: isOnRightHalf ? null : offset.dx,
|
||||
right: isOnRightHalf ? (screenWidth - (offset.dx + size.width)) : null,
|
||||
top: offset.dy + size.height + spacing,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: popup,
|
||||
),
|
||||
),
|
||||
if (footer != null)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).viewPadding.bottom,
|
||||
child: footer!,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class ModalHeader extends StatelessWidget {
|
||||
const ModalHeader(
|
||||
{super.key, required this.iconPath, required this.message, required this.title});
|
||||
{super.key,
|
||||
required this.iconPath,
|
||||
required this.message,
|
||||
required this.title,
|
||||
this.bottomWidget});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String message;
|
||||
final Widget? bottomWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -16,33 +20,47 @@ class ModalHeader extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
HashImageWidget(imageUrl: iconPath, width: 36, height: 36),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
message,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
HashImageWidget(imageUrl: iconPath, width: 36, height: 36),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
))
|
||||
],
|
||||
),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (bottomWidget != null) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child:
|
||||
Container(height: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
|
||||
child: bottomWidget!)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:hash_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:hash_wallet/entities/balance_display_mode.dart";
|
||||
import "package:hash_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:hash_wallet/src/screens/wallet_connect/utils/string_parsing.dart";
|
||||
import "package:hash_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,20 @@
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/use_base_unit_provider.dart";
|
||||
import "package:hash_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, required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocProvider(
|
||||
create: (_) => MoneySettingsCubit(settingsStore),
|
||||
child: BaseUnitScope(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_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,63 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// Read-only view of the money display settings for the current subtree.
|
||||
///
|
||||
/// ```dart
|
||||
/// final useBaseUnit = BaseUnit.useBaseUnitOf(context, CryptoCurrency.btc);
|
||||
/// ```
|
||||
class BaseUnit extends InheritedModel<Currency> {
|
||||
const BaseUnit({required this.state, required super.child});
|
||||
|
||||
final MoneySettingsState state;
|
||||
|
||||
static bool useBaseUnitOf(BuildContext context, Currency currency) =>
|
||||
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.useBaseUnit(currency);
|
||||
|
||||
static String getSymbolOf(BuildContext context, Currency currency) =>
|
||||
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.getSymbol(currency);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BaseUnit old) =>
|
||||
state.bitcoinAmountDisplayMode != old.state.bitcoinAmountDisplayMode;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(BaseUnit old, Set<Currency> aspects) => aspects.any((c) =>
|
||||
state.useBaseUnit(c) != old.state.useBaseUnit(c) ||
|
||||
state.getSymbol(c) != old.state.getSymbol(c));
|
||||
}
|
||||
|
||||
class BaseUnitScope extends StatefulWidget {
|
||||
const BaseUnitScope({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<BaseUnitScope> createState() => _BaseUnitScopeState();
|
||||
}
|
||||
|
||||
class _BaseUnitScopeState extends State<BaseUnitScope> {
|
||||
late MoneySettingsState _state;
|
||||
late final StreamSubscription<MoneySettingsState> _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cubit = context.read<MoneySettingsCubit>();
|
||||
_state = cubit.state;
|
||||
_sub = cubit.stream.listen((s) => setState(() => _state = s));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BaseUnit(state: _state, child: widget.child);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import "package:hash_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,14 +1,13 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class NewPrimaryButton extends StatelessWidget {
|
||||
const NewPrimaryButton({
|
||||
required this.onPressed,
|
||||
this.image,
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
this.image,
|
||||
this.isLoading = false,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.disabled = false,
|
||||
@@ -18,48 +17,46 @@ class NewPrimaryButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final bool isLoading;
|
||||
final bool disabled;
|
||||
final SvgPicture? image;
|
||||
final Widget? image;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final Color borderColor;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 52.0,
|
||||
child: TextButton(
|
||||
onPressed: disabled ? null : onPressed,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
disabled ? color.withAlpha(128) : color,
|
||||
),
|
||||
shape: WidgetStateProperty.all<RoundedSuperellipseBorder>(
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: 52,
|
||||
child: TextButton(
|
||||
onPressed: disabled ? null : onPressed,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
disabled ? color.withAlpha(128) : color,
|
||||
),
|
||||
shape: WidgetStateProperty.all<RoundedSuperellipseBorder>(
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Row(
|
||||
spacing: 10,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (image != null) image!,
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? CupertinoActivityIndicator()
|
||||
: Row(
|
||||
spacing: 10,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (image != null) image!,
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_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:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_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)
|
||||
HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
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:hash_wallet/src/widgets/section_divider.dart';
|
||||
import 'package:hash_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: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg")),
|
||||
)
|
||||
],
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration:
|
||||
ReceiveAddressTypeSelector.otherOptionsExpandDuration,
|
||||
turns: _otherOptionsExpanded ? 0.0 : 0.5,
|
||||
curve: Curves.easeOut,
|
||||
child: HashImageWidget(
|
||||
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)
|
||||
HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
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)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ class ReceiveAddressWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
key: const ValueKey("receive_page_address_key"),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||
child: Observer(
|
||||
builder: (_) => AddressFormatter.buildSegmentedAddress(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/utils/decimal_input_formatter.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class ReceiveAmountModal extends StatefulWidget {
|
||||
const ReceiveAmountModal(
|
||||
@@ -54,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),
|
||||
@@ -63,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: [
|
||||
Image.asset(
|
||||
widget.walletAddressListViewModel.tokenCurrency
|
||||
?.iconPath ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
.iconPath ??
|
||||
"",
|
||||
width: 32,
|
||||
height: 32),
|
||||
Text((widget.walletAddressListViewModel.tokenCurrency ??
|
||||
widget.walletAddressListViewModel.currencies.first
|
||||
as CryptoCurrency)
|
||||
.title
|
||||
.toUpperCase())
|
||||
],
|
||||
),
|
||||
RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
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: [
|
||||
@@ -130,66 +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.useSatoshi),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -218,35 +260,30 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
|
||||
);
|
||||
}
|
||||
|
||||
void _presentTokenCurrencyPicker(BuildContext context) async {
|
||||
await showPopUp(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: widget.walletAddressListViewModel.tokenCurrencyIndex,
|
||||
items: widget.walletAddressListViewModel.tokenCurrencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: widget.walletAddressListViewModel.setTokenCurrency,
|
||||
),
|
||||
void _presentTokenCurrencyPicker(BuildContext context) {
|
||||
final items =
|
||||
widget.walletAddressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList();
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: items,
|
||||
selected: widget.walletAddressListViewModel.tokenCurrency,
|
||||
onSelected: widget.walletAddressListViewModel.setTokenCurrency,
|
||||
symbolResolver: widget.walletAddressListViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _presentFiatCurrencyPicker(BuildContext context) async {
|
||||
final currs = widget.walletAddressListViewModel.currencies.toList();
|
||||
// the first options should be the crypto and the default fiat currency
|
||||
currs.sort((a, b) {
|
||||
if (a == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return -1;
|
||||
if (b == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
await showPopUp(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: currs.indexOf(widget.walletAddressListViewModel.selectedCurrency),
|
||||
items: currs,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
),
|
||||
void _presentFiatCurrencyPicker(BuildContext context) {
|
||||
final selected = widget.walletAddressListViewModel.selectedCurrency;
|
||||
final cryptoOption =
|
||||
widget.walletAddressListViewModel.currencies.whereType<CryptoCurrency>().firstOrNull;
|
||||
FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: selected,
|
||||
cryptoOption: cryptoOption,
|
||||
onSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
onCryptoSelected: widget.walletAddressListViewModel.selectCurrency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/copy_wrapper.dart";
|
||||
import "package:hash_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 VoidCallback onCopyButtonPressed;
|
||||
final VoidCallback onAmountButtonPressed;
|
||||
final VoidCallback onLabelButtonPressed;
|
||||
final VoidCallback onAccountsButtonPressed;
|
||||
final bool showLabelButton;
|
||||
final bool showAccountsButton;
|
||||
|
||||
const ReceiveBottomButtons({
|
||||
super.key,
|
||||
required this.largeQrMode,
|
||||
required this.onCopyButtonPressed,
|
||||
required this.onAccountsButtonPressed,
|
||||
@@ -23,25 +13,24 @@ class ReceiveBottomButtons extends StatefulWidget {
|
||||
required this.onLabelButtonPressed,
|
||||
required this.showLabelButton,
|
||||
required this.showAccountsButton,
|
||||
required this.copyData,
|
||||
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();
|
||||
}
|
||||
|
||||
class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
|
||||
bool copied = false;
|
||||
|
||||
void handleCopy() async {
|
||||
widget.onCopyButtonPressed();
|
||||
if (await shouldShowCopied()) {
|
||||
setState(() => copied = true);
|
||||
Future.delayed(const Duration(milliseconds: 1200), () {
|
||||
if (mounted) setState(() => copied = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double targetOpacity = widget.largeQrMode ? 0 : 1;
|
||||
@@ -57,49 +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: [
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 200),
|
||||
child: ModernButton.svg(
|
||||
key: ValueKey(copied),
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
svgPath: "assets/new-ui/copy.svg",
|
||||
onPressed: handleCopy,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -107,20 +104,4 @@ class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// android 13 (sdk 33) added a built-in "text was copied to clipboard" ui element
|
||||
// older android and iphone still needs an indicator though
|
||||
Future<bool> shouldShowCopied() async {
|
||||
if (!Platform.isAndroid) return true;
|
||||
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
final sdk = androidInfo.version.sdkInt;
|
||||
|
||||
return sdk < 33;
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'dart:math';
|
||||
|
||||
import 'package:hash_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -15,10 +17,15 @@ class ReceiveInfoBox extends StatelessWidget {
|
||||
required this.onDismissed,
|
||||
this.bottomWidget});
|
||||
|
||||
/// [addressRotates] tells whether the address type currently selected on the
|
||||
/// receive page actually rotates. Most wallet types only ever expose rotating
|
||||
/// address types, so it defaults to true; Zcash also offers static types, for
|
||||
/// which the rotation notice would be wrong.
|
||||
static ReceiveInfoBox? forWalletType(WalletType type,
|
||||
{required VoidCallback onDismissed,
|
||||
required AutoGenerateSubaddressStatus autoGenerateSubaddressStatus,
|
||||
List<CryptoCurrency>? supportedCurrencies}) {
|
||||
List<CryptoCurrency>? supportedCurrencies,
|
||||
bool addressRotates = true}) {
|
||||
switch (type) {
|
||||
case WalletType.nano:
|
||||
return null;
|
||||
@@ -34,11 +41,11 @@ class ReceiveInfoBox extends StatelessWidget {
|
||||
onDismissed: onDismissed,
|
||||
bottomWidget: InfoboxCurrencyRow(
|
||||
currencies: supportedCurrencies ?? [],
|
||||
chainIconPath:
|
||||
"assets/new-ui/chain_badges/${walletTypeToString(type).toLowerCase()}.svg",
|
||||
chainIconPath: walletTypeToCryptoCurrency(type).chainIconPath ?? "",
|
||||
));
|
||||
default:
|
||||
if (autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.disabled) return null;
|
||||
if (!addressRotates) return null;
|
||||
return ReceiveInfoBox(
|
||||
iconPath: "assets/new-ui/info.svg",
|
||||
message: S.current.infobox_auto_address,
|
||||
@@ -67,12 +74,14 @@ class ReceiveInfoBox extends StatelessWidget {
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (iconPath.isNotEmpty)
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
@@ -92,15 +101,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),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]),
|
||||
@@ -132,66 +146,69 @@ class InfoboxCurrencyRow extends StatelessWidget {
|
||||
|
||||
final double stackWidth = iconSize + (overlap * (currenciesLimited.length));
|
||||
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
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: [
|
||||
HashImageWidget(
|
||||
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: HashImageWidget(
|
||||
imageUrl: entry.value.iconPath,
|
||||
width: 24,
|
||||
height: 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: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/label.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)),
|
||||
ExcludeSemantics(
|
||||
child: ClipRect(
|
||||
child: HashImageWidget(
|
||||
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)),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user