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

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/dashboard/balance/actions_ui.dart';
import 'package:pweb/controllers/dashboard/balance/source_actions.dart';
import 'package:pweb/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart';
class BalanceActionsBar extends StatefulWidget {
final BalanceActionsState state;
const BalanceActionsBar({super.key, required this.state});
@override
State<BalanceActionsBar> createState() => _BalanceActionsBarState();
}
class _BalanceActionsBarState extends State<BalanceActionsBar> {
static const double _buttonHeight = 34.0;
static const double _buttonGap = 6.0;
static const double _iconSize = 18.0;
static const double _textGap = 8.0;
static const double _horizontalPadding = 6.0;
final BalanceActionsUiController _uiController = BalanceActionsUiController();
@override
void dispose() {
_uiController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textStyle = Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w400,
color: colorScheme.onSecondary,
fontSize: 14,
);
final buttons = <BalanceActionButtonState>[
widget.state.topLeading,
widget.state.topTrailing,
widget.state.bottom,
];
return ListenableBuilder(
listenable: _uiController,
builder: (context, _) {
return Align(
alignment: Alignment.centerRight,
child: OverflowBox(
alignment: Alignment.centerRight,
minWidth: 0,
maxWidth: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < buttons.length; i++) ...[
HoverExpandableActionButton(
height: _buttonHeight,
icon: buttons[i].icon,
label: buttons[i].label,
iconSize: _iconSize,
textStyle: textStyle,
expanded: _uiController.isExpanded(i),
textGap: _textGap,
horizontalPadding: _horizontalPadding,
onHoverChanged: (hovered) =>
_uiController.onHoverChanged(i, hovered),
onPressed: buttons[i].onPressed,
),
if (i != buttons.length - 1)
const SizedBox(height: _buttonGap),
],
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class HoverExpandableActionButton extends StatelessWidget {
final double height;
final IconData icon;
final String label;
final double iconSize;
final TextStyle? textStyle;
final bool expanded;
final double textGap;
final double horizontalPadding;
final ValueChanged<bool> onHoverChanged;
final VoidCallback onPressed;
const HoverExpandableActionButton({
super.key,
required this.height,
required this.icon,
required this.label,
required this.iconSize,
required this.textStyle,
required this.expanded,
required this.textGap,
required this.horizontalPadding,
required this.onHoverChanged,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return MouseRegion(
onEnter: (_) => onHoverChanged(true),
onExit: (_) => onHoverChanged(false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
height: height,
decoration: BoxDecoration(
color: colorScheme.primaryFixed,
borderRadius: BorderRadius.circular(999),
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: onPressed,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: iconSize, color: colorScheme.onSecondary),
AnimatedSize(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
alignment: Alignment.centerRight,
child: expanded
? Padding(
padding: EdgeInsets.only(left: textGap),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.visible,
style: textStyle,
),
)
: const SizedBox.shrink(),
),
],
),
),
),
),
),
);
}
}

View File

@@ -29,12 +29,17 @@ class BalanceAmount extends StatelessWidget {
final isMasked = wallets.isBalanceMasked(wallet.id);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isMasked ? '•••• $currencyBalance' : '${amountToString(wallet.balance)} $currencyBalance',
style: textTheme.headlineSmall?.copyWith(
isMasked
? '•••• $currencyBalance'
: '${amountToString(wallet.balance)} $currencyBalance',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
color: colorScheme.primary,
),
),
const SizedBox(width: _iconSpacing),
@@ -43,7 +48,7 @@ class BalanceAmount extends StatelessWidget {
child: Icon(
isMasked ? Icons.visibility_off : Icons.visibility,
size: _iconSize,
color: colorScheme.onSurface,
color: colorScheme.primary,
),
),
],

View File

@@ -1,152 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/dashboard/balance_item.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart';
class BalanceCarousel extends StatefulWidget {
final List<BalanceItem> items;
final int currentIndex;
final ValueChanged<int> onIndexChanged;
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const BalanceCarousel({
super.key,
required this.items,
required this.currentIndex,
required this.onIndexChanged,
required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap,
required this.onLedgerTap,
});
@override
State<BalanceCarousel> createState() => _BalanceCarouselState();
}
class _BalanceCarouselState extends State<BalanceCarousel> {
late final PageController _controller;
@override
void initState() {
super.initState();
_controller = PageController(
initialPage: widget.currentIndex,
viewportFraction: WalletCardConfig.viewportFraction,
);
}
@override
void didUpdateWidget(covariant BalanceCarousel oldWidget) {
super.didUpdateWidget(oldWidget);
if (!mounted) return;
if (_controller.hasClients) {
final currentPage = _controller.page?.round();
if (currentPage != widget.currentIndex) {
_controller.jumpToPage(widget.currentIndex);
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _goToPage(int index) {
if (!_controller.hasClients) return;
_controller.animateToPage(
index,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
);
}
@override
Widget build(BuildContext context) {
if (widget.items.isEmpty) {
return const SizedBox.shrink();
}
final safeIndex = widget.currentIndex.clamp(0, widget.items.length - 1);
final scrollBehavior = ScrollConfiguration.of(context).copyWith(
dragDevices: const {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
},
);
return Column(
children: [
SizedBox(
height: WalletCardConfig.cardHeight,
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: ScrollConfiguration(
behavior: scrollBehavior,
child: PageView.builder(
controller: _controller,
onPageChanged: widget.onIndexChanged,
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
final Widget card = switch (item) {
WalletBalanceItem(:final wallet) => WalletCard(
wallet: wallet,
onTopUp: () => widget.onTopUp(wallet),
onTap: () => widget.onWalletTap(wallet),
),
LedgerBalanceItem(:final account) => LedgerAccountCard(
account: account,
onTap: () => widget.onLedgerTap(account),
onAddFunds: () => widget.onLedgerAddFunds(account),
),
AddBalanceActionItem() => const AddBalanceCard(),
};
return Padding(
padding: WalletCardConfig.cardPadding,
child: card,
);
},
),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: safeIndex > 0 ? () => _goToPage(safeIndex - 1) : null,
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 16),
CarouselIndicator(itemCount: widget.items.length, index: safeIndex),
const SizedBox(width: 16),
IconButton(
onPressed: safeIndex < widget.items.length - 1
? () => _goToPage(safeIndex + 1)
: null,
icon: const Icon(Icons.arrow_forward),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/dashboard/balance_item.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/cards/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/cards/wallet.dart';
class BalanceCarouselCardItem extends StatelessWidget {
final BalanceItem item;
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const BalanceCarouselCardItem({
super.key,
required this.item,
required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap,
required this.onLedgerTap,
});
@override
Widget build(BuildContext context) {
final card = switch (item) {
WalletBalanceItem(:final wallet) => WalletCard(
wallet: wallet,
onTopUp: () => onTopUp(wallet),
onTap: () => onWalletTap(wallet),
),
LedgerBalanceItem(:final account) => LedgerAccountCard(
account: account,
onTap: () => onLedgerTap(account),
onAddFunds: () => onLedgerAddFunds(account),
),
AddBalanceActionItem() => const AddBalanceCard(),
};
return Padding(padding: WalletCardConfig.cardPadding, child: card);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/controllers/dashboard/balance/carousel.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel/card_item.dart';
class BalanceCarouselCardsView extends StatelessWidget {
final BalanceCarouselController controller;
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
final double height;
const BalanceCarouselCardsView({
super.key,
required this.controller,
required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap,
required this.onLedgerTap,
required this.height,
});
@override
Widget build(BuildContext context) {
final scrollBehavior = ScrollConfiguration.of(context).copyWith(
dragDevices: const {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
},
);
return SizedBox(
height: height,
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: ScrollConfiguration(
behavior: scrollBehavior,
child: PageView.builder(
controller: controller.pageController,
onPageChanged: controller.onPageChanged,
itemCount: controller.items.length,
itemBuilder: (context, index) => BalanceCarouselCardItem(
item: controller.items[index],
onTopUp: onTopUp,
onLedgerAddFunds: onLedgerAddFunds,
onWalletTap: onWalletTap,
onLedgerTap: onLedgerTap,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/controllers/dashboard/balance/carousel.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel/cards_view.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel/navigation.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
class BalanceCarousel extends StatelessWidget {
final BalanceCarouselController controller;
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const BalanceCarousel({
super.key,
required this.controller,
required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap,
required this.onLedgerTap,
});
@override
Widget build(BuildContext context) {
if (controller.items.isEmpty) {
return const SizedBox.shrink();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.syncPageController();
});
final safeIndex = controller.index.clamp(0, controller.items.length - 1);
return LayoutBuilder(
builder: (context, constraints) {
final cardHeight = WalletCardConfig.cardHeightForWidth(
constraints.maxWidth,
);
return Column(
children: [
BalanceCarouselCardsView(
controller: controller,
onTopUp: onTopUp,
onLedgerAddFunds: onLedgerAddFunds,
onWalletTap: onWalletTap,
onLedgerTap: onLedgerTap,
height: cardHeight,
),
const SizedBox(height: 16),
BalanceCarouselNavigation(controller: controller, index: safeIndex),
],
);
},
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/dashboard/balance/carousel.dart';
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
class BalanceCarouselNavigation extends StatelessWidget {
final BalanceCarouselController controller;
final int index;
const BalanceCarouselNavigation({
super.key,
required this.controller,
required this.index,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: index > 0 ? controller.goBack : null,
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 16),
CarouselIndicator(itemCount: controller.items.length, index: index),
const SizedBox(width: 16),
IconButton(
onPressed: index < controller.items.length - 1
? controller.goForward
: null,
icon: const Icon(Icons.arrow_forward),
),
],
);
}
}

View File

@@ -2,14 +2,21 @@ import 'package:flutter/material.dart';
abstract class WalletCardConfig {
static const double cardHeight = 145.0;
static const double elevation = 4.0;
static const double borderRadius = 16.0;
static const double viewportFraction = 0.9;
static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 8);
static const EdgeInsets contentPadding = EdgeInsets.all(16);
static const double viewportFraction = 0.96;
static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 6);
static const EdgeInsets contentPadding = EdgeInsets.symmetric(
horizontal: 28,
vertical: 16,
);
static const double dotSize = 8.0;
static const EdgeInsets dotMargin = EdgeInsets.symmetric(horizontal: 4);
static double cardHeightForWidth(double width) {
final adaptiveHeight = width * 0.18;
return adaptiveHeight.clamp(150.0, 230.0);
}
}

View File

@@ -1,129 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pweb/models/dashboard/balance_item.dart';
class BalanceCarouselController with ChangeNotifier {
WalletsController? _walletsController;
List<BalanceItem> _items = const <BalanceItem>[BalanceItem.addAction()];
int _index = 0;
List<BalanceItem> get items => _items;
int get index => _index;
void update({
required WalletsController walletsController,
required LedgerAccountsProvider ledgerProvider,
}) {
_walletsController = walletsController;
final nextItems = <BalanceItem>[
...walletsController.wallets.map(BalanceItem.wallet),
...ledgerProvider.accounts.map(BalanceItem.ledger),
const BalanceItem.addAction(),
];
final nextIndex = _resolveNextIndex(nextItems, walletsController);
final hasItemsChanged = !_isSameItems(_items, nextItems);
final hasIndexChanged = _index != nextIndex;
_items = nextItems;
_index = nextIndex;
if (hasItemsChanged || hasIndexChanged) {
notifyListeners();
}
}
void onPageChanged(int value) {
final next = _clampIndex(value, _items.length);
if (_index == next) {
_syncSelectedWallet();
return;
}
_index = next;
_syncSelectedWallet();
notifyListeners();
}
void goBack() => onPageChanged(_index - 1);
void goForward() => onPageChanged(_index + 1);
int _resolveNextIndex(
List<BalanceItem> nextItems,
WalletsController walletsController,
) {
final currentWalletRef = _currentWalletRef(_items, _index);
if (currentWalletRef != null) {
final byCurrentWallet = _walletIndexByRef(nextItems, currentWalletRef);
if (byCurrentWallet != null) return byCurrentWallet;
final selectedWalletRef = walletsController.selectedWalletRef;
final bySelectedWallet = _walletIndexByRef(nextItems, selectedWalletRef);
if (bySelectedWallet != null) return bySelectedWallet;
}
return _clampIndex(_index, nextItems.length);
}
String? _currentWalletRef(List<BalanceItem> items, int index) {
if (items.isEmpty || index < 0 || index >= items.length) return null;
final current = items[index];
return switch (current) {
WalletBalanceItem(:final wallet) => wallet.id,
_ => null,
};
}
int? _walletIndexByRef(List<BalanceItem> items, String? walletRef) {
if (walletRef == null || walletRef.isEmpty) return null;
final idx = items.indexWhere(
(item) => switch (item) {
WalletBalanceItem(:final wallet) => wallet.id == walletRef,
_ => false,
},
);
if (idx < 0) return null;
return idx;
}
int _clampIndex(int value, int itemCount) {
if (itemCount <= 0) return 0;
return value.clamp(0, itemCount - 1);
}
bool _isSameItems(List<BalanceItem> left, List<BalanceItem> right) {
if (left.length != right.length) return false;
for (var i = 0; i < left.length; i++) {
final a = left[i];
final b = right[i];
if (a.runtimeType != b.runtimeType) return false;
if (_itemIdentity(a) != _itemIdentity(b)) return false;
}
return true;
}
String _itemIdentity(BalanceItem item) => switch (item) {
WalletBalanceItem(:final wallet) => wallet.id,
LedgerBalanceItem(:final account) => account.ledgerAccountRef,
AddBalanceActionItem() => 'add',
};
void _syncSelectedWallet() {
final walletsController = _walletsController;
if (walletsController == null || _items.isEmpty) return;
final current = _items[_index];
if (current is! WalletBalanceItem) return;
final wallet = current.wallet;
if (walletsController.selectedWallet?.id == wallet.id) return;
walletsController.selectWallet(wallet);
}
}

View File

@@ -20,46 +20,51 @@ class BalanceHeader extends StatelessWidget {
final subtitleText = subtitle?.trim();
final badgeText = badge?.trim();
return Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
title,
style: textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: textTheme.titleLarge?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.w700,
),
),
if (subtitleText != null && subtitleText.isNotEmpty)
Text(
subtitleText,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
if (badgeText != null && badgeText.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.primaryFixed,
borderRadius: BorderRadius.circular(999),
),
child: Text(
badgeText,
style: textTheme.labelSmall?.copyWith(
color: colorScheme.onSecondary,
fontWeight: FontWeight.w700,
),
),
],
),
),
if (badgeText != null && badgeText.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
badgeText,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
],
],
),
if (subtitleText != null && subtitleText.isNotEmpty)
Text(
subtitleText,
style: textTheme.titleSmall?.copyWith(
color: colorScheme.primaryFixed,
fontWeight: FontWeight.w500,
),
maxLines: 1,
),
],
],
);
}

View File

@@ -26,14 +26,15 @@ class LedgerBalanceAmount extends StatelessWidget {
: LedgerBalanceFormatter.format(account);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
balance,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
Text(
balance,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(width: 12),
@@ -44,7 +45,7 @@ class LedgerBalanceAmount extends StatelessWidget {
child: Icon(
isMasked ? Icons.visibility_off : Icons.visibility,
size: 24,
color: colorScheme.onSurface,
color: colorScheme.primary,
),
),
],

View File

@@ -5,7 +5,8 @@ import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
import 'package:pweb/controllers/dashboard/balance/carousel.dart';
class BalanceWidgetProviders extends StatelessWidget {
final Widget child;

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,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),
),
),
],
);
},
);
}
}

View File

@@ -7,11 +7,12 @@ import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel/carousel.dart';
import 'package:pweb/controllers/dashboard/balance/carousel.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class BalanceWidget extends StatelessWidget {
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
@@ -46,9 +47,7 @@ class BalanceWidget extends StatelessWidget {
}
final carouselWidget = BalanceCarousel(
items: carousel.items,
currentIndex: carousel.index,
onIndexChanged: carousel.onPageChanged,
controller: carousel,
onTopUp: onTopUp,
onLedgerAddFunds: onLedgerAddFunds,
onWalletTap: onWalletTap,

View File

@@ -6,7 +6,8 @@ import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pweb/controllers/payments/amount_field.dart';
import 'package:pweb/pages/dashboard/payouts/amount/feild.dart';
import 'package:pweb/pages/dashboard/payouts/amount/field.dart';
class PaymentAmountWidget extends StatelessWidget {
const PaymentAmountWidget({super.key});