wallet card redesign

This commit is contained in:
Arseni
2026-03-06 17:48:36 +03:00
parent 2b0ada1541
commit 281b3834d3
29 changed files with 927 additions and 287 deletions

View File

@@ -1,44 +1,31 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pweb/controllers/dashboard/balance/source_actions.dart';
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class LedgerSourceActions extends StatelessWidget {
final String ledgerAccountRef;
final VoidCallback onAddFunds;
final VoidCallback onWalletDetails;
const LedgerSourceActions({
super.key,
required this.ledgerAccountRef,
required this.onAddFunds,
required this.onWalletDetails,
});
@override
Widget build(BuildContext context) {
final ledgerProvider = context.watch<LedgerAccountsProvider>();
final loc = AppLocalizations.of(context)!;
final isBusy =
ledgerProvider.isWalletRefreshing(ledgerAccountRef) ||
ledgerProvider.isLoading;
final hasTarget = ledgerProvider.accounts.any(
(a) => a.ledgerAccountRef == ledgerAccountRef,
const controller = BalanceSourceActionsController();
final state = controller.ledger(
context: context,
ledgerAccountRef: ledgerAccountRef,
onAddFunds: onAddFunds,
onWalletDetails: onWalletDetails,
);
return BalanceActionsBar(
isRefreshBusy: isBusy,
canRefresh: hasTarget,
onRefresh: () {
context.read<LedgerAccountsProvider>().refreshBalance(ledgerAccountRef);
},
onAddFunds: onAddFunds,
refreshLabel: loc.refreshBalance,
addFundsLabel: loc.addFunds,
);
return BalanceActionsBar(state: state);
}
}

View File

@@ -1,13 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/wallets.dart';
import 'package:pweb/controllers/dashboard/balance/source_actions.dart';
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletSourceActions extends StatelessWidget {
final String walletRef;
@@ -21,22 +16,13 @@ class WalletSourceActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
final walletsProvider = context.watch<WalletsProvider>();
final loc = AppLocalizations.of(context)!;
final isBusy =
walletsProvider.isWalletRefreshing(walletRef) ||
walletsProvider.isLoading;
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef);
return BalanceActionsBar(
isRefreshBusy: isBusy,
canRefresh: hasTarget,
onRefresh: () {
context.read<WalletsProvider>().refreshBalance(walletRef);
},
const controller = BalanceSourceActionsController();
final state = controller.wallet(
context: context,
walletRef: walletRef,
onAddFunds: onAddFunds,
refreshLabel: loc.refreshBalance,
addFundsLabel: loc.addFunds,
);
return BalanceActionsBar(state: state);
}
}
}

View File

@@ -9,11 +9,15 @@ import 'package:pshared/models/payment/source_type.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/l10n/chain.dart';
import 'package:pweb/controllers/dashboard/balance/source_copy.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart';
import 'package:pweb/widgets/refresh_balance/ledger.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -24,6 +28,8 @@ class BalanceSourceCard extends StatelessWidget {
final LedgerAccount? _ledgerAccount;
final VoidCallback onTap;
final VoidCallback onAddFunds;
static const BalanceSourceCopyController _copyController =
BalanceSourceCopyController();
const BalanceSourceCard.wallet({
super.key,
@@ -55,12 +61,29 @@ class BalanceSourceCard extends StatelessWidget {
? null
: wallet.network!.localizedName(context);
final symbol = wallet.tokenSymbol?.trim();
final copyState = _copyController.wallet(wallet.depositAddress);
return BalanceSourceCardLayout(
title: wallet.name,
subtitle: networkLabel,
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
onTap: onTap,
onTap: null,
copyLabel: copyState.label,
canCopy: copyState.canCopy,
onCopy: copyState.canCopy
? () async {
final copied = await _copyController.copy(copyState);
if (!copied || !context.mounted) return;
final loc = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(loc.addressCopied)));
}
: null,
refreshButton: WalletBalanceRefreshButton(
walletRef: wallet.id,
iconOnly: VisibilityState.hidden,
),
actions: WalletSourceActions(
walletRef: wallet.id,
onAddFunds: onAddFunds,
@@ -79,19 +102,35 @@ class BalanceSourceCard extends StatelessWidget {
final accountName = account.name.trim();
final accountCode = account.accountCode.trim();
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
final subtitle = accountCode.isNotEmpty ? accountCode : null;
final badge = account.currency.trim().isEmpty
? null
: account.currency.toUpperCase();
final copyState = _copyController.ledger(accountCode);
return BalanceSourceCardLayout(
title: title,
subtitle: subtitle,
subtitle: null,
badge: badge,
onTap: onTap,
copyLabel: copyState.label,
canCopy: copyState.canCopy,
onCopy: copyState.canCopy
? () async {
final copied = await _copyController.copy(copyState);
if (!copied || !context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(loc.addressCopied)));
}
: null,
refreshButton: LedgerBalanceRefreshButton(
ledgerAccountRef: account.ledgerAccountRef,
iconOnly: VisibilityState.hidden,
),
actions: LedgerSourceActions(
ledgerAccountRef: account.ledgerAccountRef,
onAddFunds: onAddFunds,
onWalletDetails: onTap,
),
amount: LedgerBalanceAmount(account: account),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/layout/wide_body.dart';
class BalanceSourceCardLayout extends StatelessWidget {
@@ -9,8 +9,12 @@ class BalanceSourceCardLayout extends StatelessWidget {
final String? subtitle;
final String? badge;
final Widget amount;
final Widget refreshButton;
final Widget actions;
final VoidCallback onTap;
final VoidCallback? onTap;
final String copyLabel;
final bool canCopy;
final VoidCallback? onCopy;
const BalanceSourceCardLayout({
super.key,
@@ -18,40 +22,39 @@ class BalanceSourceCardLayout extends StatelessWidget {
required this.subtitle,
required this.badge,
required this.amount,
required this.refreshButton,
required this.actions,
required this.onTap,
required this.copyLabel,
required this.canCopy,
required this.onCopy,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final borderRadius = BorderRadius.circular(WalletCardConfig.borderRadius);
return Card(
color: colorScheme.onSecondary,
elevation: WalletCardConfig.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
),
shape: RoundedRectangleBorder(borderRadius: borderRadius),
child: InkWell(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
borderRadius: borderRadius,
onTap: onTap,
child: SizedBox.expand(
child: Padding(
padding: WalletCardConfig.contentPadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: amount),
const SizedBox(width: 12),
actions,
],
),
],
child: BalanceSourceBody(
title: title,
subtitle: subtitle,
badge: badge,
amount: amount,
refreshButton: refreshButton,
actions: actions,
copyLabel: copyLabel,
canCopy: canCopy,
onCopy: onCopy,
),
),
),

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
class LedgerAccountCard extends StatelessWidget {
final LedgerAccount account;
final VoidCallback onAddFunds;
final VoidCallback? onTap;
const LedgerAccountCard({
super.key,
required this.account,
required this.onAddFunds,
this.onTap,
});
@override
Widget build(BuildContext context) {
return BalanceSourceCard.ledger(
account: account,
onTap: onTap ?? () {},
onAddFunds: onAddFunds,
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
class WalletCard extends StatelessWidget {
final Wallet wallet;
final VoidCallback onTopUp;
final VoidCallback onTap;
const WalletCard({
super.key,
required this.wallet,
required this.onTopUp,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return BalanceSourceCard.wallet(
wallet: wallet,
onTap: onTap,
onAddFunds: onTopUp,
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
class BalanceAmountWithRefresh extends StatelessWidget {
final Widget amount;
final Widget refreshButton;
const BalanceAmountWithRefresh({
super.key,
required this.amount,
required this.refreshButton,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButtonTheme(
data: IconButtonThemeData(
style: IconButton.styleFrom(
minimumSize: const Size(30, 30),
maximumSize: const Size(40, 40),
padding: EdgeInsets.zero,
foregroundColor: colorScheme.primary,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
child: refreshButton,
),
const SizedBox(width: 8),
amount,
],
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
class BalanceCopyableField extends StatelessWidget {
final String label;
final bool canCopy;
final VoidCallback? onCopy;
const BalanceCopyableField({
super.key,
required this.label,
required this.canCopy,
required this.onCopy,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
decoration: BoxDecoration(
color: colorScheme.onSecondary,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.primaryFixed, width: 0.6),
),
child: InkWell(
onTap: canCopy ? onCopy : null,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.copy_rounded,
size: 16,
color: canCopy
? colorScheme.primaryFixed
: colorScheme.primary.withValues(alpha: 0.35),
),
const SizedBox(width: 6),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelMedium?.copyWith(
color: canCopy
? colorScheme.primaryFixed
: colorScheme.primary.withValues(alpha: 0.45),
fontWeight: FontWeight.normal,
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/layout/copyable_field.dart';
class BalanceSourceBody extends StatelessWidget {
final String title;
final String? subtitle;
final String? badge;
final Widget amount;
final Widget refreshButton;
final Widget actions;
final String copyLabel;
final bool canCopy;
final VoidCallback? onCopy;
const BalanceSourceBody({
super.key,
required this.title,
required this.subtitle,
required this.badge,
required this.amount,
required this.refreshButton,
required this.actions,
required this.copyLabel,
required this.canCopy,
required this.onCopy,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final sideMaxWidth = constraints.maxWidth * 0.30;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
fit: FlexFit.loose,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: sideMaxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
BalanceHeader(
title: title,
subtitle: subtitle,
badge: badge,
),
SizedBox(height: constraints.maxHeight * 0.06),
BalanceCopyableField(
label: copyLabel,
canCopy: canCopy,
onCopy: onCopy,
),
],
),
),
),
Expanded(
child: Align(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.scaleDown,
child: BalanceAmountWithRefresh(
amount: amount,
refreshButton: refreshButton,
),
),
),
),
Flexible(
fit: FlexFit.loose,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: sideMaxWidth),
child: SizedBox(height: constraints.maxHeight, child: actions),
),
),
],
);
},
);
}
}