mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
CW-1513/CW-1610: Add account archival and UI improvements (#3600)
* add ability to hide accounts * remove import * merge * merge * fix loading accounts when styleSettings are not saved yet * merge * auto-reformat * small fixes * proper reload on card unhide * block hiding last card * fixup * revert formatting-only changes * liner fixes * fix overflows * card layout as separate class to prevent index errors * fix mini card on addresses page * block opening customizers if wallet is syncing * switch to BlocProvider.value to prevent auto dispose * imports * fix fiat balance * Improve homescreen layout and account access * Add homescreen layout regression tests * improve account management w/ education + archival flows * reorganize wallet-specific settings * preserve account archival during dismissal * fix: align account visuals with design feedback * minor account & archival visual corrections * simplify wallet settings + account UI state * generalise education carousel & dismissal tracking * refactor account UI flows + minor improvements * minor refactors and improvement * pass account balances as Money and format at display * use fresh full balance for account archival check * refactor BalanceCard w/ MoneyText + cleanup * minor card_customizer cleanup * rework card customizer flow --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
Omar Hatem
parent
9f1f82fdd4
commit
20f1ceb651
+31
-13
@@ -295,7 +295,6 @@ import 'dogecoin/dogecoin.dart';
|
||||
import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
|
||||
import 'new-ui/widgets/addresses_page/address_info.dart';
|
||||
import 'src/screens/buy/buy_sell_page.dart';
|
||||
import "src/screens/settings/widgets/account_creation_modal.dart";
|
||||
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
@@ -561,14 +560,15 @@ Future<void> setup({
|
||||
sharedPreferences: getIt.get<SharedPreferences>(),
|
||||
keyService: getIt.get<KeyService>()));
|
||||
|
||||
getIt.registerFactoryParam<CardCustomizerBloc, bool, BitcoinAmountDisplayMode?>(
|
||||
(lightningMode, displayMode) {
|
||||
getIt.registerFactoryParam<CardCustomizerBloc, CardCustomizerBlocParams, void>(
|
||||
(params, _) {
|
||||
final wallet = getIt.get<AppStore>().wallet!;
|
||||
return CardCustomizerBloc(wallet,
|
||||
lightningMode: lightningMode,
|
||||
canHide: params.canHide,
|
||||
lightningMode: params.lightningMode,
|
||||
displaySats: wallet.type == WalletType.bitcoin &&
|
||||
(displayMode == BitcoinAmountDisplayMode.satoshi ||
|
||||
(displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode)));
|
||||
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshi ||
|
||||
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning && params.lightningMode)));
|
||||
});
|
||||
|
||||
getIt.registerLazySingleton<PriceStore>(() => PriceStore(settingsStore: getIt.get<SettingsStore>()));
|
||||
@@ -896,7 +896,7 @@ Future<void> setup({
|
||||
if (wallet.type == WalletType.monero ||
|
||||
wallet.type == WalletType.wownero ||
|
||||
wallet.type == WalletType.haven) {
|
||||
return MoneroAccountListViewModel(wallet, getIt.get<SettingsStore>());
|
||||
return MoneroAccountListViewModel(wallet);
|
||||
}
|
||||
throw Exception(
|
||||
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
|
||||
@@ -905,7 +905,7 @@ Future<void> setup({
|
||||
getIt.registerFactory<BitcoinAccountListViewModel>(() {
|
||||
final wallet = getIt.get<AppStore>().wallet!;
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
return BitcoinAccountListViewModel(wallet, getIt.get<SettingsStore>());
|
||||
return BitcoinAccountListViewModel(wallet);
|
||||
}
|
||||
throw Exception(
|
||||
'Unexpected wallet type: ${wallet.type} for generate Bitcoin AccountListViewModel');
|
||||
@@ -927,11 +927,29 @@ Future<void> setup({
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel, AccountListItem?, void>(
|
||||
(AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel(
|
||||
monero!.getAccountList(getIt.get<AppStore>().wallet!),
|
||||
wownero?.getAccountList(getIt.get<AppStore>().wallet!),
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
accountListItem: account));
|
||||
(AccountListItem? account, _) {
|
||||
final wallet = getIt.get<AppStore>().wallet!;
|
||||
|
||||
if (wallet.type == WalletType.monero) {
|
||||
return MoneroAccountEditOrCreateViewModel(
|
||||
monero!.getAccountList(wallet),
|
||||
null,
|
||||
wallet: wallet,
|
||||
accountListItem: account,
|
||||
);
|
||||
}
|
||||
|
||||
if (wallet.type == WalletType.wownero) {
|
||||
return MoneroAccountEditOrCreateViewModel(
|
||||
null,
|
||||
wownero!.getAccountList(wallet),
|
||||
wallet: wallet,
|
||||
accountListItem: account,
|
||||
);
|
||||
}
|
||||
|
||||
throw StateError("Account creation is unavailable for ${wallet.type}");
|
||||
});
|
||||
|
||||
getIt.registerFactory<BitcoinAccountEditOrCreateViewModel>(
|
||||
() => BitcoinAccountEditOrCreateViewModel(
|
||||
|
||||
@@ -3,6 +3,8 @@ import "package:cw_core/wallet_type.dart";
|
||||
class PreferencesKey {
|
||||
static const currentWalletType = 'current_wallet_type';
|
||||
static const currentWalletName = 'current_wallet_name';
|
||||
static const accountsHomePromoDismissed = 'accounts_home_promo_dismissed';
|
||||
static String educationDismissed(String educationId) => "education_dismissed_$educationId";
|
||||
static const currentNodeIdKey = 'current_node_id';
|
||||
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
|
||||
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
|
||||
|
||||
@@ -146,6 +146,10 @@ class CWMonero extends Monero {
|
||||
@override
|
||||
MoneroAccountList getAccountList(Object wallet) => CWMoneroAccountList(wallet);
|
||||
|
||||
@override
|
||||
Money getAccountFullBalance(int accountIndex) =>
|
||||
monero_wallet_api.getFullBalance(accountIndex: accountIndex);
|
||||
|
||||
@override
|
||||
MoneroSubaddressList getSubaddressList(Object wallet) => CWMoneroSubaddressList(wallet);
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/pages/education_page.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class AccountEducationPage extends EducationPage {
|
||||
const AccountEducationPage({required super.settingsStore, super.key});
|
||||
|
||||
@override
|
||||
String get educationId => "accounts";
|
||||
|
||||
@override
|
||||
String get title => S.current.accounts;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/settings_row_icons/accounts.svg";
|
||||
|
||||
@override
|
||||
String get completionLabel => S.current.accounts_education_understand_continue;
|
||||
|
||||
@override
|
||||
String progressLabel(int current, int total) =>
|
||||
S.current.accounts_education_progress("$current", "$total");
|
||||
|
||||
@override
|
||||
List<EducationSlide> get slides => <EducationSlide>[
|
||||
EducationSlide(
|
||||
distributeChildren: true,
|
||||
children: [
|
||||
EducationText(text: S.current.accounts_education_organize_title),
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/accounts_overview.svg",
|
||||
key: ValueKey("accounts-education-overview-image"),
|
||||
width: 133,
|
||||
height: 324,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_organize_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/recovery_shield.svg",
|
||||
key: ValueKey("accounts-education-recovery-image"),
|
||||
width: 213,
|
||||
height: 213,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_recovery_title,
|
||||
highlightedText: S.current.accounts_education_recovery_highlight,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_recovery_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/account_order.svg",
|
||||
key: ValueKey("accounts-education-order-image"),
|
||||
width: 309,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
EducationText(text: S.current.accounts_education_order_title),
|
||||
const SizedBox(height: 51),
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/ordered_account.svg",
|
||||
key: ValueKey("accounts-education-ordered-account-image"),
|
||||
width: 160,
|
||||
height: 99,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EducationText(
|
||||
text: S.current.accounts_education_order_description,
|
||||
highlightedText: S.current.accounts_education_order_highlight,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_order_restore_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/archive_warning.svg",
|
||||
key: ValueKey("accounts-education-archive-image"),
|
||||
width: 232,
|
||||
height: 101,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EducationText(
|
||||
text: S.current.accounts_education_archive_title,
|
||||
warning: true,
|
||||
),
|
||||
const SizedBox(height: 13),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_archive_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import 'dart:ui';
|
||||
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
|
||||
@@ -16,7 +15,6 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -69,6 +67,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
late final TextEditingController _searchController;
|
||||
|
||||
CardDesign? design;
|
||||
ReactionDisposer? _cardDesignsDisposer;
|
||||
|
||||
void updateItems() {
|
||||
setState(() {
|
||||
@@ -86,20 +85,32 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
loadCardDesign();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
reaction((_) {
|
||||
final index = widget.dashboardViewModel.accountListViewModel?.selectedAccount?.id ?? 0;
|
||||
final designs = widget.dashboardViewModel.cardDesigns;
|
||||
return (index >= 0 && index < designs.length) ? designs[index] : null;
|
||||
}, (value) {
|
||||
if (!mounted) return;
|
||||
_cardDesignsDisposer = reaction((_) => widget.dashboardViewModel.currentCardDesign, (value) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
design = value;
|
||||
design = widget.dashboardViewModel.currentCardDesign;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadCardDesign() async {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
design = widget.dashboardViewModel.currentCardDesign;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cardDesignsDisposer?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final filteredItems = items.where((item) {
|
||||
|
||||
@@ -1,75 +1,96 @@
|
||||
import "dart:async";
|
||||
import "dart:math";
|
||||
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/account_confirmation_content.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart";
|
||||
import "package:cake_wallet/utils/show_pop_up.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class CardCustomizer extends StatefulWidget {
|
||||
const CardCustomizer({required this.cryptoTitle, required this.cryptoName, super.key});
|
||||
const CardCustomizer({
|
||||
required this.cryptoTitle,
|
||||
required this.cryptoName,
|
||||
this.account,
|
||||
this.fiatBalance,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String cryptoTitle;
|
||||
final String cryptoName;
|
||||
final AccountListItem? account;
|
||||
final String? fiatBalance;
|
||||
|
||||
static Future<void> show({
|
||||
static Future<bool> show({
|
||||
required BuildContext context,
|
||||
required DashboardViewModel dashboardViewModel,
|
||||
required bool lightningMode,
|
||||
bool asModalSheet = true,
|
||||
VoidCallback? onSaved,
|
||||
required AccountListItem? account,
|
||||
bool lightningMode = false,
|
||||
bool canHide = false,
|
||||
bool asPage = false,
|
||||
}) async {
|
||||
final bloc = getIt.get<CardCustomizerBloc>(
|
||||
param1: lightningMode,
|
||||
param2: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
|
||||
param1: CardCustomizerBlocParams(
|
||||
lightningMode: lightningMode,
|
||||
amountDisplayMode: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
|
||||
canHide: canHide,
|
||||
),
|
||||
);
|
||||
|
||||
final customizer = BlocProvider(
|
||||
create: (_) => bloc,
|
||||
if (bloc.state is CardCustomizerNotLoaded) {
|
||||
await bloc.stream.firstWhere((state) => state is! CardCustomizerNotLoaded);
|
||||
}
|
||||
if (!context.mounted) {
|
||||
await bloc.close();
|
||||
return false;
|
||||
}
|
||||
final balanceViewModel = dashboardViewModel.balanceViewModel;
|
||||
final customizer = BlocProvider.value(
|
||||
value: bloc,
|
||||
child: Material(
|
||||
child: CardCustomizer(
|
||||
cryptoTitle: dashboardViewModel.wallet.currency.fullName ??
|
||||
dashboardViewModel.wallet.currency.name,
|
||||
cryptoName: dashboardViewModel.wallet.currency.name,
|
||||
account: account,
|
||||
fiatBalance: account == null ? null : balanceViewModel.accountFiatBalance(account),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (asModalSheet) {
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
context: context,
|
||||
builder: (context) => ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: customizer,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => customizer),
|
||||
);
|
||||
}
|
||||
final archived = await (asPage
|
||||
? Navigator.of(context).push<bool>(CupertinoPageRoute(builder: (_) => customizer))
|
||||
: CupertinoScaffold.showCupertinoModalBottomSheet<bool>(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
context: context,
|
||||
builder: (context) => ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: customizer,
|
||||
),
|
||||
));
|
||||
|
||||
// Save edits before AccountHidden writes the hidden state.
|
||||
bloc.add(DesignSaved());
|
||||
|
||||
await bloc.stream.firstWhere(
|
||||
(state) => state is CardCustomizerSaved,
|
||||
orElse: () => bloc.state,
|
||||
);
|
||||
|
||||
await dashboardViewModel.loadCardDesigns();
|
||||
await dashboardViewModel.accountListViewModel?.reload();
|
||||
|
||||
onSaved?.call();
|
||||
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
|
||||
if (archived == true) {
|
||||
bloc.add(AccountHidden());
|
||||
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
|
||||
}
|
||||
await bloc.close();
|
||||
return archived == true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -78,9 +99,10 @@ class CardCustomizer extends StatefulWidget {
|
||||
|
||||
class _CardCustomizerState extends State<CardCustomizer> {
|
||||
final accountNameController = TextEditingController();
|
||||
bool editEnabled = false;
|
||||
late final CardCustomizerBloc bloc;
|
||||
|
||||
bool get _isAccount => widget.account != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -88,18 +110,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
bloc = context.read<CardCustomizerBloc>();
|
||||
|
||||
if (bloc.state is! CardCustomizerNotLoaded) {
|
||||
editEnabled = bloc.state.accountName.isNotEmpty;
|
||||
accountNameController.text = bloc.state.accountName;
|
||||
} else {
|
||||
late final StreamSubscription sub;
|
||||
sub = bloc.stream.listen((state) {
|
||||
if (state is! CardCustomizerNotLoaded) {
|
||||
setState(() {
|
||||
editEnabled = state.accountName.isNotEmpty;
|
||||
});
|
||||
sub.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +129,46 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
return design;
|
||||
}
|
||||
|
||||
Future<void> _requestArchive() async {
|
||||
final account = AccountListItem(
|
||||
id: widget.account!.id,
|
||||
label: bloc.state.accountName,
|
||||
balance: widget.account!.balance,
|
||||
);
|
||||
final isFunded = account.isFunded;
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: isFunded
|
||||
? S.of(dialogContext).archive_account_confirmation_title
|
||||
: S.of(dialogContext).archive_account_title,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: ArchiveConfirmationContent(
|
||||
account: account,
|
||||
fiatBalance: widget.fiatBalance,
|
||||
isFunded: isFunded,
|
||||
),
|
||||
leftButtonText: S.of(dialogContext).cancel,
|
||||
rightButtonText: S.of(dialogContext).continue_text,
|
||||
leftAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
|
||||
rightAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true),
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
accountNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocListener<CardCustomizerBloc, CardCustomizerState>(
|
||||
listenWhen: (previous, current) => previous.accountName != current.accountName,
|
||||
@@ -128,288 +179,352 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
},
|
||||
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
|
||||
builder: (context, state) {
|
||||
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
|
||||
if (state is CardCustomizerNotLoaded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return PopScope(
|
||||
child: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
spacing: 25.0,
|
||||
spacing: 25,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
|
||||
title: _isAccount ? S.of(context).edit_account : S.of(context).edit_card,
|
||||
subtitle: _isAccount ? "#${widget.account!.id + 1}" : null,
|
||||
leadingIcon:
|
||||
Icon(_isAccount ? Icons.arrow_back_ios_new : Icons.close, size: 18),
|
||||
leadingSemanticLabel:
|
||||
_isAccount ? S.of(context).seed_alert_back : S.of(context).close,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
// onTrailingPressed: () {},
|
||||
),
|
||||
if (editEnabled)
|
||||
BalanceCard(
|
||||
width: min(MediaQuery.of(context).size.width * 0.87, 768),
|
||||
selected: true,
|
||||
designSwitchDuration: const Duration(milliseconds: 300),
|
||||
accountIndex: _isAccount ? widget.account!.id + 1 : null,
|
||||
accountName: !_isAccount
|
||||
? ""
|
||||
: state.accountName.trim().isEmpty
|
||||
? S.of(context).unnamed_account
|
||||
: state.accountName,
|
||||
accountBalance: widget.account?.balance,
|
||||
balance: _isAccount ? "" : "0.00",
|
||||
fiatBalance: widget.fiatBalance ?? "",
|
||||
assetName: state.displaySats ? "sats" : widget.cryptoName,
|
||||
capitalizeAssetName: !state.displaySats,
|
||||
design: state.selectedDesign,
|
||||
),
|
||||
if (_isAccount)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).account_name),
|
||||
TextField(
|
||||
maxLength: 32,
|
||||
decoration: InputDecoration(counterText: ""),
|
||||
decoration: const InputDecoration(counterText: ""),
|
||||
onChanged: (value) {
|
||||
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
|
||||
},
|
||||
controller: accountNameController,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BalanceCard(
|
||||
width: min(MediaQuery.of(context).size.width * 0.87, 768),
|
||||
selected: true,
|
||||
designSwitchDuration: Duration(milliseconds: 300),
|
||||
accountName: editEnabled ? state.accountName : "",
|
||||
balance: "0.00",
|
||||
assetName: state.displaySats ? "sats" : widget.cryptoName,
|
||||
capitalizeAssetName: !state.displaySats,
|
||||
design: state.selectedDesign,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).card_style),
|
||||
SizedBox(
|
||||
height: 63,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableDesigns.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(CardDesignSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
color: ((index ==
|
||||
state.selectedDesignIndex)
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).card_style),
|
||||
SizedBox(
|
||||
height: 63,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableDesigns.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(CardDesignSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
color: ((index == state.selectedDesignIndex)
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Colors.transparent),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadiusGeometry.circular(12),
|
||||
),
|
||||
),
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: index == state.selectedDesignIndex ? 0.94 : 1,
|
||||
child: BalanceCard(
|
||||
width: 96,
|
||||
borderRadius: 10,
|
||||
selected: false,
|
||||
designSwitchDuration:
|
||||
const Duration(milliseconds: 300),
|
||||
design: _cardStylePreviewDesign(state, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: _showIconStylePanel(state)
|
||||
? Column(
|
||||
key: const ValueKey("icon_style_panel"),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).icon_style),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableIconPaths.length,
|
||||
separatorBuilder: (context, _) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final icon = state.availableIconPaths[index];
|
||||
final isSelected =
|
||||
index == state.selectedIconIndex;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(IconStyleSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
const Duration(milliseconds: 200),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withAlpha(64)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Colors.transparent),
|
||||
width: 1),
|
||||
borderRadius:
|
||||
BorderRadiusGeometry.circular(12),
|
||||
),
|
||||
),
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: index == state.selectedDesignIndex
|
||||
? 0.94
|
||||
: 1,
|
||||
child: BalanceCard(
|
||||
width: 96,
|
||||
borderRadius: 10,
|
||||
selected: false,
|
||||
designSwitchDuration:
|
||||
const Duration(milliseconds: 300),
|
||||
design:
|
||||
_cardStylePreviewDesign(state, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
transitionBuilder: (child, Animation<double> animation) =>
|
||||
FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: _showIconStylePanel(state)
|
||||
? Column(
|
||||
key: const ValueKey("icon_style_panel"),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).icon_style),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableIconPaths.length,
|
||||
separatorBuilder: (context, _) =>
|
||||
const SizedBox(width: 8.0),
|
||||
itemBuilder: (context, index) {
|
||||
final icon =
|
||||
state.availableIconPaths[index];
|
||||
final isSelected =
|
||||
index == state.selectedIconIndex;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(IconStyleSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
const Duration(milliseconds: 200),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(18),
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withAlpha(64)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: icon.path),
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: icon.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).color),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
spacing: 4,
|
||||
runSpacing: 8,
|
||||
children: List.generate(
|
||||
state.availableColors.length,
|
||||
(index) => Material(
|
||||
borderRadius: BorderRadius.circular(999999999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999999),
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(ColorSelected(index));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity:
|
||||
index == state.selectedColorIndex ? 1 : 0,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: index == state.selectedColorIndex
|
||||
? 0.8
|
||||
: 1,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
gradient: state.availableColors[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bloc.canHide)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 24),
|
||||
child: ListItemStyleWrapper(
|
||||
isFirstInSection: true,
|
||||
isLastInSection: true,
|
||||
onTap: _requestArchive,
|
||||
builder: (context, textStyle, labelStyle) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 40),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.5),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/archived.svg",
|
||||
width: 19,
|
||||
height: 19,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(S.of(context).color),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
spacing: 4, // space between items in a row
|
||||
runSpacing: 8,
|
||||
children: List.generate(
|
||||
state.availableColors.length,
|
||||
(index) => Material(
|
||||
borderRadius:
|
||||
BorderRadius.circular(999999999),
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(999999999),
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(ColorSelected(index));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(
|
||||
milliseconds: 200),
|
||||
opacity: index ==
|
||||
state.selectedColorIndex
|
||||
? 1
|
||||
: 0,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
99999999),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface))),
|
||||
),
|
||||
AnimatedScale(
|
||||
duration: const Duration(
|
||||
milliseconds: 200),
|
||||
scale: index ==
|
||||
state.selectedColorIndex
|
||||
? 0.8
|
||||
: 1,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
99999999,
|
||||
),
|
||||
gradient: state
|
||||
.availableColors[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
)),
|
||||
Text(
|
||||
S.of(context).archive_account,
|
||||
style: textStyle.copyWith(
|
||||
height: 18 / 14,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).archive_account_reversible,
|
||||
style: labelStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 15 / 12,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
SizedBox.square(
|
||||
dimension: 16,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_right.svg",
|
||||
width: 7,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
abstract class EducationPage extends StatefulWidget {
|
||||
const EducationPage({required this.settingsStore, super.key});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
String get educationId;
|
||||
String get title;
|
||||
String get iconPath;
|
||||
String get completionLabel;
|
||||
List<EducationSlide> get slides;
|
||||
String progressLabel(int current, int total);
|
||||
|
||||
bool get isDismissed => settingsStore.isEducationDismissed(educationId);
|
||||
|
||||
Future<void> show(BuildContext context) async {
|
||||
await showCupertinoModalBottomSheet<void>(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => this,
|
||||
);
|
||||
await settingsStore.dismissEducation(educationId);
|
||||
}
|
||||
|
||||
@override
|
||||
State<EducationPage> createState() => _EducationPageState();
|
||||
}
|
||||
|
||||
class _EducationPageState extends State<EducationPage> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
bool _isCompleting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _complete() {
|
||||
if (_isCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isCompleting = true;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _continue(int pageCount) async {
|
||||
if (_currentPage == pageCount - 1) {
|
||||
_complete();
|
||||
return;
|
||||
}
|
||||
|
||||
await _pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pages = widget.slides;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: widget.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.title, style: Theme.of(context).textTheme.headlineMedium),
|
||||
const Spacer(),
|
||||
ModernButton(
|
||||
size: 36,
|
||||
icon: const Icon(Icons.close),
|
||||
semanticLabel: S.of(context).close,
|
||||
onPressed: _complete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (page) => setState(() => _currentPage = page),
|
||||
children: pages,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label: widget.progressLabel(_currentPage + 1, pages.length),
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
pages.length,
|
||||
(index) => AnimatedContainer(
|
||||
key: ValueKey("education-dot-$index"),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index == _currentPage
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: () => _continue(pages.length),
|
||||
text: _currentPage == pages.length - 1
|
||||
? widget.completionLabel
|
||||
: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EducationSlide extends StatelessWidget {
|
||||
const EducationSlide({
|
||||
required this.children,
|
||||
super.key,
|
||||
this.distributeChildren = false,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
final bool distributeChildren;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 8),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight - 16),
|
||||
child: distributeChildren
|
||||
? IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < children.length; index++) ...[
|
||||
children[index],
|
||||
if (index < children.length - 1) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Spacer(),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class EducationText extends StatelessWidget {
|
||||
const EducationText({
|
||||
required this.text,
|
||||
super.key,
|
||||
this.highlightedText,
|
||||
this.secondary = false,
|
||||
this.warning = false,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String? highlightedText;
|
||||
final bool secondary;
|
||||
final bool warning;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: warning
|
||||
? context.customColors.warningOutlineColor
|
||||
: secondary
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
final highlight = highlightedText;
|
||||
final highlightStart = highlight == null ? -1 : text.indexOf(highlight);
|
||||
|
||||
if (highlight == null || highlightStart < 0) {
|
||||
return Text(text, textAlign: TextAlign.center, style: style);
|
||||
}
|
||||
|
||||
final highlightEnd = highlightStart + highlight.length;
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
style: style,
|
||||
children: [
|
||||
TextSpan(text: text.substring(0, highlightStart)),
|
||||
TextSpan(
|
||||
text: text.substring(highlightStart, highlightEnd),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
TextSpan(text: text.substring(highlightEnd)),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/core/utilities.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/account_confirmation_content.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/utils/show_pop_up.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class HiddenAccountsPage extends StatefulWidget {
|
||||
const HiddenAccountsPage({
|
||||
required this.accountListViewModel,
|
||||
required this.dashboardViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WalletAccountListViewModel accountListViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
State<HiddenAccountsPage> createState() => _HiddenAccountsPageState();
|
||||
}
|
||||
|
||||
class _HiddenAccountsPageState extends State<HiddenAccountsPage> {
|
||||
final List<AccountListItem> _items = [];
|
||||
final Set<int> _accountsBeingRestored = <int>{};
|
||||
bool _isLoading = true;
|
||||
|
||||
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_loadAccounts());
|
||||
}
|
||||
|
||||
Future<BalanceCardLayout> _layout() async => BalanceCardLayout.resolve(
|
||||
accountIndices: widget.accountListViewModel.accounts.map((account) => account.id).toList(),
|
||||
settings: await BalanceCardStyleSettings.getAll(_walletInfoId),
|
||||
);
|
||||
|
||||
Future<void> _loadAccounts() async {
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
final layout = await _layout();
|
||||
final hiddenAccounts = <AccountListItem>[];
|
||||
|
||||
for (final accountIndex in layout.hidden) {
|
||||
final account = accounts.firstWhereOrNull((account) => account.id == accountIndex);
|
||||
if (account != null) {
|
||||
hiddenAccounts.add(account);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_items
|
||||
..clear()
|
||||
..addAll(hiddenAccounts);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _confirmUnarchival(AccountListItem account) async {
|
||||
final isFunded = account.isFunded;
|
||||
|
||||
final result = await showPopUp<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: S.of(dialogContext).unarchive_account_title,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountSummary(account: account),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
isFunded
|
||||
? S.of(dialogContext).unarchive_account_funds_description
|
||||
: S.of(dialogContext).unarchive_account_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(dialogContext).textTheme.bodyMedium,
|
||||
),
|
||||
if (isFunded) ...[
|
||||
const SizedBox(height: 20),
|
||||
AccountFundsSummary(
|
||||
account: account,
|
||||
fiatBalance: widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account),
|
||||
borderColor: Theme.of(dialogContext).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
leftButtonText: S.of(dialogContext).cancel,
|
||||
rightButtonText: S.of(dialogContext).continue_text,
|
||||
leftAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
|
||||
rightAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true),
|
||||
),
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> _unarchive(AccountListItem account) async {
|
||||
final accountForConfirmation =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
|
||||
account;
|
||||
final confirmed = await _confirmUnarchival(accountForConfirmation);
|
||||
if (!confirmed || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _accountsBeingRestored.add(account.id));
|
||||
try {
|
||||
final restored = (await _layout()).unhiding(account.id);
|
||||
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, restored.orders);
|
||||
final latestAccount =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
|
||||
account;
|
||||
await widget.accountListViewModel.select(latestAccount);
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await _loadAccounts();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _accountsBeingRestored.remove(account.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ModalPageWrapper(
|
||||
topBar: ModalTopBar(
|
||||
title: S.of(context).archived_accounts,
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
content: _isLoading
|
||||
? const SizedBox(
|
||||
height: 320,
|
||||
child: Center(child: CupertinoActivityIndicator()),
|
||||
)
|
||||
: _items.isEmpty
|
||||
? const _EmptyArchiveView()
|
||||
: _PopulatedArchiveView(
|
||||
accounts: _items,
|
||||
accountsBeingRestored: _accountsBeingRestored,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
onUnarchive: _unarchive,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _EmptyArchiveView extends StatelessWidget {
|
||||
const _EmptyArchiveView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height - 180,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ArchiveIcon(size: 50, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).no_archived_accounts,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).no_archived_accounts_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopulatedArchiveView extends StatelessWidget {
|
||||
const _PopulatedArchiveView({
|
||||
required this.accounts,
|
||||
required this.accountsBeingRestored,
|
||||
required this.dashboardViewModel,
|
||||
required this.onUnarchive,
|
||||
});
|
||||
|
||||
final List<AccountListItem> accounts;
|
||||
final Set<int> accountsBeingRestored;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final Future<void> Function(AccountListItem account) onUnarchive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final fundedAccounts = accounts.where((account) => account.isFunded).toList(growable: false);
|
||||
final emptyAccounts =
|
||||
accounts.where((account) => !account.isFunded).toList(growable: false);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ArchiveIcon(size: 50, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 36),
|
||||
child: Text(
|
||||
S.of(context).select_account_to_unarchive,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (fundedAccounts.isNotEmpty) ...[
|
||||
_ArchiveSectionHeader(
|
||||
title: S.of(context).funded_accounts,
|
||||
description: S.of(context).funded_accounts_description,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_accountRows(context, fundedAccounts),
|
||||
],
|
||||
if (fundedAccounts.isNotEmpty && emptyAccounts.isNotEmpty) const SizedBox(height: 28),
|
||||
if (emptyAccounts.isNotEmpty) ...[
|
||||
_ArchiveSectionHeader(title: S.of(context).empty_accounts),
|
||||
const SizedBox(height: 12),
|
||||
_accountRows(context, emptyAccounts),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _accountRows(BuildContext context, List<AccountListItem> sectionAccounts) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < sectionAccounts.length; index++) ...[
|
||||
_ArchivedAccountRow(
|
||||
account: sectionAccounts[index],
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isBusy: accountsBeingRestored.contains(sectionAccounts[index].id),
|
||||
onTap: accountsBeingRestored.contains(sectionAccounts[index].id)
|
||||
? null
|
||||
: () => onUnarchive(sectionAccounts[index]),
|
||||
),
|
||||
if (index < sectionAccounts.length - 1)
|
||||
Divider(
|
||||
height: 1,
|
||||
indent: 48,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ArchivedAccountRow extends StatelessWidget {
|
||||
const _ArchivedAccountRow({
|
||||
required this.account,
|
||||
required this.dashboardViewModel,
|
||||
required this.isBusy,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool isBusy;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final label = account.label.trim();
|
||||
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${account.id + 1}. ",
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: label.isEmpty ? S.of(context).unnamed_account : label,
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_AccountBalanceTrailing(
|
||||
account: account,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isBusy: isBusy,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArchiveSectionHeader extends StatelessWidget {
|
||||
const _ArchiveSectionHeader({required this.title, this.description});
|
||||
|
||||
final String title;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (description != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountBalanceTrailing extends StatelessWidget {
|
||||
const _AccountBalanceTrailing({
|
||||
required this.account,
|
||||
required this.dashboardViewModel,
|
||||
required this.isBusy,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool isBusy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
if (isBusy) {
|
||||
return const CupertinoActivityIndicator(radius: 10);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (account.isFunded) ...[
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 142),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final fiatBalance = dashboardViewModel.balanceViewModel.accountFiatBalance(account);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
MoneyText(
|
||||
account.balance,
|
||||
fractionalDigits: account.balance.decimals,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (fiatBalance != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
fiatBalance,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArchiveIcon extends StatelessWidget {
|
||||
const _ArchiveIcon({required this.size, required this.color});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox.square(
|
||||
dimension: size,
|
||||
child: Center(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/archived.svg",
|
||||
width: size * 19 / 24,
|
||||
height: size * 19 / 24,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/accounts_promo.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:cake_wallet/new-ui/pages/seed/seed_backup_reminder_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/send_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/settings_page.dart";
|
||||
@@ -214,6 +218,14 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) &&
|
||||
widget.dashboardViewModel.accountListViewModel != null &&
|
||||
!_lightningMode)
|
||||
AccountsPromo(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
walletName: walletTypeToString(widget.dashboardViewModel.wallet.type),
|
||||
onTap: _openAccountsFromPromo,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CoinActionRow(
|
||||
@@ -264,11 +276,18 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
|
||||
);
|
||||
|
||||
Future<void> openCardCustomizer() async {
|
||||
if (!_checkReadyToManage()) return;
|
||||
await CardCustomizer.show(
|
||||
context: context,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
account: !widget.dashboardViewModel.lightningMode &&
|
||||
widget.dashboardViewModel.isMultiAccountsEnabled
|
||||
? widget.dashboardViewModel.accountListViewModel?.selectedAccount
|
||||
: null,
|
||||
lightningMode: widget.dashboardViewModel.lightningMode,
|
||||
);
|
||||
await widget.dashboardViewModel.accountListViewModel?.reload();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
void openSeedBackupReminder() {
|
||||
@@ -283,7 +302,9 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
|
||||
}
|
||||
|
||||
Future<void> openAccountCustomizer() async {
|
||||
if (widget.dashboardViewModel.accountListViewModel == null) return;
|
||||
if (widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
|
||||
|
||||
final page = getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel);
|
||||
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
@@ -292,10 +313,36 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: Material(
|
||||
child: getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel),
|
||||
child: page,
|
||||
),
|
||||
),
|
||||
);
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
Future<void> _openAccountsFromPromo() async {
|
||||
if (!AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) ||
|
||||
widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
|
||||
|
||||
await AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
).show(context);
|
||||
if (mounted) await openAccountCustomizer();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+403
-144
@@ -1,30 +1,33 @@
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import "package:cake_wallet/core/auth_service.dart";
|
||||
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:cake_wallet/new-ui/pages/coin_control_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
bool _trueFunc(DashboardViewModel _) => true;
|
||||
|
||||
bool _falseFunc(DashboardViewModel _) => false;
|
||||
|
||||
bool _isBtc(DashboardViewModel vm) => vm.wallet.type == WalletType.bitcoin;
|
||||
bool _alwaysVisible(DashboardViewModel _) => true;
|
||||
|
||||
bool _hasLightning(DashboardViewModel vm) => vm.hasLightning;
|
||||
|
||||
bool _hasSilentPayments(DashboardViewModel vm) => vm.hasSilentPayments;
|
||||
|
||||
bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
|
||||
|
||||
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
|
||||
|
||||
bool _hasAccounts(DashboardViewModel vm) => vm.wallet.hasAccountsSupport;
|
||||
|
||||
bool _hasCoinControl(DashboardViewModel vm) => vm.wallet.hasCoinControl;
|
||||
|
||||
bool _requiresKeyImageSync(DashboardViewModel vm) =>
|
||||
vm.wallet.type == WalletType.monero &&
|
||||
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
|
||||
@@ -34,16 +37,20 @@ class SettingsListItem {
|
||||
this.iconPath,
|
||||
this.title,
|
||||
this.route, {
|
||||
this.isCore = false,
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc,
|
||||
this.use2fa = _neverUse2fa,
|
||||
this.condition = _alwaysVisible,
|
||||
this.routeArgs,
|
||||
this.routeArgsBuilder,
|
||||
});
|
||||
|
||||
static bool _neverUse2fa(DashboardViewModel _) => false;
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String route;
|
||||
final bool isCore;
|
||||
final Object? routeArgs;
|
||||
final Object? Function(DashboardViewModel)? routeArgsBuilder;
|
||||
final bool requireAuth;
|
||||
@@ -52,72 +59,165 @@ class SettingsListItem {
|
||||
}
|
||||
|
||||
class SettingsSectionData {
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
const SettingsSectionData(
|
||||
this.title,
|
||||
this.titleIconPath,
|
||||
this.groups, {
|
||||
this.titleColor,
|
||||
this.headerSpacing = 16,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String titleIconPath;
|
||||
final List<SettingsListItem> items;
|
||||
final Map<String, List<ListItem>> groups;
|
||||
final Color? titleColor;
|
||||
final double headerSpacing;
|
||||
}
|
||||
|
||||
static SettingsSectionData walletSettings =
|
||||
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/accounts.svg",
|
||||
S.current.accounts,
|
||||
Routes.walletAccountsPage,
|
||||
condition: (vm) => vm.wallet.hasAccountsSupport,
|
||||
routeArgsBuilder: (vm) => vm,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/privacy.svg", S.current.privacy, Routes.privacyPage),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/seed.svg", S.current.recovery_and_keys,
|
||||
Routes.showKeysDisclaimer),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/lightning_username.svg",
|
||||
"Lightning ${S.current.username}", Routes.lightningUsernamePage,
|
||||
condition: _hasLightning),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/wc.svg", S.current.walletConnect,
|
||||
class WalletSettingsResolver {
|
||||
const WalletSettingsResolver();
|
||||
|
||||
String titleFor(WalletType walletType, S strings) =>
|
||||
'${walletTypeToString(walletType)} ${strings.settings_title}'.trim();
|
||||
|
||||
String iconPathFor(WalletType walletType) =>
|
||||
walletType == WalletType.robinhood
|
||||
? "assets/new-ui/crypto_full_icons/robinhood.svg"
|
||||
: "assets/new-ui/network_icons/${walletType.name.toLowerCase()}.svg";
|
||||
|
||||
List<List<SettingsListItem>> resolveSections(DashboardViewModel viewModel) {
|
||||
final visibleItems = <SettingsListItem>[
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/accounts.svg",
|
||||
viewModel.wallet.type == WalletType.bitcoin ? S.current.accounts_onchain : S.current.accounts,
|
||||
Routes.walletAccountsPage,
|
||||
isCore: true,
|
||||
condition: _hasAccounts,
|
||||
routeArgsBuilder: (vm) => vm,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/nodes.svg",
|
||||
S.current.nodes,
|
||||
Routes.manageNodes,
|
||||
isCore: true,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/coin-control.svg",
|
||||
S.current.coin_control_settings,
|
||||
Routes.unspentCoinsList,
|
||||
isCore: true,
|
||||
condition: _hasCoinControl,
|
||||
routeArgs: const CoinControlPageArgs(canEdit: false),
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/lightning_username.svg",
|
||||
"Lightning ${S.current.username}",
|
||||
Routes.lightningUsernamePage,
|
||||
condition: _hasLightning,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/silent-payments.svg",
|
||||
S.current.silent_payments,
|
||||
Routes.silentPaymentsSettings,
|
||||
condition: _hasSilentPayments,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/mweb.svg",
|
||||
S.current.litecoin_mweb,
|
||||
Routes.mwebSettings,
|
||||
condition: _hasMweb,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/wc.svg",
|
||||
S.current.walletConnect,
|
||||
Routes.walletConnectConnectionsListing,
|
||||
condition: _hasWalletConnect),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/sync-balance.svg",
|
||||
S.current.resync_device,
|
||||
Routes.syncKeyImagesDevices,
|
||||
routeArgs: {'export-outputs': 'export-outputs'},
|
||||
condition: _requiresKeyImageSync,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
|
||||
]);
|
||||
condition: _hasWalletConnect,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/sync-balance.svg",
|
||||
S.current.resync_device,
|
||||
Routes.syncKeyImagesDevices,
|
||||
routeArgs: const {"export-outputs": "export-outputs"},
|
||||
condition: _requiresKeyImageSync,
|
||||
),
|
||||
].where((item) => item.condition(viewModel)).toList();
|
||||
|
||||
static SettingsSectionData appSettings =
|
||||
SettingsSectionData(S.current.app_settings, "assets/new-ui/cake-setting.svg", [
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/connections.svg", S.current.connections,
|
||||
Routes.connectionSync),
|
||||
// SettingsListItem("assets/new-ui/settings_row_icons/defaults.svg", "Defaults", ""),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/display.svg", S.current.display,
|
||||
Routes.displaySettingsPage),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/security.svg", S.current.security,
|
||||
Routes.securityBackupPage),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/backup.svg", S.current.backup, Routes.backup,
|
||||
requireAuth: true,
|
||||
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
|
||||
]);
|
||||
final coreItems = visibleItems.where((item) => item.isCore).toList();
|
||||
final featureItems = visibleItems.where((item) => !item.isCore).toList();
|
||||
|
||||
static SettingsSectionData otherSettings = SettingsSectionData("", "", [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/support.svg", S.current.settings_support, Routes.support),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/info.svg", S.current.about, Routes.aboutPage),
|
||||
]);
|
||||
return [
|
||||
if (coreItems.isNotEmpty) coreItems,
|
||||
if (featureItems.isNotEmpty) featureItems,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
static List<SettingsSectionData> all = [walletSettings, appSettings, otherSettings];
|
||||
class SettingsPageSectionsResolver {
|
||||
const SettingsPageSectionsResolver();
|
||||
|
||||
List<SettingsListItem> walletGeneral(S strings) => [
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/privacy.svg',
|
||||
strings.privacy,
|
||||
Routes.privacyPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/seed.svg',
|
||||
strings.recovery_and_keys,
|
||||
Routes.showKeysDisclaimer,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/other.svg',
|
||||
strings.other,
|
||||
Routes.otherSettingsPage,
|
||||
),
|
||||
];
|
||||
|
||||
List<SettingsListItem> appSettings() => [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/connections.svg",
|
||||
S.current.connections,
|
||||
Routes.connectionSync,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/display.svg",
|
||||
S.current.display,
|
||||
Routes.displaySettingsPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/security.svg",
|
||||
S.current.security,
|
||||
Routes.securityBackupPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/backup.svg",
|
||||
S.current.backup,
|
||||
Routes.backup,
|
||||
requireAuth: true,
|
||||
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
),
|
||||
];
|
||||
|
||||
List<SettingsListItem> supportAndAbout(S strings) => [
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/support.svg',
|
||||
strings.settings_support,
|
||||
Routes.support,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/info.svg',
|
||||
strings.about,
|
||||
Routes.aboutPage,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class NewSettingsPage extends StatelessWidget {
|
||||
const NewSettingsPage({super.key, required this.dashboardViewModel, required this.authService});
|
||||
const NewSettingsPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
@@ -125,89 +225,248 @@ class NewSettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ModalNavigator(
|
||||
parentContext: context,
|
||||
rootPage: SettingsMainPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
));
|
||||
parentContext: context,
|
||||
rootPage: SettingsMainPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsMainPage extends StatelessWidget {
|
||||
const SettingsMainPage({super.key, required this.dashboardViewModel, required this.authService});
|
||||
const SettingsMainPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
static const _walletSettingsResolver = WalletSettingsResolver();
|
||||
static const _sectionsResolver = SettingsPageSectionsResolver();
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).settings_title,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: ModalScrollController.of(context),
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
|
||||
children: [
|
||||
for (final (index, section) in _sections(context).indexed) ...[
|
||||
if (index > 0) ...[
|
||||
const SizedBox(height: 24),
|
||||
Divider(color: Theme.of(context).colorScheme.surfaceContainerHighest),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_SettingsSectionHeader(
|
||||
iconPath: section.titleIconPath,
|
||||
title: section.title,
|
||||
foregroundColor: section.titleColor,
|
||||
),
|
||||
SizedBox(height: section.headerSpacing),
|
||||
for (final (groupIndex, group) in section.groups.entries.indexed) ...[
|
||||
if (groupIndex > 0) const SizedBox(height: 24),
|
||||
NewListSections(sections: {group.key: group.value}),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
List<SettingsSectionData> _sections(BuildContext context) => [
|
||||
SettingsSectionData(
|
||||
dashboardViewModel.wallet.name,
|
||||
"assets/new-ui/wallet-setting.svg",
|
||||
{
|
||||
"wallet_type_settings": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "wallet_type_settings",
|
||||
label: _walletSettingsResolver.titleFor(
|
||||
dashboardViewModel.wallet.type,
|
||||
S.of(context),
|
||||
),
|
||||
iconPath: _walletSettingsResolver.iconPathFor(dashboardViewModel.wallet.type),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => WalletSettingsPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
"wallet_general": _buildRows(context, _sectionsResolver.walletGeneral(S.of(context))),
|
||||
},
|
||||
titleColor: Theme.of(context).colorScheme.primary,
|
||||
headerSpacing: 20,
|
||||
),
|
||||
SettingsSectionData(
|
||||
S.of(context).app_settings,
|
||||
"assets/new-ui/cake-setting.svg",
|
||||
{
|
||||
"app_settings": _buildRows(context, _sectionsResolver.appSettings()),
|
||||
"support_and_about":
|
||||
_buildRows(context, _sectionsResolver.supportAndAbout(S.of(context))),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
List<ListItem> _buildRows(BuildContext context, List<SettingsListItem> items) => items
|
||||
.where((item) => item.condition(dashboardViewModel))
|
||||
.map((item) => _buildRow(context, item))
|
||||
.toList();
|
||||
|
||||
ListItemRegularRow _buildRow(BuildContext context, SettingsListItem item) => ListItemRegularRow(
|
||||
keyValue: item.route,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () => _openItem(context, item),
|
||||
);
|
||||
|
||||
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
|
||||
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
|
||||
if (item.requireAuth) {
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route,
|
||||
arguments: arguments,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class WalletSettingsPage extends StatelessWidget {
|
||||
const WalletSettingsPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
static const _resolver = WalletSettingsResolver();
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<ListItem> buildItems(SettingsSectionData section) => section.items
|
||||
.map((item) => item.condition(dashboardViewModel)
|
||||
? ListItemRegularRow(
|
||||
keyValue: item.route,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () {
|
||||
if (item.route.isNotEmpty) {
|
||||
if (item.requireAuth) {
|
||||
authService.authenticateAction(context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(
|
||||
item.route,
|
||||
arguments:
|
||||
item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
: null)
|
||||
.whereType<ListItem>()
|
||||
.toList();
|
||||
final strings = S.of(context);
|
||||
final sections = _resolver.resolveSections(dashboardViewModel);
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).settings_title,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
onTrailingPressed: () {},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(controller: ModalScrollController.of(context), children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
children: SettingsSectionData.all.expand((section) {
|
||||
return [
|
||||
if (section.titleIconPath.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: section.titleIconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
)),
|
||||
Text(section.title, style: Theme.of(context).textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
NewListSections(sections: {section.title: buildItems(section)}),
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _resolver.titleFor(dashboardViewModel.wallet.type, strings),
|
||||
titleLeadingWidget: CakeImageWidget(
|
||||
imageUrl: _resolver.iconPathFor(dashboardViewModel.wallet.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: strings.seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: ModalScrollController.of(context),
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
|
||||
children: [
|
||||
for (var index = 0; index < sections.length; index++) ...[
|
||||
if (index > 0) const SizedBox(height: 24),
|
||||
NewListSections(
|
||||
sections: {
|
||||
'wallet_settings_$index': sections[index]
|
||||
.map(
|
||||
(item) => ListItemRegularRow(
|
||||
keyValue: item.route,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () => _openItem(context, item),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
|
||||
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
|
||||
if (item.requireAuth) {
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route,
|
||||
arguments: arguments,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSectionHeader extends StatelessWidget {
|
||||
const _SettingsSectionHeader({
|
||||
required this.iconPath,
|
||||
required this.title,
|
||||
this.foregroundColor,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final Color? foregroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: iconPath,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import "dart:async";
|
||||
import "dart:math";
|
||||
import "dart:ui";
|
||||
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/core/utilities.dart";
|
||||
import "package:cake_wallet/core/amount_parsing_proxy.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:cake_wallet/utils/show_pop_up.dart";
|
||||
import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/pages/hidden_accounts.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/card_design.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:mobx/mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class AccountCustomizerListItem {
|
||||
AccountCustomizerListItem({
|
||||
const AccountCustomizerListItem({
|
||||
required this.card,
|
||||
required this.order,
|
||||
required this.accountListItem,
|
||||
required this.settings,
|
||||
});
|
||||
|
||||
final BalanceCard card;
|
||||
final int order;
|
||||
final AccountListItem accountListItem;
|
||||
final BalanceCardStyleSettings? settings;
|
||||
}
|
||||
|
||||
class WalletAccountsPage extends StatefulWidget {
|
||||
@@ -55,315 +59,229 @@ class WalletAccountsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _WalletAccountsPageState extends State<WalletAccountsPage> {
|
||||
static const double _kStackVisibleFactor = 0.2;
|
||||
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
|
||||
|
||||
final List<AccountCustomizerListItem> _items = [];
|
||||
|
||||
bool _hasArchivedAccounts = false;
|
||||
bool _loading = true;
|
||||
ReactionDisposer? _accountsReaction;
|
||||
|
||||
WalletAccountListViewModel get accountListViewModel =>
|
||||
widget.dashboardViewModel.accountListViewModel ?? widget.accountListViewModel;
|
||||
|
||||
bool get _isMultiAccountsEnabled =>
|
||||
widget.dashboardViewModel.isMultiAccountsEnabled;
|
||||
|
||||
String get _assetName => widget.dashboardViewModel.appStore.amountParsingProxy
|
||||
.getCryptoSymbol(accountListViewModel.currency);
|
||||
bool get _isMultiAccountsEnabled => widget.dashboardViewModel.isMultiAccountsEnabled;
|
||||
|
||||
String get _assetName => AmountParsingProxy(
|
||||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi,
|
||||
).getCryptoSymbol(widget.accountListViewModel.currency);
|
||||
|
||||
bool get _capitalizeAssetName =>
|
||||
widget.dashboardViewModel.wallet.type != WalletType.bitcoin ||
|
||||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
|
||||
BitcoinAmountDisplayMode.satoshi;
|
||||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
|
||||
BitcoinAmountDisplayMode.satoshi;
|
||||
|
||||
double get cardWidth => min(MediaQuery.sizeOf(context).width * 0.9, 768);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_accountsReaction = reaction(
|
||||
(_) => accountListViewModel.accounts
|
||||
(_) => widget.accountListViewModel.accounts
|
||||
.map((account) => "${account.id}:${account.label}:${account.balance}")
|
||||
.join(","),
|
||||
(_) {
|
||||
if (!mounted) return;
|
||||
loadCards();
|
||||
if (mounted) unawaited(loadCards());
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => unawaited(_initialize()));
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
loadCards();
|
||||
Future<void> _initialize() async {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
await _bringActiveAccountToFront();
|
||||
if (!mounted || !_isMultiAccountsEnabled || _items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isMultiAccountsEnabled) return;
|
||||
|
||||
final activeId = accountListViewModel.selectedAccount?.id;
|
||||
|
||||
for (int i = 0; i < _items.length - 1; i++) {
|
||||
if (activeId != null && _items[i].accountListItem.id == activeId) {
|
||||
final lastIndex = _items.length - 1;
|
||||
final temp = _items[i];
|
||||
_items[i] = _items[lastIndex];
|
||||
_items[lastIndex] = temp;
|
||||
|
||||
await saveCardOrder();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
loadCards();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
final educationPage = AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
);
|
||||
if (!educationPage.isDismissed) {
|
||||
await educationPage.show(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accountsReaction?.call();
|
||||
accountListViewModel.reload();
|
||||
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadCards() {
|
||||
final allAccounts = accountListViewModel.accounts;
|
||||
final visible = widget.dashboardViewModel.visibleAccounts;
|
||||
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
|
||||
|
||||
if (visible.isEmpty && _items.isNotEmpty) return;
|
||||
Future<void> _bringActiveAccountToFront() async {
|
||||
await loadCards();
|
||||
|
||||
_items.clear();
|
||||
|
||||
if (!_isMultiAccountsEnabled) {
|
||||
if (visible.isNotEmpty) {
|
||||
final account = visible.first;
|
||||
final realIndex = allAccounts.indexWhere((a) => a.id == account.id);
|
||||
final design = (realIndex >= 0 &&
|
||||
realIndex < widget.dashboardViewModel.cardDesigns.length)
|
||||
? widget.dashboardViewModel.cardDesigns[realIndex]
|
||||
: CardDesign.genericDefault;
|
||||
|
||||
_items.add(AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: account.label,
|
||||
accountIndex: account.id,
|
||||
balance: account.balance ?? "0.00",
|
||||
accountBalance: account.balance ?? "0.00",
|
||||
designSwitchDuration: Duration.zero,
|
||||
assetName: _assetName,
|
||||
capitalizeAssetName: _capitalizeAssetName,
|
||||
selected: true,
|
||||
width: cardWidth,
|
||||
design: design,
|
||||
),
|
||||
order: realIndex >= 0 ? realIndex : 0,
|
||||
accountListItem: account,
|
||||
));
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
final activeAccount =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((account) => account.isSelected);
|
||||
if (activeAccount == null || _items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < allAccounts.length; i++) {
|
||||
final realIndex = widget.dashboardViewModel.cardOrder[i];
|
||||
final index = _items.indexWhere((item) => item.accountListItem.id == activeAccount.id);
|
||||
if (index == -1 || index == _items.length - 1 || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (realIndex == null || realIndex < 0 || realIndex >= allAccounts.length) {
|
||||
// db order broken.
|
||||
reset();
|
||||
break;
|
||||
reorder(index, _items.length);
|
||||
await saveCardOrder();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
Future<void> loadCards() async {
|
||||
if (!mounted) return;
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
final unnamedAccount = S.of(context).unnamed_account;
|
||||
final resolvedCardWidth = cardWidth;
|
||||
final walletCurrency = widget.dashboardViewModel.wallet.currency;
|
||||
final styleSettings = await BalanceCardStyleSettings.getAll(_walletInfoId);
|
||||
final layout = BalanceCardLayout.resolve(
|
||||
accountIndices: accounts.map((account) => account.id).toList(),
|
||||
settings: styleSettings,
|
||||
);
|
||||
|
||||
final List<AccountCustomizerListItem> newItems = [];
|
||||
for (int position = 0; position < layout.visible.length; position++) {
|
||||
final accountIndex = layout.visible[position];
|
||||
final account = accounts.firstWhereOrNull((item) => item.id == accountIndex);
|
||||
|
||||
if (account == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_items.add(AccountCustomizerListItem(
|
||||
final setting = layout.settingFor(accountIndex);
|
||||
final isFrontCard = position == layout.visible.length - 1;
|
||||
final accountLabel = account.label.trim().isEmpty ? unnamedAccount : account.label;
|
||||
|
||||
newItems.add(
|
||||
AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: allAccounts[realIndex].label,
|
||||
accountIndex: allAccounts[realIndex].id,
|
||||
balance: allAccounts[realIndex].balance ?? "0.00",
|
||||
accountBalance: allAccounts[realIndex].balance ?? "0.00",
|
||||
accountName: "${account.id + 1}. $accountLabel",
|
||||
accountBalance: account.balance,
|
||||
fiatBalance:
|
||||
widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account) ?? "",
|
||||
designSwitchDuration: Duration.zero,
|
||||
assetName: _assetName,
|
||||
capitalizeAssetName: _capitalizeAssetName,
|
||||
selected: i == allAccounts.length - 1,
|
||||
width: cardWidth,
|
||||
design: widget.dashboardViewModel.cardDesigns[realIndex],
|
||||
onCustomizeTapped: isFrontCard ? _openCardCustomizer : null,
|
||||
selected: isFrontCard,
|
||||
width: resolvedCardWidth,
|
||||
design: CardDesign.fromStyleSettings(setting, walletCurrency),
|
||||
),
|
||||
order: realIndex,
|
||||
accountListItem: allAccounts[realIndex]));
|
||||
accountListItem: account,
|
||||
settings: setting,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
setState(() {
|
||||
_items
|
||||
..clear()
|
||||
..addAll(newItems);
|
||||
_hasArchivedAccounts = layout.hidden.isNotEmpty;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (layout.needsRepair) {
|
||||
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, layout.orders);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showResetButton = _isMultiAccountsEnabled && _items.length > 1;
|
||||
final showAccountsToggle = widget.dashboardViewModel.canToggleMultiAccounts;
|
||||
final isToggleEnabled = widget.dashboardViewModel.isMultiAccountsEnabled;
|
||||
|
||||
if (_items.isEmpty && !showAccountsToggle) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).wallet_accounts,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: showResetButton ? const Icon(Icons.refresh) : null,
|
||||
trailingSemanticLabel: S.of(context).reset,
|
||||
onTrailingPressed: showResetDialog,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (showAccountsToggle)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: NewListSections(
|
||||
sections: {
|
||||
"": [
|
||||
ListItemToggle(
|
||||
keyValue: S.of(context).multiple_accounts,
|
||||
label: S.of(context).multiple_accounts,
|
||||
value: isToggleEnabled,
|
||||
onChanged: (val) async {
|
||||
await widget.dashboardViewModel.setMultiAccountsEnabled(val);
|
||||
if (!mounted) return;
|
||||
loadCards();
|
||||
}),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isMultiAccountsEnabled && _items.isNotEmpty)
|
||||
Expanded(
|
||||
child: Column(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).accounts,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingWidget: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Text(
|
||||
S.of(context).account_customizer_desc,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
ModernButton(
|
||||
semanticLabel: S.of(context).accounts_help,
|
||||
icon: const Icon(Icons.question_mark),
|
||||
size: 36,
|
||||
iconSize: 19,
|
||||
onPressed: () => AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
).show(context),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 196),
|
||||
scrollController: ModalScrollController.of(context),
|
||||
onReorder: reorder,
|
||||
proxyDecorator: (child, index, animation) => 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: (context, int index) {
|
||||
final card = _items[index].card;
|
||||
final selectedItemIndex = _items.length - 1;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(index),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selectedItemIndex == index,
|
||||
label: _items[index].accountListItem.label,
|
||||
onTap: () => reorder(index, _items.length),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: () => reorder(index, _items.length),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
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),
|
||||
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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_isMultiAccountsEnabled)
|
||||
ModernButton.svg(
|
||||
semanticLabel: S.of(context).archived_accounts,
|
||||
svgPath: "assets/new-ui/archived.svg",
|
||||
size: 36,
|
||||
iconSize: 19,
|
||||
backgroundColor:
|
||||
_hasArchivedAccounts ? Theme.of(context).colorScheme.primary : null,
|
||||
iconColor:
|
||||
_hasArchivedAccounts ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
onPressed: _openArchivedAccounts,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.dashboardViewModel.canToggleMultiAccounts)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: NewListSections(sections: {
|
||||
"": [
|
||||
ListItemToggle(
|
||||
keyValue: S.of(context).multiple_accounts,
|
||||
label: S.of(context).multiple_accounts,
|
||||
value: _isMultiAccountsEnabled,
|
||||
onChanged: (value) async {
|
||||
await widget.dashboardViewModel.setMultiAccountsEnabled(value);
|
||||
if (mounted) await _initialize();
|
||||
},
|
||||
),
|
||||
],
|
||||
}),
|
||||
),
|
||||
if (_isMultiAccountsEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 18),
|
||||
child: Text(
|
||||
S.of(context).account_customizer_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: !_isMultiAccountsEnabled
|
||||
? const SizedBox.shrink()
|
||||
: _loading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: _items.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _AccountCards(
|
||||
items: _items,
|
||||
cardWidth: cardWidth,
|
||||
onReorder: reorder,
|
||||
onAddAccount: _showAddAccountModal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
bool _checkReadyToManage() {
|
||||
if (widget.dashboardViewModel.wallet.type == WalletType.bitcoin) {
|
||||
return true;
|
||||
}
|
||||
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -380,33 +298,48 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
|
||||
}
|
||||
|
||||
Future<void> _showAddAccountModal() async {
|
||||
if (!_checkReadyToManage()) return;
|
||||
|
||||
final createViewModel = getIt.get<WalletAccountEditOrCreateViewModel>();
|
||||
|
||||
final res = await showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Material(
|
||||
child: AccountCreationModal(
|
||||
viewModel: createViewModel,
|
||||
),
|
||||
));
|
||||
|
||||
if (res != null && res is bool && res == true) {
|
||||
await accountListViewModel.reload();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final accounts = accountListViewModel.accounts;
|
||||
if (accounts.isNotEmpty) {
|
||||
final newestAccount = accounts.reduce((a, b) => a.id > b.id ? a : b);
|
||||
await accountListViewModel.select(newestAccount);
|
||||
}
|
||||
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
loadCards();
|
||||
if (mounted) setState(() {});
|
||||
if (!_checkReadyToManage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final modal = AccountCreationModal(viewModel: widget.accountEditOrCreateViewModel);
|
||||
final res = await showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
expand: true,
|
||||
enableDrag: false,
|
||||
builder: (_) => Material(child: modal),
|
||||
);
|
||||
if (res == true && mounted) {
|
||||
await widget.accountListViewModel.reload();
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
if (accounts.isNotEmpty) {
|
||||
await widget.accountListViewModel.select(
|
||||
accounts.reduce((a, b) => a.id > b.id ? a : b),
|
||||
);
|
||||
}
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await loadCards();
|
||||
await saveCardOrder();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openArchivedAccounts() async {
|
||||
await Navigator.of(context).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
child: HiddenAccountsPage(
|
||||
accountListViewModel: widget.accountListViewModel,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
Future<void> _openCardCustomizer() async {
|
||||
@@ -414,49 +347,34 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
await CardCustomizer.show(
|
||||
final account = _items.last.accountListItem;
|
||||
await widget.accountListViewModel.select(account);
|
||||
if (!mounted) return;
|
||||
|
||||
final archived = await CardCustomizer.show(
|
||||
context: context,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
lightningMode: widget.dashboardViewModel.lightningMode,
|
||||
asModalSheet: false,
|
||||
onSaved: _reloadAccountsAndCards,
|
||||
account: account,
|
||||
canHide: _items.length > 1,
|
||||
asPage: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reloadAccountsAndCards() async {
|
||||
await accountListViewModel.reload();
|
||||
if (archived && _items.length > 1) {
|
||||
final nextAccount = _items[_items.length - 2].accountListItem;
|
||||
await widget.accountListViewModel.select(
|
||||
widget.accountListViewModel.accounts
|
||||
.firstWhereOrNull((item) => item.id == nextAccount.id) ??
|
||||
nextAccount,
|
||||
);
|
||||
}
|
||||
await widget.accountListViewModel.reload();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
loadCards();
|
||||
}
|
||||
|
||||
void _onCardsReordered(int oldRealIndex, int newRealIndex) {
|
||||
final accounts = accountListViewModel.accounts;
|
||||
if (oldRealIndex < 0 ||
|
||||
newRealIndex < 0 ||
|
||||
oldRealIndex >= accounts.length ||
|
||||
newRealIndex >= accounts.length) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldAccountId = accounts[oldRealIndex].id;
|
||||
final newAccountId = accounts[newRealIndex].id;
|
||||
|
||||
final from = _items.indexWhere((item) => item.accountListItem.id == oldAccountId);
|
||||
final to = _items.indexWhere((item) => item.accountListItem.id == newAccountId);
|
||||
if (from < 0 || to < 0 || from == to) return;
|
||||
|
||||
final insertIndex = from < to ? to + 1 : to;
|
||||
reorder(from, insertIndex);
|
||||
|
||||
saveCardOrder().then((_) {
|
||||
if (!mounted) return;
|
||||
widget.dashboardViewModel.loadCardDesigns();
|
||||
});
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
void reorder(int oldIndex, int newIndex) {
|
||||
if (!_isMultiAccountsEnabled) return;
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
@@ -468,100 +386,159 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
|
||||
// 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,
|
||||
capitalizeAssetName: _items[i].card.capitalizeAssetName,
|
||||
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);
|
||||
card: BalanceCard(
|
||||
accountName: _items[i].card.accountName,
|
||||
balance: _items[i].card.balance,
|
||||
accountIndex: _items[i].card.accountIndex,
|
||||
accountBalance: _items[i].card.accountBalance,
|
||||
fiatBalance: _items[i].card.fiatBalance,
|
||||
assetName: _items[i].card.assetName,
|
||||
capitalizeAssetName: _items[i].card.capitalizeAssetName,
|
||||
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,
|
||||
),
|
||||
accountListItem: _items[i].accountListItem,
|
||||
settings: _items[i].settings,
|
||||
);
|
||||
}
|
||||
|
||||
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
|
||||
accountListViewModel.select(_items[_items.length - 1].accountListItem);
|
||||
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveCardOrder() async {
|
||||
|
||||
if (!_isMultiAccountsEnabled) return;
|
||||
|
||||
for (int orderIndex = 0; orderIndex < _items.length; orderIndex++) {
|
||||
final item = _items[orderIndex];
|
||||
printV("${item.accountListItem.id}: $orderIndex");
|
||||
|
||||
final existing = await BalanceCardStyleSettings.get(
|
||||
widget.dashboardViewModel.wallet.walletInfo.internalId,
|
||||
item.accountListItem.id,
|
||||
);
|
||||
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: widget.dashboardViewModel.wallet.walletInfo.internalId,
|
||||
accountIndex: item.accountListItem.id,
|
||||
cardOrder: orderIndex,
|
||||
design: item.card.design,
|
||||
iconStyleIndex: existing?.iconStyleIndex ?? 0,
|
||||
gradientIndexOverride: existing?.gradientIndex)
|
||||
.insert();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showResetDialog() async {
|
||||
final res = await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => 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 = accountListViewModel.accounts;
|
||||
for (int i = 0; i < 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: _assetName,
|
||||
capitalizeAssetName: _capitalizeAssetName,
|
||||
selected: i == accounts.length - 1,
|
||||
designSwitchDuration: const Duration(milliseconds: 200),
|
||||
width: cardWidth,
|
||||
design: i >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[i],
|
||||
),
|
||||
order: i,
|
||||
accountListItem: accounts[i]));
|
||||
}
|
||||
|
||||
await saveCardOrder();
|
||||
|
||||
if (accounts.isNotEmpty) {
|
||||
await accountListViewModel.select(accounts.last);
|
||||
}
|
||||
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (mounted) setState(() {});
|
||||
await BalanceCardStyleSettings.setOrder(_walletInfoId, {
|
||||
for (int position = 0; position < _items.length; position++)
|
||||
_items[position].accountListItem.id: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountCards extends StatelessWidget {
|
||||
const _AccountCards({
|
||||
required this.items,
|
||||
required this.cardWidth,
|
||||
required this.onReorder,
|
||||
required this.onAddAccount,
|
||||
});
|
||||
|
||||
static const double _kStackVisibleFactor = 0.2;
|
||||
|
||||
final List<AccountCustomizerListItem> items;
|
||||
final double cardWidth;
|
||||
final ReorderCallback onReorder;
|
||||
final VoidCallback onAddAccount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
children: [
|
||||
ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 196),
|
||||
scrollController: ModalScrollController.of(context),
|
||||
onReorder: onReorder,
|
||||
proxyDecorator: (child, index, animation) => 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: (context, index) {
|
||||
final selectedItemIndex = items.length - 1;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(items[index].accountListItem.id),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selectedItemIndex == index,
|
||||
label: "${items[index].accountListItem.id + 1}. "
|
||||
"${items[index].accountListItem.label.trim().isEmpty ? S.of(context).unnamed_account : items[index].accountListItem.label}",
|
||||
onTap: () => onReorder(index, items.length),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: () {
|
||||
onReorder(index, items.length);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: items[index].card,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
onTap: onAddAccount,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import "package:cake_wallet/monero/monero.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/src/painting/gradient.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import "package:flutter/src/painting/gradient.dart";
|
||||
import "package:meta/meta.dart";
|
||||
|
||||
part 'card_customizer_event.dart';
|
||||
part 'card_customizer_state.dart';
|
||||
part "card_customizer_event.dart";
|
||||
part "card_customizer_state.dart";
|
||||
|
||||
class CardCustomizerBlocParams {
|
||||
CardCustomizerBlocParams({required this.lightningMode, required this.canHide, this.amountDisplayMode});
|
||||
|
||||
final bool lightningMode;
|
||||
final BitcoinAmountDisplayMode? amountDisplayMode;
|
||||
final bool canHide;
|
||||
}
|
||||
|
||||
class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState> {
|
||||
final WalletBase _wallet;
|
||||
final bool lightningMode;
|
||||
final bool displaySats;
|
||||
|
||||
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false})
|
||||
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false, this.canHide = false})
|
||||
: super(CardCustomizerNotLoaded(
|
||||
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) {
|
||||
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0,),) {
|
||||
on<_Init>(_init);
|
||||
on<CardDesignSelected>(_onDesignSelected);
|
||||
on<ColorSelected>(_onColorSelected);
|
||||
on<IconStyleSelected>(_onIconStyleSelected);
|
||||
on<DesignSaved>(_onDesignSaved);
|
||||
on<AccountHidden>(_onAccountHidden);
|
||||
on<AccountNameChanged>(_onAccountNameChanged);
|
||||
|
||||
add(_Init());
|
||||
}
|
||||
|
||||
bool get _accountNameEnabled =>
|
||||
!lightningMode && _wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
final WalletBase _wallet;
|
||||
final bool lightningMode;
|
||||
final bool displaySats;
|
||||
final bool canHide;
|
||||
|
||||
bool get _accountNameEnabled => !lightningMode &&
|
||||
(_wallet.hasNativeAccounts || _wallet.walletInfo.isMultiAccountsEnabled == true);
|
||||
|
||||
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
|
||||
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
|
||||
@@ -42,8 +54,21 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) async {
|
||||
return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex));
|
||||
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) =>
|
||||
BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex);
|
||||
|
||||
Future<int> _cardOrderFor(BalanceCardStyleSettings? currentDesignSettings) async {
|
||||
if (currentDesignSettings != null) {
|
||||
return currentDesignSettings.cardOrder;
|
||||
}
|
||||
|
||||
var highest = -1;
|
||||
for (final setting in await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId)) {
|
||||
if (setting.cardOrder > highest) {
|
||||
highest = setting.cardOrder;
|
||||
}
|
||||
}
|
||||
return highest + 1;
|
||||
}
|
||||
|
||||
List<CardDesign> _initAvailableDesigns({bool lightningMode = false}) {
|
||||
@@ -53,8 +78,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
ret.add(CardDesign.gradientOnlyDesign);
|
||||
ret.add(CardDesign.forCurrencyIcon(curr));
|
||||
|
||||
if (CardDesign.specialDesignsForCurrencies[curr] != null)
|
||||
if (CardDesign.specialDesignsForCurrencies[curr] != null) {
|
||||
ret.add(CardDesign.forCurrencySpecial(curr));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -76,8 +102,12 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
BalanceCardStyleSettings? settings,
|
||||
List<CardIconPath> availableIconPaths,
|
||||
) {
|
||||
if (settings == null || availableIconPaths.isEmpty) return 0;
|
||||
if (settings.iconStyleIndex >= availableIconPaths.length) return 0;
|
||||
if (settings == null || availableIconPaths.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
if (settings.iconStyleIndex >= availableIconPaths.length) {
|
||||
return 0;
|
||||
}
|
||||
return settings.iconStyleIndex;
|
||||
}
|
||||
|
||||
@@ -86,7 +116,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
return ret == -1 ? CardDesign.allGradients.length : ret;
|
||||
}
|
||||
|
||||
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
|
||||
Future<void> _init(_Init event, Emitter<CardCustomizerState> emit) async {
|
||||
final accountInfo = await _getCurrentAccountInfo();
|
||||
final accountName = accountInfo.accountName;
|
||||
final accountIndex = accountInfo.accountIndex;
|
||||
@@ -108,9 +138,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
accountName,
|
||||
accountIndex,
|
||||
displaySats,
|
||||
currentDesignSettings?.cardOrder ?? 0,
|
||||
await _cardOrderFor(currentDesignSettings),
|
||||
availableIconPaths: availableIconPaths,
|
||||
selectedIconIndex: selectedIconIndex));
|
||||
selectedIconIndex: selectedIconIndex,),);
|
||||
}
|
||||
|
||||
Future<({String accountName, int accountIndex})> _getCurrentAccountInfo() async {
|
||||
@@ -134,7 +164,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.wownero) {
|
||||
final account = wownero!.getCurrentAccount(_wallet);
|
||||
final account = wow.wownero!.getCurrentAccount(_wallet);
|
||||
return (accountName: account.label, accountIndex: account.id);
|
||||
}
|
||||
|
||||
@@ -160,7 +190,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
emit(state.copyWith(
|
||||
selectedDesignIndex: event.newDesignIndex,
|
||||
availableColors: newColors,
|
||||
selectedColorIndex: newColorIndex));
|
||||
selectedColorIndex: newColorIndex,),);
|
||||
}
|
||||
|
||||
void _onColorSelected(ColorSelected event, Emitter<CardCustomizerState> emit) {
|
||||
@@ -176,18 +206,20 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
}
|
||||
|
||||
Future<void> _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) async {
|
||||
final currentSettings = await _loadCurrentDesignSettings(state.accountIndex);
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: state.accountIndex,
|
||||
cardOrder: state.cardOrder,
|
||||
design: state.selectedDesign,
|
||||
iconStyleIndex: state.selectedIconIndex,
|
||||
gradientIndexOverride: state.selectedColorIndex)
|
||||
.insert();
|
||||
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: state.accountIndex,
|
||||
cardOrder: state.cardOrder,
|
||||
design: state.selectedDesign,
|
||||
iconStyleIndex: state.selectedIconIndex,
|
||||
gradientIndexOverride: state.selectedColorIndex,
|
||||
hidden: currentSettings?.hidden ?? false,
|
||||
).insert();
|
||||
await saveAccountName();
|
||||
|
||||
emit(CardCustomizerSaved(
|
||||
emit(
|
||||
CardCustomizerSaved(
|
||||
state.selectedDesignIndex,
|
||||
state.selectedColorIndex,
|
||||
state.availableDesigns,
|
||||
@@ -197,7 +229,34 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
state.displaySats,
|
||||
state.cardOrder,
|
||||
availableIconPaths: state.availableIconPaths,
|
||||
selectedIconIndex: state.selectedIconIndex));
|
||||
selectedIconIndex: state.selectedIconIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAccountHidden(AccountHidden event, Emitter<CardCustomizerState> emit) async {
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: state.accountIndex,
|
||||
cardOrder: state.cardOrder,
|
||||
design: state.selectedDesign,
|
||||
iconStyleIndex: state.selectedIconIndex,
|
||||
gradientIndexOverride: state.selectedColorIndex,
|
||||
hidden: true,
|
||||
).insert();
|
||||
|
||||
emit(
|
||||
CardCustomizerSaved(
|
||||
state.selectedDesignIndex,
|
||||
state.selectedColorIndex,
|
||||
state.availableDesigns,
|
||||
state.availableColors,
|
||||
state.accountName,
|
||||
state.accountIndex,
|
||||
state.displaySats,
|
||||
state.cardOrder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveAccountName() async {
|
||||
@@ -205,12 +264,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
await saveMoneroAccountName();
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.wownero) {
|
||||
} else if (_wallet.type == WalletType.wownero) {
|
||||
await saveWowneroAccountName();
|
||||
}
|
||||
if (_wallet.type == WalletType.bitcoin && !lightningMode) {
|
||||
} else if (_wallet.type == WalletType.bitcoin) {
|
||||
await saveBitcoinAccountName();
|
||||
}
|
||||
}
|
||||
@@ -218,19 +274,21 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
Future<void> saveMoneroAccountName() async {
|
||||
final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet);
|
||||
await moneroAccountList.setLabelAccount(_wallet,
|
||||
accountIndex: state.accountIndex, label: state.accountName);
|
||||
accountIndex: state.accountIndex, label: state.accountName,);
|
||||
|
||||
await _wallet.save();
|
||||
}
|
||||
|
||||
Future<void> saveWowneroAccountName() async {
|
||||
final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet);
|
||||
await wowneroAccountList.setLabelAccount(_wallet,
|
||||
accountIndex: state.accountIndex, label: state.accountName);
|
||||
final accountList = wow.wownero!.getAccountList(_wallet);
|
||||
await accountList.setLabelAccount(
|
||||
_wallet,
|
||||
accountIndex: state.accountIndex,
|
||||
label: state.accountName,
|
||||
);
|
||||
|
||||
await _wallet.save();
|
||||
}
|
||||
|
||||
Future<void> saveBitcoinAccountName() async {
|
||||
await _wallet.walletInfo.renameAccount(
|
||||
accountIndex: state.accountIndex,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
part of 'card_customizer_bloc.dart';
|
||||
part of "card_customizer_bloc.dart";
|
||||
|
||||
@immutable
|
||||
sealed class CardCustomizerEvent {}
|
||||
@@ -6,27 +6,29 @@ sealed class CardCustomizerEvent {}
|
||||
class _Init extends CardCustomizerEvent {}
|
||||
|
||||
class CardDesignSelected extends CardCustomizerEvent {
|
||||
final int newDesignIndex;
|
||||
|
||||
CardDesignSelected(this.newDesignIndex);
|
||||
final int newDesignIndex;
|
||||
}
|
||||
|
||||
class ColorSelected extends CardCustomizerEvent {
|
||||
final int newColorIndex;
|
||||
|
||||
ColorSelected(this.newColorIndex);
|
||||
final int newColorIndex;
|
||||
}
|
||||
|
||||
class AccountNameChanged extends CardCustomizerEvent {
|
||||
final String newAccountName;
|
||||
|
||||
AccountNameChanged(this.newAccountName);
|
||||
final String newAccountName;
|
||||
}
|
||||
|
||||
class DesignSaved extends CardCustomizerEvent {}
|
||||
|
||||
class IconStyleSelected extends CardCustomizerEvent {
|
||||
final int iconIndex;
|
||||
|
||||
IconStyleSelected(this.iconIndex);
|
||||
final int iconIndex;
|
||||
}
|
||||
|
||||
class AccountHidden extends CardCustomizerEvent {}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/themes/core/theme_extension.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ArchiveConfirmationContent extends StatelessWidget {
|
||||
const ArchiveConfirmationContent({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.isFunded,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final bool isFunded;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isFunded) ...[
|
||||
Text(
|
||||
S.of(context).archive_account_funds_title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AccountFundsSummary(
|
||||
account: account,
|
||||
fiatBalance: fiatBalance,
|
||||
borderColor: context.customColors.warningOutlineColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_move_funds,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
S.of(context).archive_account_funded_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
AccountSummary(account: account),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_empty_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).archive_account_restore_hint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class AccountSummary extends StatelessWidget {
|
||||
const AccountSummary({required this.account});
|
||||
|
||||
final AccountListItem account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = account.label.trim();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${account.id + 1}. ",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: label.isEmpty ? S.of(context).unnamed_account : label,
|
||||
),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountFundsSummary extends StatelessWidget {
|
||||
const AccountFundsSummary({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.borderColor,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final Color borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
border: Border.all(color: borderColor),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: account.balance.currency.iconPath ??
|
||||
"assets/new-ui/crypto_full_icons/${account.balance.currency.name.toLowerCase()}.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MoneyText(
|
||||
account.balance,
|
||||
fractionalDigits: account.balance.decimals,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (fiatBalance != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
fiatBalance!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class AccountsPromo extends StatelessWidget {
|
||||
const AccountsPromo({
|
||||
required this.settingsStore,
|
||||
required this.walletName,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
final String walletName;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static bool supportsWallet(WalletType walletType) => walletType == WalletType.bitcoin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (settingsStore.accountsHomePromoDismissed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 64),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/accounts.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_title(walletName),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_description,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => settingsStore.accountsHomePromoDismissed = true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BalanceCardAction {
|
||||
final String label;
|
||||
@@ -22,7 +24,7 @@ class BalanceCard extends StatelessWidget {
|
||||
this.borderRadius = 20,
|
||||
this.selected = false,
|
||||
this.accountName = "",
|
||||
this.accountBalance = "",
|
||||
this.accountBalance,
|
||||
this.balance = "",
|
||||
this.fiatBalance = "",
|
||||
this.assetName = "",
|
||||
@@ -37,7 +39,7 @@ class BalanceCard extends StatelessWidget {
|
||||
final double width;
|
||||
final double borderRadius;
|
||||
final Gradient? gradient;
|
||||
final String accountBalance;
|
||||
final Money? accountBalance;
|
||||
final String accountName;
|
||||
final String balance;
|
||||
final String fiatBalance;
|
||||
@@ -62,7 +64,7 @@ class BalanceCard extends StatelessWidget {
|
||||
|
||||
final leadText = fiatFirst ? S.of(context).wallet_balance : accountName;
|
||||
|
||||
final bool showText = accountBalance.isNotEmpty ||
|
||||
final bool showText = accountBalance != null ||
|
||||
leadText.isNotEmpty ||
|
||||
balance.isNotEmpty ||
|
||||
fiatBalance.isNotEmpty ||
|
||||
@@ -102,7 +104,7 @@ class BalanceCard extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgFullOff'),
|
||||
key: ValueKey("svgFullOff"),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -117,7 +119,7 @@ class BalanceCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (leadText.isNotEmpty || accountBalance.isNotEmpty)
|
||||
if (leadText.isNotEmpty || accountBalance != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -153,8 +155,10 @@ class BalanceCard extends StatelessWidget {
|
||||
// readers, so drop it while it is invisible.
|
||||
child: ExcludeSemantics(
|
||||
excluding: selected,
|
||||
child: Text(
|
||||
child: MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
),
|
||||
),
|
||||
@@ -190,7 +194,13 @@ class BalanceCard extends StatelessWidget {
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(fiatFirst ? fiatBalance : balance),
|
||||
child: !fiatFirst && balance.isEmpty
|
||||
? MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
)
|
||||
: Text(fiatFirst ? fiatBalance : balance),
|
||||
),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
@@ -270,7 +280,7 @@ class BalanceCard extends StatelessWidget {
|
||||
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgIconOff'),
|
||||
key: ValueKey("svgIconOff"),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'package:cake_wallet/core/utilities.dart';
|
||||
import 'package:cw_core/balance_card_layout.dart';
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
@@ -9,7 +11,6 @@ import 'package:cake_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
@@ -60,9 +61,9 @@ class _CardsViewState extends State<CardsView> {
|
||||
_selectedIndex = _fallbackSelectedIndex;
|
||||
_cardOrderReaction = reaction(
|
||||
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
|
||||
(_) => setState(() {
|
||||
_selectedIndex = _fallbackSelectedIndex;
|
||||
}),
|
||||
(_) {
|
||||
if (mounted) setState(() => _selectedIndex = _fallbackSelectedIndex);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,20 +97,18 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
final isSelected = _selectedIndex == visualIndex;
|
||||
final accounts = widget.accountListViewModel?.accounts;
|
||||
final cardLabel = (!_hideAccountInfo && accounts != null && realIndex < accounts.length)
|
||||
? accounts[realIndex].label
|
||||
: S.of(context).balance;
|
||||
final account = accounts?.firstWhereOrNull((account) => account.id == realIndex);
|
||||
final cardLabel = !_hideAccountInfo && account != null ? account.label : S.of(context).balance;
|
||||
|
||||
void onCardTap() {
|
||||
Future<void> onCardTap() async {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
setState(() {
|
||||
if (widget.accountListViewModel != null && !widget.lightningMode)
|
||||
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
_selectedIndex = visualIndex;
|
||||
});
|
||||
if (account != null && !_hideAccountInfo) {
|
||||
await widget.accountListViewModel!.select(account);
|
||||
}
|
||||
if (mounted) setState(() => _selectedIndex = visualIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +148,8 @@ class _CardsViewState extends State<CardsView> {
|
||||
onTap: onCardTap,
|
||||
onLongPress: onCardLongPress,
|
||||
child: Observer(builder: (_) {
|
||||
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
|
||||
return Container();
|
||||
}
|
||||
final account = widget.accountListViewModel?.accounts[realIndex];
|
||||
final account = widget.accountListViewModel?.accounts
|
||||
.firstWhereOrNull((account) => account.id == realIndex);
|
||||
|
||||
// The second balance should always be the lightning balance
|
||||
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
|
||||
@@ -180,25 +177,17 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
// the card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final CardDesign cardDesign;
|
||||
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
|
||||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
|
||||
cardDesign = CardDesign.genericDefault;
|
||||
else if (widget.lightningMode)
|
||||
// the lightning design is always the last one (after all bitcoin accounts)
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns.last;
|
||||
else
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
|
||||
final designIndex = widget.dashboardViewModel.cardAccountIndices.indexOf(
|
||||
widget.lightningMode ? -2 : realIndex,
|
||||
);
|
||||
final cardDesign = designIndex < 0 ||
|
||||
designIndex >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[designIndex];
|
||||
|
||||
final String accountName;
|
||||
final String accountBalance;
|
||||
if (account == null || _hideAccountInfo) {
|
||||
accountName = "";
|
||||
accountBalance = "";
|
||||
} else {
|
||||
accountName = account.label;
|
||||
accountBalance = account.balance ?? "0.00";
|
||||
}
|
||||
final showAccount = account != null && !_hideAccountInfo;
|
||||
final accountName = showAccount ? account.label : "";
|
||||
final accountBalance = showAccount ? account.balance : null;
|
||||
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
@@ -288,28 +277,13 @@ class _CardsViewState extends State<CardsView> {
|
||||
}
|
||||
|
||||
|
||||
int _visibleCardsCount() {
|
||||
if (widget.dashboardViewModel.wallet.type != WalletType.bitcoin) {
|
||||
return widget.dashboardViewModel.cardDesigns.length;
|
||||
}
|
||||
|
||||
if (widget.lightningMode || !widget.dashboardViewModel.isMultiAccountsEnabled) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return widget.accountListViewModel?.accounts.length ?? 1;
|
||||
}
|
||||
|
||||
int? _visualIndexForSelectedAccount(Map<int, int> order) {
|
||||
final vm = widget.accountListViewModel;
|
||||
if (vm == null || _hideAccountInfo) return null;
|
||||
|
||||
final selectedId = vm.selectedAccount?.id;
|
||||
final realIndex = vm.accounts.indexWhere((a) => a.id == selectedId);
|
||||
if (realIndex < 0) return null;
|
||||
|
||||
for (final entry in order.entries) {
|
||||
if (entry.value == realIndex) return entry.key;
|
||||
if (entry.value == selectedId) return entry.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -319,27 +293,15 @@ class _CardsViewState extends State<CardsView> {
|
||||
final parentWidth = MediaQuery.of(context).size.width;
|
||||
final children = <Widget>[];
|
||||
|
||||
int numCards = _visibleCardsCount();
|
||||
if (numCards == 0) numCards = 1;
|
||||
|
||||
if (_selectedIndex >= numCards) {
|
||||
_selectedIndex = 0;
|
||||
}
|
||||
|
||||
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
|
||||
? Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
)
|
||||
: widget.dashboardViewModel.cardOrder;
|
||||
|
||||
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
|
||||
int visualIndex = (_selectedIndex - i + numCards) % numCards;
|
||||
if (order[visualIndex] == null) {
|
||||
order = Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
);
|
||||
}
|
||||
final stack = _hideAccountInfo
|
||||
? const [0]
|
||||
: BalanceCardLayout.stackFromOrder(widget.dashboardViewModel.cardOrder);
|
||||
if (stack.isEmpty) return const SizedBox.shrink();
|
||||
final numCards = stack.length;
|
||||
if (_selectedIndex < 0 || _selectedIndex >= numCards) {
|
||||
_selectedIndex = numCards - 1;
|
||||
}
|
||||
final order = {for (int i = 0; i < stack.length; i++) i: stack[i]};
|
||||
|
||||
final followIndex = _visualIndexForSelectedAccount(order);
|
||||
if (followIndex != null) _selectedIndex = followIndex;
|
||||
@@ -461,4 +423,4 @@ class _CardsViewState extends State<CardsView> {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class ModalTopBar extends StatelessWidget {
|
||||
this.bottomText,
|
||||
this.leadingWidget,
|
||||
this.trailingWidget,
|
||||
this.titleLeadingWidget,
|
||||
this.leadingSemanticLabel,
|
||||
this.trailingSemanticLabel}) {
|
||||
if (leadingIcon != null && leadingWidget != null) {
|
||||
@@ -36,6 +37,7 @@ class ModalTopBar extends StatelessWidget {
|
||||
final Widget? trailingIcon;
|
||||
final Widget? leadingWidget;
|
||||
final Widget? trailingWidget;
|
||||
final Widget? titleLeadingWidget;
|
||||
|
||||
/// Accessible name for the leading chrome button. Required (and must be
|
||||
/// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
|
||||
@@ -53,6 +55,51 @@ class ModalTopBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasBottomText = bottomText != null && bottomText!.isNotEmpty;
|
||||
final titleContent = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Semantics(
|
||||
key: ValueKey(title),
|
||||
header: title.isNotEmpty,
|
||||
// Android reads the heading from headingLevel since the
|
||||
// Flutter 3.41 engine; header: alone only covers iOS.
|
||||
headingLevel: title.isNotEmpty ? 1 : null,
|
||||
child: titleLeadingWidget == null
|
||||
? Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontSize: hasBottomText ? 16 : 18,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
ExcludeSemantics(child: titleLeadingWidget),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontSize: hasBottomText ? 16 : 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasBottomText)
|
||||
Text(
|
||||
bottomText!,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.all(18),
|
||||
child: Stack(
|
||||
@@ -60,36 +107,14 @@ class ModalTopBar extends StatelessWidget {
|
||||
children: [
|
||||
Positioned(
|
||||
top: hasBottomText ? -4 : 6,
|
||||
left: titleLeadingWidget == null ? null : buttonSize + 8,
|
||||
right: titleLeadingWidget == null ? null : buttonSize + 8,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Semantics(
|
||||
key: ValueKey(title),
|
||||
header: title.isNotEmpty,
|
||||
// Android reads the heading from headingLevel since the
|
||||
// Flutter 3.41 engine; header: alone only covers iOS.
|
||||
headingLevel: title.isNotEmpty ? 1 : null,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasBottomText)
|
||||
Text(
|
||||
bottomText!,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (titleLeadingWidget == null) titleContent else Expanded(child: titleContent),
|
||||
if (subtitle != null && subtitle!.isNotEmpty)
|
||||
Text(subtitle!,
|
||||
style: Theme.of(context)
|
||||
|
||||
+2
-5
@@ -129,7 +129,6 @@ import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
|
||||
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
|
||||
@@ -138,7 +137,6 @@ import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/nano_account.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
@@ -848,9 +846,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
|
||||
case Routes.walletAccountsPage:
|
||||
final dashboardViewModel = settings.arguments as DashboardViewModel;
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(_) => getIt.get<WalletAccountsPage>(param1: dashboardViewModel),
|
||||
);
|
||||
final page = getIt.get<WalletAccountsPage>(param1: dashboardViewModel);
|
||||
return handleRouteWithPlatformAwareness((_) => page);
|
||||
|
||||
case Routes.managePowNodes:
|
||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: true));
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
|
||||
@@ -75,29 +72,6 @@ class PrivacyPage extends BasePage {
|
||||
_privacySettingsViewModel.setUseLightning(val);
|
||||
}),
|
||||
],
|
||||
"": [
|
||||
if (_privacySettingsViewModel.hasSilentPaymentsScanning)
|
||||
ListItemRegularRow(
|
||||
iconPath: "assets/new-ui/settings_row_icons/silent-payments.svg",
|
||||
keyValue: "silent_payments",
|
||||
label: S.of(context).silent_payments,
|
||||
onTap: () =>
|
||||
Navigator.of(context).pushNamed(Routes.silentPaymentsSettings)),
|
||||
if (_privacySettingsViewModel.hasMWEB)
|
||||
ListItemRegularRow(
|
||||
iconPath: "assets/new-ui/settings_row_icons/mweb.svg",
|
||||
keyValue: "mweb",
|
||||
label: "MWEB",
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings)),
|
||||
if (_privacySettingsViewModel.hasCoinControl)
|
||||
ListItemRegularRow(
|
||||
iconPath: "assets/new-ui/settings_row_icons/coin-control.svg",
|
||||
keyValue: "coin_control",
|
||||
label: "Coin Control",
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList,
|
||||
arguments:
|
||||
CoinControlPageArgs(canEdit: false, coinTypeToSpendFrom: null))),
|
||||
],
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modal_grab_handle.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:cw_core/generate_name.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class AccountCreationModal extends StatefulWidget {
|
||||
const AccountCreationModal({
|
||||
required this.viewModel,
|
||||
super.key,
|
||||
});
|
||||
const AccountCreationModal({required this.viewModel, super.key});
|
||||
|
||||
final WalletAccountEditOrCreateViewModel viewModel;
|
||||
|
||||
@@ -26,14 +22,59 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
|
||||
static const int maxAccountNameLength = 25;
|
||||
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.text = widget.viewModel.label;
|
||||
_controller.addListener(() {
|
||||
widget.viewModel.label = _controller.text;
|
||||
});
|
||||
bool get _canContinue =>
|
||||
_controller.text.trim().isNotEmpty && _controller.text.length <= maxAccountNameLength;
|
||||
|
||||
Future<void> _generateAccountName() async {
|
||||
final generatedName = await generateName();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _controller.text = generatedName.length > maxAccountNameLength
|
||||
? generatedName.substring(0, maxAccountNameLength)
|
||||
: generatedName);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_loading || !_canContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _loading = true);
|
||||
widget.viewModel.label = _controller.text;
|
||||
|
||||
late final String errorMessage;
|
||||
try {
|
||||
await widget.viewModel.save();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final state = widget.viewModel.state;
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
Navigator.of(context).pop(true);
|
||||
return;
|
||||
}
|
||||
errorMessage = state is FailureState ? state.error : S.of(context).error_while_processing;
|
||||
} catch (error) {
|
||||
errorMessage = error.toString();
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = false);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: S.of(dialogContext).error,
|
||||
alertContent: errorMessage,
|
||||
buttonText: S.of(dialogContext).ok,
|
||||
buttonAction: Navigator.of(dialogContext).pop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -42,105 +83,117 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onSubmit() async {
|
||||
if (widget.viewModel.state is IsExecutingState) return;
|
||||
if (_controller.text.trim().isEmpty || _controller.text.length > maxAccountNameLength) return;
|
||||
|
||||
await widget.viewModel.save();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (widget.viewModel.state is ExecutedSuccessfullyState) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: !_loading,
|
||||
child: _buildContent(context),
|
||||
);
|
||||
|
||||
Widget _buildContent(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30))),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ModalGrabHandle(),
|
||||
ModalTopBar(
|
||||
title: widget.viewModel.isEdit
|
||||
? S.of(context).edit_account
|
||||
: S.of(context).create_account,
|
||||
title: S.of(context).create_account,
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: Navigator.of(context).maybePop,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 50,
|
||||
children: [
|
||||
const SizedBox(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(18, 32, 18, 24),
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
const CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/create_account.svg",
|
||||
width: 125,
|
||||
height: 125,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
maxLength: maxAccountNameLength,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(maxAccountNameLength),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).account_name,
|
||||
counterText: "",
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final generated = await generateName();
|
||||
_controller.text = generated.length > maxAccountNameLength
|
||||
? generated.substring(0, maxAccountNameLength)
|
||||
: generated;
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/randomize.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
Text(
|
||||
S.of(context).account_creation_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
maxLength: maxAccountNameLength,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(maxAccountNameLength)
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).account_name,
|
||||
counterText: "",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _controller,
|
||||
builder: (context, value, _) => Observer(
|
||||
builder: (_) => NewPrimaryButton(
|
||||
onPressed: _onSubmit,
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
disabled: value.text.trim().isEmpty ||
|
||||
value.text.length > maxAccountNameLength,
|
||||
isLoading: widget.viewModel.state is IsExecutingState,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).generate_name,
|
||||
onTap: _generateAccountName,
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: _generateAccountName,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/randomize.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: NewPrimaryButton(
|
||||
onPressed: _save,
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: _loading,
|
||||
disabled: !_canContinue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -153,6 +153,7 @@ abstract class SettingsStoreBase with Store {
|
||||
required this.hasEnabledMwebBefore,
|
||||
required this.mwebNodeUri,
|
||||
required this.mwebAdDismissed,
|
||||
required this.accountsHomePromoDismissed,
|
||||
required this.balanceHideCounter,
|
||||
required this.zcashMigrationModalViewed,
|
||||
required bool initialEnableAutomaticNodeSwitching,
|
||||
@@ -313,6 +314,9 @@ abstract class SettingsStoreBase with Store {
|
||||
reaction((_) => mwebAdDismissed,
|
||||
(val) => sharedPreferences.setBool(PreferencesKey.mwebAdDismissed, val));
|
||||
|
||||
reaction((_) => accountsHomePromoDismissed,
|
||||
(val) => sharedPreferences.setBool(PreferencesKey.accountsHomePromoDismissed, val));
|
||||
|
||||
priority.observe((change) {
|
||||
final String? key;
|
||||
switch (change.key) {
|
||||
@@ -1105,6 +1109,9 @@ abstract class SettingsStoreBase with Store {
|
||||
@observable
|
||||
bool mwebAdDismissed;
|
||||
|
||||
@observable
|
||||
bool accountsHomePromoDismissed;
|
||||
|
||||
@observable
|
||||
bool zcashMigrationModalViewed;
|
||||
|
||||
@@ -1191,6 +1198,12 @@ abstract class SettingsStoreBase with Store {
|
||||
Future<void> setShouldShowReceiveWarning(bool value) async =>
|
||||
_sharedPreferences.setBool(PreferencesKey.shouldShowReceiveWarning, value);
|
||||
|
||||
bool isEducationDismissed(String educationId) =>
|
||||
_sharedPreferences.getBool(PreferencesKey.educationDismissed(educationId)) ?? false;
|
||||
|
||||
Future<void> dismissEducation(String educationId) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.educationDismissed(educationId), true);
|
||||
|
||||
static Future<SettingsStore> load(
|
||||
{required bool isBitcoinBuyEnabled,
|
||||
FiatCurrency initialFiatCurrency = FiatCurrency.usd,
|
||||
@@ -1692,6 +1705,9 @@ abstract class SettingsStoreBase with Store {
|
||||
final mwebAdDismissed =
|
||||
await sharedPreferences.getBool(PreferencesKey.mwebAdDismissed) ?? false;
|
||||
|
||||
final accountsHomePromoDismissed =
|
||||
sharedPreferences.getBool(PreferencesKey.accountsHomePromoDismissed) ?? false;
|
||||
|
||||
final balanceHideCounter =
|
||||
await sharedPreferences.getInt(PreferencesKey.balanceHideCounter) ?? 0;
|
||||
|
||||
@@ -1815,6 +1831,7 @@ abstract class SettingsStoreBase with Store {
|
||||
shouldShowRepWarning: shouldShowRepWarning,
|
||||
initialBuiltinTor: builtinTor,
|
||||
mwebAdDismissed: mwebAdDismissed,
|
||||
accountsHomePromoDismissed: accountsHomePromoDismissed,
|
||||
balanceHideCounter: balanceHideCounter,
|
||||
zcashMigrationModalViewed: zcashMigrationModalViewed
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/tron/tron.dart';
|
||||
import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
@@ -546,6 +547,21 @@ abstract class BalanceViewModelBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
String? accountFiatBalance(AccountListItem account) {
|
||||
if (isFiatDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fiat = settingsStore.fiatCurrency.title;
|
||||
final value = savedDisplayMode == BalanceDisplayMode.hiddenBalance
|
||||
? "●●●●●"
|
||||
: calculateFiatAmount(
|
||||
price: price,
|
||||
cryptoAmount: account.balance.toString(),
|
||||
).withLocalSeperator(settingsStore.languageCode);
|
||||
return "$value $fiat";
|
||||
}
|
||||
|
||||
String _getFiatBalance({required double price, Money? cryptoAmount}) {
|
||||
if (cryptoAmount == null) {
|
||||
return '0.00';
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_v
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cw_core/account.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import 'package:cake_wallet/core/trade_monitor.dart';
|
||||
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
@@ -110,7 +111,8 @@ abstract class DashboardViewModelBase with Store {
|
||||
showDecredInfoCard = wallet.type == WalletType.decred &&
|
||||
(sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
|
||||
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
|
||||
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
isMultiAccountsEnabled =
|
||||
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
name = wallet.name;
|
||||
type = wallet.type;
|
||||
isShowFirstYatIntroduction = false;
|
||||
@@ -426,72 +428,86 @@ abstract class DashboardViewModelBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
List<int> get cardAccountIndices {
|
||||
if (wallet.type == WalletType.monero) {
|
||||
return monero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
|
||||
}
|
||||
if (wallet.type == WalletType.wownero) {
|
||||
return wow.wownero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
|
||||
}
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
final accounts = accountListViewModel?.accounts;
|
||||
return [
|
||||
if (accounts == null || accounts.isEmpty) 0 else ...accounts.map((account) => account.id),
|
||||
-2,
|
||||
];
|
||||
}
|
||||
return const [-1];
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> loadCardDesigns() async {
|
||||
final walletCardStyleSettings =
|
||||
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
|
||||
final btcAccounts =
|
||||
wallet.type == WalletType.bitcoin ? await wallet.walletInfo.getAccounts() : null;
|
||||
final lightningCardIndex = btcAccounts?.length;
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
await accountListViewModel?.reload();
|
||||
}
|
||||
final accountIndices = cardAccountIndices;
|
||||
final settings = await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
|
||||
if (wallet.type != WalletType.bitcoin && accountIndices.contains(0) &&
|
||||
!settings.any((setting) => setting.accountIndex == 0)) {
|
||||
final primarySettings = settings.where((setting) => setting.accountIndex == -1)
|
||||
.firstOrNull?.copyWith(accountIndex: 0);
|
||||
if (primarySettings != null) {
|
||||
await primarySettings.insert();
|
||||
settings.add(primarySettings);
|
||||
}
|
||||
}
|
||||
final layout = BalanceCardLayout.resolve(
|
||||
accountIndices: accountIndices.where((index) => index != -2).toList(),
|
||||
settings: settings,
|
||||
);
|
||||
|
||||
int numAccounts = 1;
|
||||
cardDesigns
|
||||
..clear()
|
||||
..addAll(accountIndices.map((accountIndex) => CardDesign.fromStyleSettings(
|
||||
settings.where((setting) => setting.accountIndex == accountIndex).firstOrNull,
|
||||
accountIndex == -2 ? CryptoCurrency.btcln : wallet.currency,
|
||||
)));
|
||||
cardOrder = {
|
||||
for (int position = 0; position < layout.visible.length; position++)
|
||||
position: layout.visible[position],
|
||||
}.asObservable();
|
||||
|
||||
if (layout.needsRepair) {
|
||||
await BalanceCardStyleSettings.setVisibleOrder(wallet.walletInfo.internalId, layout.orders);
|
||||
}
|
||||
if (isMultiAccountsEnabled && !lightningMode && layout.visible.isNotEmpty &&
|
||||
!layout.visible.contains(accountListViewModel?.selectedAccount?.id)) {
|
||||
final visibleAccount = accountListViewModel?.accounts
|
||||
.where((account) => account.id == layout.visible.last).firstOrNull;
|
||||
if (visibleAccount != null) {
|
||||
await accountListViewModel!.select(visibleAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get currentCardAccountIndex {
|
||||
if (wallet.type == WalletType.monero) {
|
||||
numAccounts = monero!.getAccountList(wallet).accounts.length;
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
|
||||
} else if (wallet.type == WalletType.bitcoin) {
|
||||
numAccounts = btcAccounts!.length + 1; // adding 1 for the lightning account
|
||||
return monero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
|
||||
final usesAccountIndices = wallet.type == WalletType.bitcoin || balanceViewModel.hasAccounts;
|
||||
|
||||
cardDesigns.clear();
|
||||
final newOrder = <int, int>{};
|
||||
|
||||
final orderableCount = (wallet.type == WalletType.bitcoin) ? numAccounts - 1 : numAccounts;
|
||||
|
||||
for (int i = 0; i < numAccounts; i++) {
|
||||
late final int index;
|
||||
final isLightning = wallet.type == WalletType.bitcoin && i == lightningCardIndex;
|
||||
if (isLightning) {
|
||||
index = -2;
|
||||
} else if (usesAccountIndices) {
|
||||
index = i;
|
||||
} else {
|
||||
index = -1;
|
||||
}
|
||||
|
||||
final setting = walletCardStyleSettings.where((e) => e.accountIndex == index).firstOrNull ??
|
||||
(index == 0 && wallet.type != WalletType.bitcoin
|
||||
? walletCardStyleSettings.where((e) => e.accountIndex == -1).firstOrNull
|
||||
: null);
|
||||
|
||||
final curr = isLightning ? CryptoCurrency.btcln : wallet.currency;
|
||||
|
||||
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
|
||||
|
||||
if (isLightning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (setting?.cardOrder != null &&
|
||||
setting!.cardOrder >= 0 &&
|
||||
setting.cardOrder < orderableCount) {
|
||||
newOrder[setting.cardOrder] = i;
|
||||
}
|
||||
if (wallet.type == WalletType.wownero) {
|
||||
return wow.wownero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
|
||||
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
|
||||
for (int i = 0; i < orderableCount; i++) {
|
||||
if (!newOrder.containsValue(i)) {
|
||||
int free = 0;
|
||||
while (newOrder.containsKey(free)) {
|
||||
free++;
|
||||
}
|
||||
newOrder[free] = i;
|
||||
}
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
return lightningMode ? -2 : wallet.walletInfo.currentAccountIndex;
|
||||
}
|
||||
cardOrder = newOrder.asObservable();
|
||||
return -1;
|
||||
}
|
||||
|
||||
CardDesign get currentCardDesign {
|
||||
final designIndex = cardAccountIndices.indexOf(currentCardAccountIndex);
|
||||
return cardDesigns.isEmpty || designIndex >= cardDesigns.length
|
||||
? CardDesign.fromStyleSettings(null, wallet.currency)
|
||||
: cardDesigns[designIndex == -1 ? 0 : designIndex];
|
||||
}
|
||||
|
||||
void _transactionDisposerCallback(int _) async {
|
||||
@@ -925,7 +941,9 @@ abstract class DashboardViewModelBase with Store {
|
||||
List<AccountListItem> get visibleAccounts {
|
||||
final all = accountListViewModel?.accounts;
|
||||
if (all == null || all.isEmpty) return const <AccountListItem>[];
|
||||
if (isMultiAccountsEnabled) return all.toList(growable: false);
|
||||
if (isMultiAccountsEnabled) {
|
||||
return all.where((account) => cardOrder.containsValue(account.id)).toList(growable: false);
|
||||
}
|
||||
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
|
||||
return <AccountListItem>[primary];
|
||||
}
|
||||
@@ -1353,7 +1371,13 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
// Lightning belongs to the primary account only
|
||||
if (lightningMode) {
|
||||
unawaited(_selectPrimaryAccount());
|
||||
unawaited(_selectPrimaryAccount().then((_) async {
|
||||
if (!lightningMode && isMultiAccountsEnabled) {
|
||||
await loadCardDesigns();
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
unawaited(loadCardDesigns());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1382,7 +1406,8 @@ abstract class DashboardViewModelBase with Store {
|
||||
this.wallet = wallet;
|
||||
type = wallet.type;
|
||||
name = wallet.name;
|
||||
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
isMultiAccountsEnabled =
|
||||
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onAccountChangeReaction = null;
|
||||
|
||||
@@ -444,16 +444,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
@computed
|
||||
bool get hasCoinControl =>
|
||||
[
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.monero,
|
||||
WalletType.wownero,
|
||||
WalletType.decred,
|
||||
WalletType.bitcoinCash,
|
||||
WalletType.dogecoin
|
||||
].contains(wallet.type) &&
|
||||
coinTypeToSpendFrom != UnspentCoinType.lightning;
|
||||
wallet.hasCoinControl && coinTypeToSpendFrom != UnspentCoinType.lightning;
|
||||
|
||||
@computed
|
||||
bool get hasFees => feesViewModel.hasFees && coinTypeToSpendFrom != UnspentCoinType.lightning;
|
||||
|
||||
@@ -54,15 +54,7 @@ abstract class PrivacySettingsViewModelBase with Store {
|
||||
].contains(_wallet.type);
|
||||
|
||||
@computed
|
||||
bool get hasCoinControl => [
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.monero,
|
||||
WalletType.wownero,
|
||||
WalletType.decred,
|
||||
WalletType.bitcoinCash,
|
||||
WalletType.dogecoin
|
||||
].contains(_wallet.type);
|
||||
bool get hasCoinControl => _wallet.hasCoinControl;
|
||||
|
||||
bool get isMoneroWallet => _wallet.type == WalletType.monero;
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
|
||||
class AccountListItem {
|
||||
AccountListItem({required this.label, required this.id, this.balance, this.isSelected = false});
|
||||
AccountListItem({
|
||||
required this.label,
|
||||
required this.id,
|
||||
required this.balance,
|
||||
this.isSelected = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int id;
|
||||
final bool isSelected;
|
||||
final String? balance;
|
||||
final Money balance;
|
||||
|
||||
bool get isFunded => balance.sign > 0;
|
||||
}
|
||||
|
||||
+18
-38
@@ -1,24 +1,22 @@
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:collection/collection.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
part 'bitcoin_account_list_view_model.g.dart';
|
||||
part "bitcoin_account_list_view_model.g.dart";
|
||||
|
||||
class BitcoinAccountListViewModel = BitcoinAccountListViewModelBase
|
||||
with _$BitcoinAccountListViewModel;
|
||||
|
||||
abstract class BitcoinAccountListViewModelBase with Store implements WalletAccountListViewModel {
|
||||
BitcoinAccountListViewModelBase(this._wallet, this.settingsStore) {
|
||||
BitcoinAccountListViewModelBase(this._wallet) {
|
||||
unawaited(_loadAccounts());
|
||||
|
||||
reaction(
|
||||
@@ -26,14 +24,8 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
|
||||
(snapshot) => unawaited(_refreshBalances(snapshot)),
|
||||
equals: const MapEquality<int, Object>().equals,
|
||||
);
|
||||
|
||||
reaction(
|
||||
(_) => settingsStore.balanceDisplayMode,
|
||||
(_) => unawaited(_recomputeBalanceStrings()),
|
||||
);
|
||||
}
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
final WalletBase _wallet;
|
||||
|
||||
List<WalletInfoAccount>? _cachedStoredAccounts;
|
||||
@@ -71,14 +63,6 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
|
||||
return _runLoad(() async => cached);
|
||||
}
|
||||
|
||||
|
||||
@action
|
||||
Future<void> _recomputeBalanceStrings() {
|
||||
final cached = _cachedStoredAccounts;
|
||||
if (cached == null) return _loadAccounts();
|
||||
return _runLoad(() async => cached);
|
||||
}
|
||||
|
||||
Future<void> _runLoad(Future<List<WalletInfoAccount>> Function() fetch) {
|
||||
final inProgress = _loadAccountsFuture;
|
||||
if (inProgress != null) {
|
||||
@@ -99,7 +83,7 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
|
||||
.map((account) => AccountListItem(
|
||||
id: account.accountIndex,
|
||||
label: account.label,
|
||||
balance: _balanceForAccount(account.accountIndex),
|
||||
balance: _fullBalance(account.accountIndex),
|
||||
isSelected: account.accountIndex == currentAccountIndex,
|
||||
))
|
||||
.toList();
|
||||
@@ -115,16 +99,12 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
|
||||
});
|
||||
}
|
||||
|
||||
Money _fullBalance(int accountIndex) {
|
||||
final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
|
||||
return balance.confirmed + balance.unconfirmed;
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> reload() => _loadAccounts();
|
||||
|
||||
String _balanceForAccount(int accountIndex) {
|
||||
if (settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance) {
|
||||
return '●●●●●●';
|
||||
}
|
||||
final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
|
||||
|
||||
return AmountParsingProxy(settingsStore.displayAmountsInSatoshi).asDisplayString(balance.available);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-7
@@ -2,6 +2,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/balance_card_layout.dart';
|
||||
import 'package:cw_core/balance_card_style_settings.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
@@ -36,7 +37,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase
|
||||
@observable
|
||||
String label;
|
||||
|
||||
final MoneroAccountList _moneroAccountList;
|
||||
final MoneroAccountList? _moneroAccountList;
|
||||
final WowneroAccountList? _wowneroAccountList;
|
||||
final AccountListItem? _accountListItem;
|
||||
final WalletBase _wallet;
|
||||
@@ -51,13 +52,22 @@ abstract class MoneroAccountEditOrCreateViewModelBase
|
||||
return ret.isNotEmpty ? ret : CardDesign.allGradients;
|
||||
}
|
||||
|
||||
Future<void> _saveRandomCardDesign() async {
|
||||
Future<void> _saveRandomCardDesign(int accountIndex) async {
|
||||
final gradients = await _getUsableCardGradients();
|
||||
|
||||
final accountIndices = _wallet.type == WalletType.monero
|
||||
? _moneroAccountList!.accounts.map((account) => account.id).toList()
|
||||
: _wowneroAccountList!.accounts.map((account) => account.id).toList();
|
||||
|
||||
final layout = BalanceCardLayout.resolve(
|
||||
accountIndices: accountIndices,
|
||||
settings: await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId),
|
||||
);
|
||||
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: _moneroAccountList.accounts.length,
|
||||
cardOrder: _moneroAccountList.accounts.length,
|
||||
accountIndex: accountIndex,
|
||||
cardOrder: layout.visible.length,
|
||||
design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
|
||||
.withGradient(gradients[Random().nextInt(gradients.length)]))
|
||||
.insert();
|
||||
@@ -67,7 +77,11 @@ abstract class MoneroAccountEditOrCreateViewModelBase
|
||||
try {
|
||||
state = IsExecutingState();
|
||||
|
||||
if (!isEdit) await _saveRandomCardDesign();
|
||||
if (!isEdit) {
|
||||
await _saveRandomCardDesign(_wallet.type == WalletType.monero
|
||||
? _moneroAccountList!.accounts.length
|
||||
: _wowneroAccountList!.accounts.length);
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
await saveMonero();
|
||||
@@ -84,13 +98,13 @@ abstract class MoneroAccountEditOrCreateViewModelBase
|
||||
|
||||
Future<void> saveMonero() async {
|
||||
if (_accountListItem != null) {
|
||||
await _moneroAccountList.setLabelAccount(
|
||||
await _moneroAccountList!.setLabelAccount(
|
||||
_wallet,
|
||||
accountIndex: _accountListItem.id,
|
||||
label: label,
|
||||
);
|
||||
} else {
|
||||
await _moneroAccountList.addAccount(
|
||||
await _moneroAccountList!.addAccount(
|
||||
_wallet,
|
||||
label: label,
|
||||
);
|
||||
|
||||
+16
-24
@@ -1,22 +1,18 @@
|
||||
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import "package:cake_wallet/monero/monero.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
part 'monero_account_list_view_model.g.dart';
|
||||
part "monero_account_list_view_model.g.dart";
|
||||
|
||||
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
|
||||
|
||||
abstract class MoneroAccountListViewModelBase with Store implements WalletAccountListViewModel {
|
||||
MoneroAccountListViewModelBase(this._wallet, this.settingsStore);
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
MoneroAccountListViewModelBase(this._wallet);
|
||||
|
||||
@override
|
||||
AccountListItem? get selectedAccount => selected;
|
||||
@@ -25,7 +21,6 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
|
||||
|
||||
@computed
|
||||
List<AccountListItem> get accounts {
|
||||
final hideBalance = settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance;
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
return monero!
|
||||
.getAccountList(_wallet)
|
||||
@@ -33,7 +28,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
|
||||
.map((acc) => AccountListItem(
|
||||
label: acc.label,
|
||||
id: acc.id,
|
||||
balance: hideBalance ? '●●●●●●' : acc.balance,
|
||||
balance: monero!.getAccountFullBalance(acc.id),
|
||||
isSelected: acc.id == monero!.getCurrentAccount(_wallet).id))
|
||||
.toList();
|
||||
}
|
||||
@@ -45,19 +40,16 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
|
||||
.map((acc) => AccountListItem(
|
||||
label: acc.label,
|
||||
id: acc.id,
|
||||
balance: hideBalance ? '●●●●●●' : acc.balance,
|
||||
balance: wownero!.getAccountFullBalance(acc.id),
|
||||
isSelected: acc.id == wownero!.getCurrentAccount(_wallet).id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type} for monero');
|
||||
throw Exception("Unexpected wallet type: ${_wallet.type} for monero");
|
||||
}
|
||||
|
||||
@computed
|
||||
AccountListItem get selected {
|
||||
final currentId = monero!.getCurrentAccount(_wallet).id;
|
||||
return accounts.firstWhere((item) => item.id == currentId);
|
||||
}
|
||||
AccountListItem get selected => accounts.firstWhere((item) => item.isSelected);
|
||||
|
||||
final WalletBase _wallet;
|
||||
|
||||
@@ -71,7 +63,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
|
||||
_wallet,
|
||||
item.id,
|
||||
item.label,
|
||||
item.balance,
|
||||
item.balance.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +72,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
|
||||
_wallet,
|
||||
item.id,
|
||||
item.label,
|
||||
item.balance,
|
||||
item.balance.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
|
||||
abstract class WalletAccountListViewModel {
|
||||
List<AccountListItem> get accounts;
|
||||
|
||||
@@ -136,6 +136,12 @@ class CWWownero extends Wownero {
|
||||
@override
|
||||
WowneroAccountList getAccountList(Object wallet) => CWWowneroAccountList(wallet);
|
||||
|
||||
@override
|
||||
Money getAccountFullBalance(int accountIndex) => Money.fromInt(
|
||||
wownero_wallet_api.getFullBalance(accountIndex: accountIndex),
|
||||
CryptoCurrency.wow,
|
||||
);
|
||||
|
||||
@override
|
||||
WowneroSubaddressList getSubaddressList(Object wallet) => CWWowneroSubaddressList(wallet);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user