Files
hash-wallet/lib/new-ui/pages/account_customizer.dart
jwinterm 7e368f92e6 brand: de-cake the app (hash-bag success art, asset + widget renames, filter, icon padding)
Send-success art: replace the Cake birthday cake with a Hash Bags money sack
that has a # on it and erupts #/$/₿ (assets/images/hash_bag.png). Used by the
success page, send-confirm sheet, exchange-trade, and dEURO savings.

Asset renames (files + refs):
- birthday_cake.png            -> hash_bag.png
- restore_cupcake(.png/_dark)  -> restore_hashbag.png
- qr-cake.png                  -> qr-hashbag.png
- arrow_bottom_cake_green.png  -> arrow_bottom_green.png (+ 2.0x/3.0x)

Identifier renames:
- CakeImageWidget -> HashImageWidget (widget + file + 93 refs)
- typography _cakeRegular/_cakeBold/_cakeSemiBold -> _hash*

Transaction filter: drop the Cake Pay "Orders" row and every non-Trocador
trade provider; the swap gate is already Trocador-only, so this just aligns the
filter UI with reality.

Android launcher: add breathing room to the adaptive-foreground # (all
densities, monochrome included); iOS icon unchanged.

About page: swap the Cake easter eggs for Hash Bags ones.

Deferred (needs a build to verify): new-ui .svg.vec filename renames
(cake_squircle_icon, cakewallet-wordmark, cake-setting) — already Hash Bags in
content, only the filenames are cake, and they route through the .vec precompile.
2026-08-07 02:57:29 -04:00

498 lines
19 KiB
Dart

import 'dart:ui';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:hash_wallet/new-ui/pages/card_customizer.dart';
import 'package:hash_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
import 'package:hash_wallet/new-ui/widgets/modal_grab_handle.dart';
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:hash_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class AccountCustomizerListItem {
final BalanceCard card;
final int order;
final AccountListItem accountListItem;
AccountCustomizerListItem(
{required this.card, required this.order, required this.accountListItem});
}
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
{super.key,
required this.accountListViewModel,
required this.accountEditOrCreateViewModel,
required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
State<AccountCustomizer> createState() => _AccountCustomizerState();
}
class _AccountCustomizerState extends State<AccountCustomizer> {
static const double _kStackVisibleFactor = 0.2;
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
final List<AccountCustomizerListItem> _items = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
final wallet = widget.dashboardViewModel.wallet;
final int? activeId = wallet.type == WalletType.monero
? monero?.getCurrentAccount(wallet).id
: wallet.type == WalletType.wownero
? wownero?.getCurrentAccount(wallet).id
: null;
if (activeId == null) return;
for (int i = 0; i < _items.length-1; i++) {
if(_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
final temp = _items[i];
_items[i] = _items[lastIndex];
_items[lastIndex] = temp;
saveCardOrder();
widget.dashboardViewModel.loadCardDesigns();
break;
}
}
});
}
@override
void dispose() {
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
super.dispose();
}
void loadCards() {
_items.clear();
final accounts = widget.accountListViewModel.accounts;
for (int i = 0; i < accounts.length; i++) {
final index = widget.dashboardViewModel.cardOrder[i];
if(index == null || index >= accounts.length) {
// db order broken.
reset();
break;
}
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[index].label,
accountIndex: accounts[index].id,
balance: accounts[index].balance ?? "0.00",
accountBalance: accounts[index].balance ?? "0.00",
designSwitchDuration: Duration.zero,
assetName: widget.accountListViewModel.currency.title,
onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null,
selected: i == accounts.length - 1,
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[index],
),
order: index,
accountListItem: accounts[index]));
}
setState(() {});
}
@override
Widget build(BuildContext context) {
if (_items.isEmpty) return SizedBox.shrink();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
onTrailingPressed: showResetDialog,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
S.of(context).account_customizer_desc,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
Expanded(
child: Stack(
children: [
ReorderableListView.builder(
padding: EdgeInsets.only(bottom: 196),
scrollController: ModalScrollController.of(context),
onReorder: reorder,
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final animValue = Curves.easeOutCubic.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Opacity(
opacity: 1 - animValue.clamp(0.0, 0.1),
child: Center(
child: SizedBox(
width: cardWidth,
child: Transform.scale(
scale: scale,
child: child,
),
),
),
);
},
child: _items[index].card,
);
},
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final card = _items[index].card;
return Container(
key: ValueKey(index),
child: GestureDetector(
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
);
},
),
SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 50),
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
),
),
),
)))),
],
),
),
],
),
);
}
bool _checkReadyToManage() {
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
showDialog(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).wallet_is_syncing,
alertContent: S.of(context).cannot_manage_accounts_during_sync,
buttonText: S.of(context).ok,
buttonAction: Navigator.of(context).pop));
return false;
}
return true;
}
Future<void> _showAddAccountModal() async {
if (!_checkReadyToManage()) {
return;
}
final res = await showCupertinoModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
final modal = getIt.get<AccountCreationModal>();
return Material(child: modal);
});
if (res != null && res is bool && res == true) {
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
await saveCardOrder();
}
}
void _openCardCustomizer() {
if (!_checkReadyToManage()) {
return;
}
final bloc = getIt.get<CardCustomizerBloc>(param1: false);
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
widget.dashboardViewModel.wallet.currency.name,
cryptoName: widget.dashboardViewModel.wallet.currency.name,
),
),
);
},
)).then((_) async {
bloc.add(DesignSaved());
await bloc.stream.firstWhere((item) => item is CardCustomizerSaved);
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
});
}
void reorder(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
// necessary to copy all this to keep constant constructor for BalanceCard
for (int i = 0; i < _items.length; i++) {
_items[i] = AccountCustomizerListItem(
card: BalanceCard(
accountName: _items[i].card.accountName,
balance: _items[i].card.balance,
accountIndex: _items[i].card.accountIndex,
accountBalance: _items[i].card.accountBalance,
assetName: _items[i].card.assetName,
designSwitchDuration: _items[i].card.designSwitchDuration,
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
selected: i == _items.length - 1,
width: _items[i].card.width,
design: _items[i].card.design,
),
order: i,
accountListItem: _items[i].accountListItem);
}
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
}
}
Future<void> saveCardOrder() async {
for (int i = 0; i < _items.length; i++) {
final idx = _items.indexWhere((element) => element.accountListItem.id == i);
printV("$i: $idx");
await BalanceCardStyleSettings.fromCardDesign(
widget.dashboardViewModel.wallet.walletInfo.internalId,
i,
idx,
_items[idx].card.design)
.insert();
}
}
Future<void> showResetDialog() async {
final res = await showPopUp(
context: context,
builder: (context) {
return AlertWithTwoActions(
alertTitle: S.of(context).reset,
alertContent: S.of(context).card_order_reset_desc,
leftButtonText: S.of(context).yes,
rightButtonText: S.of(context).no,
actionLeftButton: () {
Navigator.of(context).pop(true);
},
actionRightButton: Navigator.of(context).pop);
});
if(res != null && res is bool && res) {
reset();
}
}
Future<void> reset() async {
_items.clear();
final accounts = widget.accountListViewModel.accounts;
for (int i = 0; i < widget.accountListViewModel.accounts.length; i++) {
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[i].label,
accountIndex: accounts[i].id,
balance: accounts[i].balance ?? "0.00",
accountBalance: accounts[i].balance ?? "0.00",
assetName: widget.accountListViewModel.currency.title,
selected: true,
designSwitchDuration: Duration(milliseconds: 200),
width: cardWidth,
design: i >= widget.dashboardViewModel.cardDesigns.length
? CardDesign.genericDefault
: widget.dashboardViewModel.cardDesigns[i],
),
order: i,
accountListItem: accounts[i]));
}
saveCardOrder();
setState(() {});
}
}
class AccountCreationModal extends StatefulWidget {
const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel});
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
@override
State<AccountCreationModal> createState() => _AccountCreationModalState();
}
class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(30))),
child: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalGrabHandle(),
ModalTopBar(title: S.of(context).create_account),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 50,
children: [
SizedBox(),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16)),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: S.of(context).account_name),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: GestureDetector(
onTap: () async {
_controller.text = await generateName();
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: HashImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
),
)
],
),
),
NewPrimaryButton(
onPressed: () async {
if (_loading) return;
setState(() {
_loading = true;
});
widget.accountEditOrCreateViewModel.label = _controller.text;
await widget.accountEditOrCreateViewModel.save();
Navigator.of(context).pop(true);
},
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _loading,
),
SizedBox(),
],
),
)
],
),
),
));
}
}