From abbabc66531470ca6dcecf5279452ae9ad6e29e0 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:56:43 +0100 Subject: [PATCH] feat(hd): HD withdrawals and portfolio overview --- assets/translations/en.json | 3 +- lib/bloc/coins_bloc/coins_bloc.dart | 42 ++- lib/bloc/coins_bloc/coins_event.dart | 10 + lib/bloc/coins_bloc/coins_state.dart | 33 ++- .../withdraw_form/withdraw_form_bloc.dart | 2 +- .../withdraw_form/withdraw_form_state.dart | 7 +- lib/generated/codegen_loader.g.dart | 1 + lib/shared/widgets/coin_balance.dart | 74 ++++- .../tables/coins_table/coins_table_item.dart | 2 +- .../charts/animated_portfolio_charts.dart | 114 ++++++++ .../coin_details_info/coin_addresses.dart | 15 +- .../transactions/transaction_details.dart | 6 +- .../custom_fee/fill_form_custom_fee.dart | 2 +- .../withdraw_form/withdraw_form.dart | 2 +- .../wallet_page/common/coin_list_item.dart | 2 +- .../common/coin_list_item_desktop.dart | 76 +----- .../wallet_page/common/coins_list_header.dart | 107 +++++--- .../common/expandable_coin_list_item.dart | 253 ++++++++++++++++++ .../wallet_main/active_coins_list.dart | 236 +++++++++++++++- .../wallet_main/all_coins_list.dart | 71 +++-- .../wallet_page/wallet_main/wallet_main.dart | 48 +--- .../wallet_main/wallet_manage_section.dart | 133 +++------ .../wallet_manager_search_field.dart | 75 ++++-- .../wallet_main/wallet_overview.dart | 2 +- .../lib/src/tips/ui_spinner.dart | 3 +- packages/komodo_ui_kit/pubspec.lock | 8 +- pubspec.lock | 16 +- 27 files changed, 997 insertions(+), 346 deletions(-) create mode 100644 lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart create mode 100644 lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 400e62aebe..4cfd98b685 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -3,7 +3,8 @@ "rewardClaiming": "Rewards claim in progress", "noKmdAddress": "No KMD address found", "dex": "DEX", - "asset": "Assets", + "asset": "Asset", + "assets": "Assets", "price": "Price", "volume": "Volume", "history": "History", diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart index ed4b505c51..1293f8c2af 100644 --- a/lib/bloc/coins_bloc/coins_bloc.dart +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -42,6 +42,10 @@ class CoinsBloc extends Bloc { transformer: droppable(), ); on(_onWalletCoinUpdated, transformer: sequential()); + on( + _onCoinsPubkeysRequested, + transformer: concurrent(), + ); } final KomodoDefiSdk _kdfSdk; @@ -70,6 +74,38 @@ class CoinsBloc extends Bloc { await super.close(); } + Future _onCoinsPubkeysRequested( + CoinsPubkeysRequested event, + Emitter emit, + ) async { + try { + // Get current coin + final coin = state.coins[event.coinId]; + if (coin == null) return; + + // Get pubkeys from the SDK through the repo + final asset = _kdfSdk.assets.assetsFromTicker(event.coinId).single; + final pubkeys = await _kdfSdk.pubkeys.getPubkeys(asset); + + // Update state with new pubkeys + emit( + state.copyWith( + pubkeys: { + ...state.pubkeys, + event.coinId: pubkeys, + }, + ), + ); + } catch (e, s) { + log( + 'Failed to get pubkeys for ${event.coinId}: $e', + isError: true, + path: 'coins_bloc => _onCoinsPubkeysRequested', + trace: s, + ).ignore(); + } + } + Future _onCoinsStarted( CoinsStarted event, Emitter emit, @@ -122,7 +158,7 @@ class CoinsBloc extends Bloc { Emitter emit, ) async { final coin = event.coin; - final walletCoins = Map.from(state.walletCoins); + final walletCoins = Map.of(state.walletCoins); if (coin.isActivating || coin.isActive || coin.isSuspended) { await _kdfSdk.addActivatedCoins([coin.abbr]); @@ -257,7 +293,7 @@ class CoinsBloc extends Bloc { return; } - final coins = Map.from(state.coins); + final coins = Map.of(state.coins); for (final entry in state.coins.entries) { final coin = entry.value; final CexPrice? usdPrice = prices[abbr2Ticker(coin.abbr)]; @@ -314,7 +350,7 @@ class CoinsBloc extends Bloc { case WalletType.iguana: case WalletType.hdwallet: coin.reset(); - final newWalletCoins = Map.from(state.walletCoins); + final newWalletCoins = Map.of(state.walletCoins); newWalletCoins.remove(coin.abbr.toUpperCase()); emit(state.copyWith(walletCoins: newWalletCoins)); log('${coin.name} has been removed', path: 'coins_bloc => _onLogout') diff --git a/lib/bloc/coins_bloc/coins_event.dart b/lib/bloc/coins_bloc/coins_event.dart index 879b68f442..a7af64ab8e 100644 --- a/lib/bloc/coins_bloc/coins_event.dart +++ b/lib/bloc/coins_bloc/coins_event.dart @@ -77,3 +77,13 @@ final class CoinsWalletCoinUpdated extends CoinsEvent { @override List get props => [coin]; } + +// TODO! Refactor to remove this so that the pubkeys are loaded with the coins +class CoinsPubkeysRequested extends CoinsEvent { + const CoinsPubkeysRequested(this.coinId); + + final String coinId; + + @override + List get props => [coinId]; +} diff --git a/lib/bloc/coins_bloc/coins_state.dart b/lib/bloc/coins_bloc/coins_state.dart index 1a97046d3e..ae1e9d196e 100644 --- a/lib/bloc/coins_bloc/coins_state.dart +++ b/lib/bloc/coins_bloc/coins_state.dart @@ -1,46 +1,53 @@ part of 'coins_bloc.dart'; -final class CoinsState extends Equatable { +class CoinsState extends Equatable { const CoinsState({ required this.coins, required this.walletCoins, required this.loginActivationFinished, + required this.pubkeys, }); factory CoinsState.initial() => const CoinsState( coins: {}, walletCoins: {}, loginActivationFinished: false, + pubkeys: {}, ); final Map coins; final Map walletCoins; final bool loginActivationFinished; + final Map pubkeys; - double? getUsdPriceByAmount(String amount, String coinAbbr) { - final Coin? coin = coins[coinAbbr]; - final double? parsedAmount = double.tryParse(amount); - final double? usdPrice = coin?.usdPrice?.price; - - if (coin == null || usdPrice == null || parsedAmount == null) { - return null; - } - return parsedAmount * usdPrice; - } + @override + List get props => + [coins, walletCoins, loginActivationFinished, pubkeys]; CoinsState copyWith({ Map? coins, Map? walletCoins, bool? loginActivationFinished, + Map? pubkeys, }) { return CoinsState( coins: coins ?? this.coins, walletCoins: walletCoins ?? this.walletCoins, loginActivationFinished: loginActivationFinished ?? this.loginActivationFinished, + pubkeys: pubkeys ?? this.pubkeys, ); } - @override - List get props => [coins, walletCoins, loginActivationFinished]; + // TODO! Migrate to SDK + double? getUsdPriceByAmount(String amount, String coinAbbr) { + final Coin? coin = coins[coinAbbr]; + final double? parsedAmount = double.tryParse(amount); + final double? usdPrice = coin?.usdPrice?.price; + + if (coin == null || usdPrice == null || parsedAmount == null) { + return null; + } + return parsedAmount * usdPrice; + } } diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index 045d4e8c81..cee77ca059 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -254,7 +254,7 @@ class WithdrawFormBloc extends Bloc { // If enabling custom fees, set a default fee or reuse from `_getDefaultFee()` emit( state.copyWith( - isCustomFeeEnabled: event.isEnabled, + isCustomFee: event.isEnabled, customFee: event.isEnabled ? () => _getDefaultFee() : () => null, customFeeError: () => null, ), diff --git a/lib/bloc/withdraw_form/withdraw_form_state.dart b/lib/bloc/withdraw_form/withdraw_form_state.dart index 9fe0f9a004..82ea4279d2 100644 --- a/lib/bloc/withdraw_form/withdraw_form_state.dart +++ b/lib/bloc/withdraw_form/withdraw_form_state.dart @@ -16,7 +16,6 @@ class WithdrawFormState extends Equatable { final PubkeyInfo? selectedSourceAddress; final bool isMaxAmount; final bool isCustomFee; - final bool isCustomFeeEnabled; final FeeInfo? customFee; final String? memo; final bool isIbcTransfer; @@ -61,7 +60,6 @@ class WithdrawFormState extends Equatable { this.selectedSourceAddress, this.isMaxAmount = false, this.isCustomFee = false, - this.isCustomFeeEnabled = false, this.customFee, this.memo, this.isIbcTransfer = false, @@ -89,7 +87,6 @@ class WithdrawFormState extends Equatable { ValueGetter? selectedSourceAddress, bool? isMaxAmount, bool? isCustomFee, - bool? isCustomFeeEnabled, ValueGetter? customFee, ValueGetter? memo, bool? isIbcTransfer, @@ -118,7 +115,6 @@ class WithdrawFormState extends Equatable { : this.selectedSourceAddress, isMaxAmount: isMaxAmount ?? this.isMaxAmount, isCustomFee: isCustomFee ?? this.isCustomFee, - isCustomFeeEnabled: isCustomFeeEnabled ?? this.isCustomFeeEnabled, customFee: customFee != null ? customFee() : this.customFee, memo: memo != null ? memo() : this.memo, isIbcTransfer: isIbcTransfer ?? this.isIbcTransfer, @@ -147,7 +143,7 @@ class WithdrawFormState extends Equatable { asset: asset.id.id, toAddress: recipientAddress, amount: isMaxAmount ? null : Decimal.parse(amount), - fee: isCustomFeeEnabled ? customFee : null, + fee: isCustomFee ? customFee : null, from: selectedSourceAddress?.derivationPath != null ? WithdrawalSource.hdDerivationPath( selectedSourceAddress!.derivationPath!, @@ -178,7 +174,6 @@ class WithdrawFormState extends Equatable { selectedSourceAddress, isMaxAmount, isCustomFee, - isCustomFeeEnabled, customFee, memo, isIbcTransfer, diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index cca4555a09..af25874caf 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -6,6 +6,7 @@ abstract class LocaleKeys { static const noKmdAddress = 'noKmdAddress'; static const dex = 'dex'; static const asset = 'asset'; + static const assets = 'assets'; static const price = 'price'; static const volume = 'volume'; static const history = 'history'; diff --git a/lib/shared/widgets/coin_balance.dart b/lib/shared/widgets/coin_balance.dart index bf20fe7dc7..c722f322aa 100644 --- a/lib/shared/widgets/coin_balance.dart +++ b/lib/shared/widgets/coin_balance.dart @@ -1,28 +1,74 @@ -import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +// TODO! Integrate this widget directly to the SDK and make it subscribe to +// the balance changes of the coin. class CoinBalance extends StatelessWidget { - const CoinBalance({required this.coin}); + const CoinBalance({ + super.key, + required this.coin, + this.isVertical = false, + }); + final Coin coin; + final bool isVertical; @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - doubleToString(coin.balance), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + final baseFont = Theme.of(context).textTheme.bodySmall; + final balanceStyle = baseFont?.copyWith( + fontWeight: FontWeight.w500, + ); + + final children = [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: AutoScrollText( + key: Key('coin-balance-asset-${coin.abbr.toLowerCase()}'), + text: doubleToString(coin.balance), + style: balanceStyle, + textAlign: TextAlign.right, + ), + ), + Text( + ' ${Coin.normalizeAbbr(coin.abbr)}', + style: balanceStyle, + ), + ], + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 100, ), - const SizedBox(height: 2), - CoinFiatBalance( - coin, - style: TextStyle(color: theme.custom.increaseColor), + child: Row( + // mainAxisSize: MainAxisSize.min, + children: [ + Text('(', style: balanceStyle), + CoinFiatBalance( + coin, + isAutoScrollEnabled: true, + ), + Text(')', style: balanceStyle), + ], ), - ], - ); + ), + ]; + + return isVertical + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: children, + ); } } diff --git a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart index c016cde038..534e420271 100644 --- a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart +++ b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart @@ -34,7 +34,7 @@ class CoinsTableItem extends StatelessWidget { subtitleText: subtitleText, ), const SizedBox(width: 8), - if (coin.isActive) CoinBalance(coin: coin), + if (coin.isActive) CoinBalance(coin: coin, isVertical: true), ], ), ); diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart b/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart new file mode 100644 index 0000000000..2f0912743a --- /dev/null +++ b/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart @@ -0,0 +1,114 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart'; + +class AnimatedPortfolioCharts extends StatefulWidget { + const AnimatedPortfolioCharts({ + required this.tabController, + required this.walletCoinsFiltered, + super.key, + }); + + final TabController tabController; + final List walletCoinsFiltered; + + @override + State createState() => + _AnimatedPortfolioChartsState(); +} + +class _AnimatedPortfolioChartsState extends State { + bool _userHasInteracted = false; + + @override + void initState() { + super.initState(); + widget.tabController.addListener(_onTabChanged); + } + + @override + void dispose() { + widget.tabController.removeListener(_onTabChanged); + super.dispose(); + } + + void _onTabChanged() { + if (!_userHasInteracted) { + setState(() { + _userHasInteracted = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bool shouldExpand = + state is PortfolioGrowthChartLoadSuccess || _userHasInteracted; + + return Column( + children: [ + Card( + child: TabBar( + controller: widget.tabController, + tabs: [ + Tab(text: LocaleKeys.portfolioGrowth.tr()), + Tab(text: LocaleKeys.profitAndLoss.tr()), + ], + ), + ), + AnimatedContainer( + key: const Key('animated_portfolio_charts_container'), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + height: shouldExpand ? 340 : 0, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(), + child: Stack( + children: [ + TabBarView( + controller: widget.tabController, + children: [ + SizedBox( + width: double.infinity, + child: PortfolioGrowthChart( + initialCoins: widget.walletCoinsFiltered, + ), + ), + SizedBox( + width: double.infinity, + child: PortfolioProfitLossChart( + initialCoins: widget.walletCoinsFiltered, + ), + ), + ], + ), + if (state is! PortfolioGrowthChartLoadSuccess && + _userHasInteracted) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart index 343a4f072c..60ed4144dc 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart @@ -76,7 +76,7 @@ class CoinAddresses extends StatelessWidget { coin: coin, ); }, - ).toList(), + ), if (state.status == FormStatus.submitting) const Padding( padding: EdgeInsets.symmetric(vertical: 20.0), @@ -126,12 +126,11 @@ class CoinAddresses extends StatelessWidget { class _Header extends StatelessWidget { const _Header({ - Key? key, required this.status, required this.createAddressStatus, required this.hideZeroBalance, required this.cantCreateNewAddressReasons, - }) : super(key: key); + }); final FormStatus status; final FormStatus createAddressStatus; @@ -397,11 +396,11 @@ class HideZeroBalanceCheckbox extends StatelessWidget { class CreateButton extends StatelessWidget { const CreateButton({ - Key? key, + super.key, required this.status, required this.createAddressStatus, required this.cantCreateNewAddressReasons, - }) : super(key: key); + }); final FormStatus status; final FormStatus createAddressStatus; @@ -414,6 +413,8 @@ class CreateButton extends StatelessWidget { return Tooltip( message: tooltipMessage, child: UiPrimaryButton( + height: 40, + borderRadius: 20, backgroundColor: isMobile ? theme.custom.dexPageTheme.emptyPlace : null, text: createAddressStatus == FormStatus.submitting ? '${LocaleKeys.creating.tr()}...' @@ -465,10 +466,10 @@ class QrCode extends StatelessWidget { final String coinAbbr; const QrCode({ - Key? key, + super.key, required this.address, required this.coinAbbr, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/views/wallet/coin_details/transactions/transaction_details.dart b/lib/views/wallet/coin_details/transactions/transaction_details.dart index 67a3709493..3989a64d6e 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_details.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_details.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -264,10 +263,11 @@ class TransactionDetails extends StatelessWidget { } Widget _buildFee(BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); + final String formattedFee = transaction.fee?.formatTotal() ?? ''; - final coinsBloc = context.read(); final double? usd = - coinsBloc.state.getUsdPriceByAmount(formattedFee, _feeCoin); + coinsRepository.getUsdPriceByAmount(formattedFee, _feeCoin); final String formattedUsd = formatAmt(usd ?? 0); final String title = LocaleKeys.fees.tr(); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart index db6cef969e..f83cea7e3d 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart @@ -21,7 +21,7 @@ class _FillFormCustomFeeState extends State { @override void initState() { - _isOpen = context.read().state.isCustomFeeEnabled; + _isOpen = context.read().state.isCustomFee; super.initState(); } diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index eb2130353e..d585e843a6 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -13,7 +13,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw class WithdrawForm extends StatefulWidget { final Asset asset; final VoidCallback onSuccess; - final VoidCallback? onBackButtonPressed; // Add this + final VoidCallback? onBackButtonPressed; const WithdrawForm({ required this.asset, diff --git a/lib/views/wallet/wallet_page/common/coin_list_item.dart b/lib/views/wallet/wallet_page/common/coin_list_item.dart index 0cf8b32c92..828aa6f2fc 100644 --- a/lib/views/wallet/wallet_page/common/coin_list_item.dart +++ b/lib/views/wallet/wallet_page/common/coin_list_item.dart @@ -14,7 +14,7 @@ class CoinListItem extends StatelessWidget { final Coin coin; final Color backgroundColor; - final Function(Coin) onTap; + final void Function(Coin) onTap; @override Widget build(BuildContext context) { diff --git a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart index 6b18678b7f..393679caeb 100644 --- a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart +++ b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart @@ -7,9 +7,7 @@ import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/ui_simple_border_button.dart'; -import 'package:web_dex/shared/utils/utils.dart'; -import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; -import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +import 'package:web_dex/shared/widgets/coin_balance.dart'; import 'package:web_dex/shared/widgets/coin_fiat_change.dart'; import 'package:web_dex/shared/widgets/coin_fiat_price.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; @@ -83,7 +81,7 @@ class CoinListItemDesktop extends StatelessWidget { coin: coin, isReEnabling: coin.isActivating, ) - : _CoinBalance( + : CoinBalance( key: Key('balance-asset-${coin.abbr}'), coin: coin, ), @@ -120,57 +118,6 @@ class CoinListItemDesktop extends StatelessWidget { } } -class _CoinBalance extends StatelessWidget { - const _CoinBalance({ - Key? key, - required this.coin, - }) : super(key: key); - - final Coin coin; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Flexible( - flex: 2, - child: AutoScrollText( - key: Key('coin-balance-asset-${coin.abbr.toLowerCase()}'), - text: doubleToString(coin.balance), - style: const TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - ), - ), - Text( - ' ${Coin.normalizeAbbr(coin.abbr)}', - style: const TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - ), - const Text(' (', - style: TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - )), - Flexible( - child: CoinFiatBalance( - coin, - isAutoScrollEnabled: true, - ), - ), - const Text(')', - style: TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - )), - ], - ); - } -} - class _SuspendedMessage extends StatelessWidget { const _SuspendedMessage({ super.key, @@ -188,15 +135,16 @@ class _SuspendedMessage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Opacity( - opacity: 0.6, - child: Text( - LocaleKeys.activationFailedMessage.tr(), - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - )), + opacity: 0.6, + child: Text( + LocaleKeys.activationFailedMessage.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: _fontSize, + fontWeight: FontWeight.w500, + ), + ), + ), const SizedBox(width: 12), Padding( padding: const EdgeInsets.only(top: 1.0), diff --git a/lib/views/wallet/wallet_page/common/coins_list_header.dart b/lib/views/wallet/wallet_page/common/coins_list_header.dart index c847479527..b15419f8de 100644 --- a/lib/views/wallet/wallet_page/common/coins_list_header.dart +++ b/lib/views/wallet/wallet_page/common/coins_list_header.dart @@ -4,63 +4,98 @@ import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class CoinsListHeader extends StatelessWidget { - const CoinsListHeader({Key? key}) : super(key: key); + const CoinsListHeader({ + super.key, + required this.isAuth, + }); + + final bool isAuth; @override Widget build(BuildContext context) { return isMobile - ? const _CoinsListHeaderMobile() - : const _CoinsListHeaderDesktop(); + ? const SizedBox.shrink() + : _CoinsListHeaderDesktop(isAuth: isAuth); } } class _CoinsListHeaderDesktop extends StatelessWidget { - const _CoinsListHeaderDesktop({Key? key}) : super(key: key); + const _CoinsListHeaderDesktop({ + required this.isAuth, + }); + + final bool isAuth; @override Widget build(BuildContext context) { - const style = TextStyle(fontSize: 14, fontWeight: FontWeight.w500); + // final style = Theme.of(context).textTheme.bodyMedium?.copyWith( + // fontWeight: FontWeight.w500, + // ); + final style = Theme.of(context).textTheme.labelSmall; - return Container( - padding: const EdgeInsets.fromLTRB(0, 0, 16, 4), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, + if (isAuth) { + return Row( children: [ - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Text(LocaleKeys.asset.tr(), style: style), - ), - ), - Expanded( - flex: 5, - child: Text(LocaleKeys.balance.tr(), style: style), + // Expand button space + SizedBox(width: 32), + + // Asset header + Container( + constraints: const BoxConstraints(maxWidth: 180), + width: double.infinity, + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.asset.tr(), style: style), ), + + const Spacer(), + + // Balance header Expanded( flex: 2, - child: Text(LocaleKeys.change24hRevert.tr(), style: style), + child: Align( + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.balance.tr(), style: style), + ), ), - Expanded( - flex: 2, - child: Text(LocaleKeys.price.tr(), style: style), + + // 24h change header + Container( + width: 68, + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.change24hRevert.tr(), style: style), ), - Expanded( - flex: 2, - child: Text(LocaleKeys.trend.tr(), style: style), + + const Spacer(), + + // // More actions space + const SizedBox(width: 48), + ], + ); + } + + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + children: [ + // Asset header + Text(LocaleKeys.asset.tr(), style: style), + + const Spacer(flex: 4), + + // Balance header + Text(LocaleKeys.balance.tr(), style: style), + + const Spacer(flex: 2), + + // 24h change header + Padding( + padding: const EdgeInsets.only(right: 48), + child: Text(LocaleKeys.change24hRevert.tr(), style: style), ), + + const Spacer(flex: 2), ], ), ); } } - -class _CoinsListHeaderMobile extends StatelessWidget { - const _CoinsListHeaderMobile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} diff --git a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart new file mode 100644 index 0000000000..0283fc9e18 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart @@ -0,0 +1,253 @@ +// lib/src/defi/asset/coin_list_item.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_balance.dart'; +import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; +import 'package:web_dex/views/wallet/common/wallet_helper.dart'; + +class ExpandableCoinListItem extends StatefulWidget { + final Coin coin; + final AssetPubkeys? pubkeys; + final bool isSelected; + final Color? backgroundColor; + final VoidCallback? onTap; + + const ExpandableCoinListItem({ + super.key, + required this.coin, + required this.pubkeys, + required this.isSelected, + this.onTap, + this.backgroundColor, + }); + + @override + State createState() => _ExpandableCoinListItemState(); +} + +class _ExpandableCoinListItemState extends State { + // Store the expansion state in the widget's state + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + // Attempt to restore state from PageStorage using a unique key + _isExpanded = PageStorage.of(context).readState( + context, + identifier: '${widget.coin.abbr}_expanded', + ) as bool? ?? + false; + } + + void _handleExpansionChanged(bool expanded) { + setState(() { + _isExpanded = expanded; + // Save state to PageStorage using a unique key + PageStorage.of(context).writeState( + context, + _isExpanded, + identifier: '${widget.coin.abbr}_expanded', + ); + }); + } + + @override + Widget build(BuildContext context) { + final hasAddresses = widget.pubkeys?.keys.isNotEmpty ?? false; + final sortedAddresses = hasAddresses + ? (List.of(widget.pubkeys!.keys) + ..sort((a, b) => b.balance.spendable.compareTo(a.balance.spendable))) + : null; + + return CollapsibleCard( + key: PageStorageKey('coin_${widget.coin.abbr}'), + borderRadius: BorderRadius.circular(12), + headerPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + onTap: widget.onTap, + childrenMargin: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + childrenDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + initiallyExpanded: _isExpanded, + onExpansionChanged: _handleExpansionChanged, + expansionControlPosition: ExpansionControlPosition.leading, + emptyChildrenBehavior: EmptyChildrenBehavior.disable, + isDense: true, + title: _buildTitle(context), + maintainState: true, + childrenDivider: const Divider(height: 1, indent: 16, endIndent: 16), + trailing: CoinMoreActionsButton(coin: widget.coin), + children: sortedAddresses + ?.map( + (pubkey) => _AddressRow( + pubkey: pubkey, + coin: widget.coin, + isSwapAddress: pubkey == sortedAddresses.first, + onTap: widget.onTap, + onCopy: () => copyToClipBoard(context, pubkey.address), + ), + ) + .toList(), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = Theme.of(context); + + return Container( + alignment: Alignment.centerLeft, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 180), + child: CoinItem(coin: widget.coin, size: CoinItemSize.large), + ), + const Spacer(), + Expanded( + flex: 2, + child: Align( + alignment: Alignment.centerLeft, + child: CoinBalance(coin: widget.coin), + ), + ), + TrendPercentageText( + investmentReturnPercentage: getTotal24Change([widget.coin]) ?? 0, + iconSize: 16, + spacing: 4, + textStyle: theme.textTheme.bodyMedium, + ), + const Spacer(), + ], + ), + ); + } +} + +class _AddressRow extends StatelessWidget { + final PubkeyInfo pubkey; + final Coin coin; + final bool isSwapAddress; + final VoidCallback? onTap; + final VoidCallback? onCopy; + + const _AddressRow({ + required this.pubkey, + required this.coin, + required this.isSwapAddress, + required this.onTap, + this.onCopy, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ListTile( + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + leading: CircleAvatar( + radius: 16, + backgroundColor: theme.colorScheme.surfaceContainerHigh, + child: const Icon(Icons.person_outline), + ), + title: Row( + children: [ + Text( + pubkey.addressShort, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: IconButton( + iconSize: 16, + icon: const Icon(Icons.copy), + onPressed: onCopy, + visualDensity: VisualDensity.compact, + // constraints: const BoxConstraints( + // minWidth: 32, + // minHeight: 32, + // ), + ), + ), + if (isSwapAddress && !kIsWalletOnly) ...[ + const SizedBox(width: 8), + const Chip( + label: Text( + 'Swap', + // style: theme.textTheme.labelSmall, + ), + // backgroundColor: theme.colorScheme.primaryContainer, + ), + ], + ], + ), + trailing: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${doubleToString(pubkey.balance.spendable.toDouble())} ${coin.abbr}', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + CoinFiatBalance( + coin.copyWith( + balance: pubkey.balance.spendable.toDouble(), + ), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +// This will be able to be removed in the near future when activation state +// is removed from the GUI because it is handled internall by the SDK. +class CoinMoreActionsButton extends StatelessWidget { + const CoinMoreActionsButton({required this.coin}); + + final Coin coin; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (action) { + switch (action) { + case CoinMoreActions.disable: + context.read().add(CoinsDeactivated([coin.abbr])); + break; + } + }, + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: CoinMoreActions.disable, + child: Text('Disable'), + ), + ]; + }, + ); + } +} + +enum CoinMoreActions { + disable, +} diff --git a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart index 58d479a863..dfc270e354 100644 --- a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart @@ -1,12 +1,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; -import 'package:web_dex/views/wallet/wallet_page/common/wallet_coins_list.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; +import 'package:web_dex/views/wallet/common/address_copy_button.dart'; +import 'package:web_dex/views/wallet/common/address_icon.dart'; +import 'package:web_dex/views/wallet/common/address_text.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/expandable_coin_list_item.dart'; class ActiveCoinsList extends StatelessWidget { const ActiveCoinsList({ @@ -15,6 +22,7 @@ class ActiveCoinsList extends StatelessWidget { required this.withBalance, required this.onCoinItemTap, }) : super(key: key); + final String searchPhrase; final bool withBalance; final Function(Coin) onCoinItemTap; @@ -43,9 +51,30 @@ class ActiveCoinsList extends StatelessWidget { sorted = removeTestCoins(sorted); } - return WalletCoinsList( - coins: sorted, - onCoinItemTap: onCoinItemTap, + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final coin = sorted[index]; + + // Fetch pubkeys if not already loaded + if (!state.pubkeys.containsKey(coin.abbr)) { + context.read().add(CoinsPubkeysRequested(coin.abbr)); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ExpandableCoinListItem( + // Changed from ExpandableCoinListItem + key: Key('coin-list-item-${coin.abbr.toLowerCase()}'), + coin: coin, + pubkeys: state.pubkeys[coin.abbr], + isSelected: false, + onTap: () => onCoinItemTap(coin), + ), + ); + }, + childCount: sorted.length, + ), ); }, ); @@ -59,3 +88,202 @@ class ActiveCoinsList extends StatelessWidget { return true; }).toList(); } + +class AddressBalanceList extends StatelessWidget { + const AddressBalanceList({ + Key? key, + required this.coin, + required this.onCreateNewAddress, + required this.pubkeys, + required this.cantCreateNewAddressReasons, + }) : super(key: key); + + final Coin coin; + final AssetPubkeys pubkeys; + final VoidCallback onCreateNewAddress; + final Set? cantCreateNewAddressReasons; + + bool get canCreateNewAddress => cantCreateNewAddressReasons?.isEmpty ?? true; + + @override + Widget build(BuildContext context) { + if (pubkeys.keys.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + // Sort addresses by balance + final sortedAddresses = [...pubkeys.keys] + ..sort((a, b) => b.balance.spendable.compareTo(a.balance.spendable)); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Address list + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sortedAddresses.length, + itemBuilder: (context, index) { + final pubkey = sortedAddresses[index]; + return AddressBalanceCard( + pubkey: pubkey, + coin: coin, + ); + }, + ), + + // Create address button + if (canCreateNewAddress) + Padding( + padding: const EdgeInsets.all(16), + child: Tooltip( + message: _getTooltipMessage(), + child: ElevatedButton.icon( + onPressed: canCreateNewAddress ? onCreateNewAddress : null, + icon: const Icon(Icons.add), + label: const Text('Create New Address'), + ), + ), + ), + ], + ); + } + + String _getTooltipMessage() { + if (cantCreateNewAddressReasons?.isEmpty ?? true) { + return ''; + } + + return cantCreateNewAddressReasons!.map((reason) { + return switch (reason) { + // TODO: Localise and possibly also move localisations to the SDK. + CantCreateNewAddressReason.maxGapLimitReached => + 'Maximum gap limit reached - please use existing unused addresses first', + CantCreateNewAddressReason.maxAddressesReached => + 'Maximum number of addresses reached for this asset', + CantCreateNewAddressReason.missingDerivationPath => + 'Missing derivation path configuration', + CantCreateNewAddressReason.protocolNotSupported => + 'Protocol does not support multiple addresses', + CantCreateNewAddressReason.derivationModeNotSupported => + 'Current wallet mode does not support multiple addresses', + CantCreateNewAddressReason.noActiveWallet => + 'No active wallet - please sign in first', + }; + }).join('\n'); + } +} + +class AddressBalanceCard extends StatelessWidget { + const AddressBalanceCard({ + Key? key, + required this.pubkey, + required this.coin, + }) : super(key: key); + + final PubkeyInfo pubkey; + final Coin coin; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Address row + Row( + children: [ + AddressIcon(address: pubkey.address), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AddressText(address: pubkey.address), + AddressCopyButton(address: pubkey.address), + if (pubkey.isActiveForSwap) + Chip( + label: const Text('Swap Address'), + backgroundColor: Theme.of(context) + .primaryColor + .withOpacity(0.1), + ), + ], + ), + if (pubkey.derivationPath != null) + Text( + 'Derivation: ${pubkey.derivationPath}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + + const Divider(), + + // Balance row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${formatBalance(pubkey.balance.spendable.toBigInt())} ${coin.abbr}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + CoinFiatBalance( + coin.copyWith( + balance: pubkey.balance.spendable.toDouble(), + ), + style: Theme.of(context).textTheme.bodySmall, + // customBalance: pubkey.balance.spendable.toDouble(), + ), + ], + ), + IconButton( + icon: const Icon(Icons.qr_code), + onPressed: () { + showDialog( + context: context, + builder: (context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrCode( + address: pubkey.address, + coinAbbr: coin.abbr, + ), + const SizedBox(height: 16), + SelectableText(pubkey.address), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } + + String formatBalance(BigInt balance) { + return doubleToString(balance.toDouble()); + } +} diff --git a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart index aa28400562..df8a1079cd 100644 --- a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart @@ -7,7 +7,7 @@ import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/views/wallet/wallet_page/common/wallet_coins_list.dart'; -class AllCoinsList extends StatelessWidget { +class AllCoinsList extends StatefulWidget { const AllCoinsList({ Key? key, required this.searchPhrase, @@ -19,25 +19,66 @@ class AllCoinsList extends StatelessWidget { final Function(Coin) onCoinItemTap; @override - Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - final List coins = state.coins.values.toList(); + _AllCoinsListState createState() => _AllCoinsListState(); +} - if (coins.isEmpty) { - return const SliverToBoxAdapter(child: UiSpinner()); - } +class _AllCoinsListState extends State { + List displayedCoins = []; - List displayedCoins = - sortByPriority(filterCoinsByPhrase(coins, searchPhrase)).toList(); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateDisplayedCoins(); + } + @override + void didUpdateWidget(AllCoinsList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.searchPhrase != widget.searchPhrase) { + _updateDisplayedCoins(); + } + } + + void _updateDisplayedCoins() { + final coins = context.read().state.coins.values.toList(); + if (coins.isNotEmpty) { + List filteredCoins = + sortByPriority(filterCoinsByPhrase(coins, widget.searchPhrase)) + .toList(); if (!context.read().state.testCoinsEnabled) { - displayedCoins = removeTestCoins(displayedCoins); + filteredCoins = removeTestCoins(filteredCoins); } + setState(() { + displayedCoins = filteredCoins; + }); + } + } - return WalletCoinsList( - coins: displayedCoins, - onCoinItemTap: onCoinItemTap, - ); - }); + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => previous.coins != current.coins, + listener: (context, state) { + _updateDisplayedCoins(); + }, + builder: (context, state) { + return state.coins.isEmpty + ? const SliverToBoxAdapter(child: UiSpinner()) + : displayedCoins.isEmpty + ? SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No coins found', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ) + : WalletCoinsList( + coins: displayedCoins, + onCoinItemTap: widget.onCoinItemTap, + ); + }, + ); } } diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart index 5f0f637f7e..c2f2646293 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -26,8 +26,7 @@ import 'package:web_dex/router/state/wallet_state.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; -import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart'; -import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/active_coins_list.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/all_coins_list.dart'; @@ -37,7 +36,7 @@ import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dar import 'package:web_dex/views/wallets_manager/wallets_manager_wrapper.dart'; class WalletMain extends StatefulWidget { - const WalletMain({Key? key = const Key('wallet-page')}) : super(key: key); + const WalletMain({super.key = const Key('wallet-page')}); @override State createState() => _WalletMainState(); @@ -119,39 +118,12 @@ class _WalletMainState extends State height: 340, child: PriceChartPage(key: Key('price-chart')), ) - else ...[ - Card( - child: TabBar( - controller: _tabController, - tabs: [ - Tab(text: LocaleKeys.portfolioGrowth.tr()), - Tab(text: LocaleKeys.profitAndLoss.tr()), - ], - ), + else + AnimatedPortfolioCharts( + key: const Key('animated_portfolio_charts'), + tabController: _tabController, + walletCoinsFiltered: walletCoinsFiltered, ), - SizedBox( - height: 340, - child: TabBarView( - controller: _tabController, - children: [ - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioGrowthChart( - initialCoins: walletCoinsFiltered, - ), - ), - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart( - initialCoins: walletCoinsFiltered, - ), - ), - ], - ), - ), - ], const Gap(8), ], ), @@ -304,9 +276,9 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { }); @override - double get minExtent => 120; + final double minExtent = 110; @override - double get maxExtent => 120; + final double maxExtent = 114; @override Widget build( @@ -314,6 +286,8 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { double shrinkOffset, bool overlapsContent, ) { + // return SizedBox.expand(); + return WalletManageSection( withBalance: withBalance, onSearchChange: onSearchChange, diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart index 3ac5fa0612..6ed3018c77 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart @@ -1,4 +1,3 @@ -import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -36,91 +35,52 @@ class WalletManageSection extends StatelessWidget { } Widget _buildDesktopSection(BuildContext context) { - final ThemeData themeData = Theme.of(context); + final ThemeData theme = Theme.of(context); return Card( clipBehavior: Clip.antiAlias, - color: Theme.of(context).colorScheme.surface, + color: theme.colorScheme.surface, margin: const EdgeInsets.all(0), - elevation: pinned ? 4 : 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + elevation: pinned ? 2 : 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + children: [ + Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + WalletManagerSearchField(onChange: onSearchChange), + Spacer(), HiddenWithoutWallet( - child: Container( - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - color: theme.custom.walletEditButtonsBackgroundColor, - borderRadius: BorderRadius.circular(18.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - UiPrimaryButton( - buttonKey: const Key('add-assets-button'), - onPressed: () => _onAddAssetsPress(context), - text: LocaleKeys.addAssets.tr(), - height: 30.0, - width: 110, - backgroundColor: themeData.colorScheme.surface, - textStyle: TextStyle( - color: themeData.colorScheme.primary, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 3.0), - child: UiPrimaryButton( - buttonKey: const Key('remove-assets-button'), - onPressed: () => _onRemoveAssetsPress(context), - text: LocaleKeys.removeAssets.tr(), - height: 30.0, - width: 125, - backgroundColor: themeData.colorScheme.surface, - textStyle: TextStyle( - color: themeData.textTheme.labelLarge?.color - ?.withValues(alpha: 0.7), - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - ), - ], - ), + child: CoinsWithBalanceCheckbox( + withBalance: withBalance, + onWithBalanceChange: onWithBalanceChange, ), ), - Row( - children: [ - HiddenWithoutWallet( - child: Padding( - padding: const EdgeInsets.only(right: 30.0), - child: CoinsWithBalanceCheckbox( - withBalance: withBalance, - onWithBalanceChange: onWithBalanceChange, - ), - ), - ), - WalletManagerSearchField(onChange: onSearchChange), - ], + SizedBox(width: 24), + HiddenWithoutWallet( + child: UiPrimaryButton( + buttonKey: const Key('add-assets-button'), + onPressed: () => _onAddAssetsPress(context), + text: LocaleKeys.addAssets.tr(), + height: 36, + width: 147, + borderRadius: 10, + textStyle: theme.textTheme.bodySmall, + ), ), ], ), - ), - const CoinsListHeader(), - ], + Spacer(), + CoinsListHeader(isAuth: mode == AuthorizeMode.logIn), + ], + ), ), ); } Widget _buildMobileSection(BuildContext context) { - final ThemeData themeData = Theme.of(context); + final ThemeData theme = Theme.of(context); return Container( padding: const EdgeInsets.fromLTRB(2, 20, 2, 10), @@ -142,7 +102,7 @@ class WalletManageSection extends StatelessWidget { Container( padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( - color: themeData.colorScheme.surface, + color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(18.0), ), child: Row( @@ -151,31 +111,10 @@ class WalletManageSection extends StatelessWidget { buttonKey: const Key('add-assets-button'), onPressed: () => _onAddAssetsPress(context), text: LocaleKeys.addAssets.tr(), - height: 25.0, - width: 110, - backgroundColor: themeData.colorScheme.onSurface, - textStyle: TextStyle( - color: themeData.colorScheme.secondary, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 3.0), - child: UiPrimaryButton( - buttonKey: const Key('remove-assets-button'), - onPressed: () => _onRemoveAssetsPress(context), - text: LocaleKeys.remove.tr(), - height: 25.0, - width: 80, - backgroundColor: themeData.colorScheme.onSurface, - textStyle: TextStyle( - color: themeData.textTheme.labelLarge?.color - ?.withValues(alpha: 0.7), - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), + height: 36, + width: 147, + borderRadius: 10, + textStyle: theme.textTheme.bodySmall, ), ], ), diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart index 105fc64fe7..7dfb5f3a9b 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart @@ -6,8 +6,8 @@ import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -const double _hiddenSearchFieldWidth = 38; -const double _normalSearchFieldWidth = 150; +const double _hiddenSearchFieldWidth = 285; +const double _normalSearchFieldWidth = 285; class WalletManagerSearchField extends StatefulWidget { const WalletManagerSearchField({required this.onChange}); @@ -21,9 +21,12 @@ class WalletManagerSearchField extends StatefulWidget { class _WalletManagerSearchFieldState extends State { double _searchFieldWidth = _normalSearchFieldWidth; final TextEditingController _searchController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + @override void initState() { _searchController.addListener(_onChange); + _focusNode.addListener(_onFocusChange); if (isMobile) { _changeSearchFieldWidth(false); } @@ -33,47 +36,69 @@ class _WalletManagerSearchFieldState extends State { @override void dispose() { _searchController.removeListener(_onChange); + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return AnimatedContainer( duration: const Duration(milliseconds: 200), constraints: BoxConstraints.tightFor( width: _searchFieldWidth, - height: isMobile ? _hiddenSearchFieldWidth : 30, + height: isMobile ? _hiddenSearchFieldWidth : 40, ), - child: UiTextFormField( + child: TextFormField( key: const Key('wallet-page-search-field'), controller: _searchController, + focusNode: _focusNode, autocorrect: false, - onFocus: (FocusNode node) { - _searchController.text = _searchController.text.trim(); - if (!isMobile) return; - _changeSearchFieldWidth(node.hasFocus); - }, textInputAction: TextInputAction.none, enableInteractiveSelection: true, - prefixIcon: Icon( - Icons.search, - size: isMobile ? 25 : 18, - ), inputFormatters: [LengthLimitingTextInputFormatter(40)], - hintText: LocaleKeys.searchAssets.tr(), - hintTextStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - height: 1.3), - inputContentPadding: const EdgeInsets.fromLTRB(0, 0, 12, 0), - maxLines: 1, - style: const TextStyle(fontSize: 12), - fillColor: _searchFieldColor, + decoration: InputDecoration( + filled: true, + // fillColor: theme.colorScheme.surfaceContainer, + // hintText: LocaleKeys.searchAssets.tr(), + hintText: LocaleKeys.search.tr(), + // hintStyle: theme.textTheme.bodyMedium?.copyWith( + // color: theme.colorScheme.onSurfaceVariant, + // ), + prefixIcon: Icon( + Icons.search, + size: 20, + // color: theme.colorScheme.onSurfaceVariant, + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + // enabledBorder: OutlineInputBorder( + // borderRadius: BorderRadius.circular(12), + // borderSide: BorderSide.none, + // ), + // focusedBorder: OutlineInputBorder( + // borderRadius: BorderRadius.circular(12), + // borderSide: BorderSide.none, + // ), + // ), + // style: theme.textTheme.bodyMedium?.copyWith( + // color: theme.colorScheme.onSurface, + ), ), ); } + void _onFocusChange() { + if (!isMobile) return; + _changeSearchFieldWidth(_focusNode.hasFocus); + } + void _changeSearchFieldWidth(bool hasFocus) { if (hasFocus) { setState(() => _searchFieldWidth = _normalSearchFieldWidth); @@ -85,8 +110,4 @@ class _WalletManagerSearchFieldState extends State { void _onChange() { widget.onChange(_searchController.text.trim()); } - - Color? get _searchFieldColor { - return isMobile ? theme.custom.searchFieldMobile : null; - } } diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart index 214f2af7cb..2e44a3fba9 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -58,7 +58,7 @@ class WalletOverview extends StatelessWidget { size: 16, ), const SizedBox(width: 4), - Text('$assetCount ${LocaleKeys.asset.tr()}'), + Text('$assetCount ${LocaleKeys.assets.tr()}'), ], ), ), diff --git a/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart b/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart index f5758f7462..cb3e39a377 100644 --- a/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart +++ b/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart @@ -17,9 +17,10 @@ class UiSpinner extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( + return Container( height: height, width: width, + alignment: Alignment.center, child: CircularProgressIndicator( color: color, strokeWidth: strokeWidth, diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index 536b80a2d6..9b3f356e59 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -95,7 +95,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "8d0a162e6b77a5ebfff230c70ac0c8cf09352528" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -104,7 +104,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "8d0a162e6b77a5ebfff230c70ac0c8cf09352528" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -113,7 +113,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "8d0a162e6b77a5ebfff230c70ac0c8cf09352528" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -195,5 +195,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.lock b/pubspec.lock index 999ccb0a12..ae7dd80b50 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -657,7 +657,7 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -666,7 +666,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0" @@ -675,7 +675,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -684,7 +684,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -693,7 +693,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -702,7 +702,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -718,7 +718,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -734,7 +734,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "6ce8fce61ccd9ceebc9920f7e1c60e7081ab20ff" + resolved-ref: "674c1d180295d7424542df857663cd84ad104a99" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0"