mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
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.
498 lines
19 KiB
Dart
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(),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
));
|
|
}
|
|
}
|