From fc9e4d35dd3d7da66d10a587a052558feee0397d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:02:56 -0600 Subject: [PATCH 001/100] remove loading future --- .../address_book_views/address_book_view.dart | 221 +++++++++--------- 1 file changed, 112 insertions(+), 109 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 50e51110b..fd2a995cc 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -18,7 +18,6 @@ import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -38,9 +37,9 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { late TextEditingController _searchController; final _searchFocusNode = FocusNode(); - - List<Contact>? _cache; - List<Contact>? _cacheFav; + // + // List<Contact>? _cache; + // List<Contact>? _cacheFav; String _searchTerm = ""; @@ -100,8 +99,10 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final addressBookEntriesFuture = ref.watch( - addressBookServiceProvider.select((value) => value.addressBookEntries)); + // final addressBookEntriesFuture = ref.watch( + // addressBookServiceProvider.select((value) => value.addressBookEntries)); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); final isDesktop = Util.isDesktop; return ConditionalParent( @@ -279,57 +280,58 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cacheFav = snapshot.data!; - } - if (_cacheFav == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cacheFav!.isNotEmpty) { - return RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ..._cacheFav! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), + // FutureBuilder( + // future: addressBookEntriesFuture, + // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + // if (snapshot.connectionState == ConnectionState.done && + // snapshot.hasData) { + // _cacheFav = snapshot.data!; + // } + // if (_cacheFav == null) { + // // TODO proper loading animation + // return const LoadingIndicator(); + // } else { + // if (_cacheFav!.isNotEmpty) { + // return + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, ), ), - ); - } - } - }, - ), + ], + ), + ) + // ; + // } else { + // return RoundedWhiteContainer( + // child: Center( + // child: Text( + // "Your favorite contacts will appear here", + // style: STextStyles.itemSubtitle(context), + // ), + // ), + // ); + // } + // } + // }, + // ) + , const SizedBox( height: 16, ), @@ -340,63 +342,64 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cache = snapshot.data!; - } - if (_cache == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cache!.isNotEmpty) { - return Column( + // FutureBuilder( + // future: addressBookEntriesFuture, + // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + // if (snapshot.connectionState == ConnectionState.done && + // snapshot.hasData) { + // _cache = snapshot.data!; + // } + // if (_cache == null) { + // // TODO proper loading animation + // return const LoadingIndicator(); + // } else { + // if (_cache!.isNotEmpty) { + // return + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ..._cache! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "desktopContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => !element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ), ], - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), + ), + ), + ), + ], + ) + // ; + // } else { + // return RoundedWhiteContainer( + // child: Center( + // child: Text( + // "Your contacts will appear here", + // style: STextStyles.itemSubtitle(context), + // ), + // ), + // ); + // } + // } + // }, + // ) + , ], ), ), From 7e2160d7ccf37fca61f918771ed7337b71fbd153 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:03:09 -0600 Subject: [PATCH 002/100] fix duplicate keys error --- .../address_book_views/subviews/contact_details_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index c0c10b3b1..a48a535c6 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -469,7 +469,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ..._cachedTransactions.map( (e) => TransactionCard( key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), transaction: e.item2, walletId: e.item1, ), @@ -499,7 +499,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ..._cachedTransactions.map( (e) => TransactionCard( key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), transaction: e.item2, walletId: e.item1, ), From e0ef78685ddf9450ac62140544b74c4c8ce8b068 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:10:28 -0600 Subject: [PATCH 003/100] empty contacts list fix --- .../address_book_views/address_book_view.dart | 175 +++++++----------- 1 file changed, 69 insertions(+), 106 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index fd2a995cc..147e677e0 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -99,8 +99,6 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - // final addressBookEntriesFuture = ref.watch( - // addressBookServiceProvider.select((value) => value.addressBookEntries)); final contacts = ref.watch(addressBookServiceProvider.select((value) => value.contacts)); @@ -280,58 +278,41 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - // FutureBuilder( - // future: addressBookEntriesFuture, - // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - // if (snapshot.connectionState == ConnectionState.done && - // snapshot.hasData) { - // _cacheFav = snapshot.data!; - // } - // if (_cacheFav == null) { - // // TODO proper loading animation - // return const LoadingIndicator(); - // } else { - // if (_cacheFav!.isNotEmpty) { - // return - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, + if (contacts.isNotEmpty) + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ], + ], + ), + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), ), - ) - // ; - // } else { - // return RoundedWhiteContainer( - // child: Center( - // child: Text( - // "Your favorite contacts will appear here", - // style: STextStyles.itemSubtitle(context), - // ), - // ), - // ); - // } - // } - // }, - // ) - , const SizedBox( height: 16, ), @@ -342,64 +323,46 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - // FutureBuilder( - // future: addressBookEntriesFuture, - // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - // if (snapshot.connectionState == ConnectionState.done && - // snapshot.hasData) { - // _cache = snapshot.data!; - // } - // if (_cache == null) { - // // TODO proper loading animation - // return const LoadingIndicator(); - // } else { - // if (_cache!.isNotEmpty) { - // return - Column( - children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, + if (contacts.isNotEmpty) + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ], + ], + ), ), ), + ], + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), - ], - ) - // ; - // } else { - // return RoundedWhiteContainer( - // child: Center( - // child: Text( - // "Your contacts will appear here", - // style: STextStyles.itemSubtitle(context), - // ), - // ), - // ); - // } - // } - // }, - // ) - , + ), ], ), ), From 7cc3c71b0d1949401d79dcbe453b0dd3783a8a27 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:22:53 -0600 Subject: [PATCH 004/100] desktop addressbook search --- .../address_book_views/address_book_view.dart | 306 +++++++++--------- .../desktop_address_book.dart | 14 +- 2 files changed, 158 insertions(+), 162 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 147e677e0..35e2601e2 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -23,11 +23,16 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookView extends ConsumerStatefulWidget { - const AddressBookView({Key? key, this.coin}) : super(key: key); + const AddressBookView({ + Key? key, + this.coin, + this.filterTerm, + }) : super(key: key); static const String routeName = "/addressBook"; final Coin? coin; + final String? filterTerm; @override ConsumerState<AddressBookView> createState() => _AddressBookViewState(); @@ -37,9 +42,6 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { late TextEditingController _searchController; final _searchFocusNode = FocusNode(); - // - // List<Contact>? _cache; - // List<Contact>? _cacheFav; String _searchTerm = ""; @@ -198,7 +200,12 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(4), - child: child, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - 271, + ), + child: child, + ), ), ), ), @@ -208,163 +215,156 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ), ); }, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - 271, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: !isDesktop - ? TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), - ) - : null, - ), - if (!isDesktop) const SizedBox(height: 16), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - if (contacts.isNotEmpty) - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ), - if (contacts.isEmpty) - RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - if (contacts.isNotEmpty) - Column( - children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], ), ), - ], - ), + ) + : null, ), - ), + ) + : null, + ), + if (!isDesktop) const SizedBox(height: 16), + Text( + "Favorites", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), ], ), - if (contacts.isEmpty) - RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), ), ), - ], - ), + ), + const SizedBox( + height: 16, + ), + Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + ), + ], + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index ec40e5f60..d561de946 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -34,11 +32,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { late final FocusNode _searchFocusNode; - List<Contact>? _cache; - List<Contact>? _cacheFav; - - late bool hasContacts = false; - String _searchTerm = ""; Future<void> selectCryptocurrency() async { @@ -90,7 +83,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; return DesktopScaffold( appBar: DesktopAppBar( @@ -171,7 +163,11 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { const SizedBox( height: 24, ), - const AddressBookView(), + Expanded( + child: AddressBookView( + filterTerm: _searchTerm, + ), + ), ], ), ), From 49103c86f1b63bd56563a0aa1167bcda5ee69cd8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 09:00:10 -0600 Subject: [PATCH 005/100] desktop addressbook layout fix --- .../desktop_address_book.dart | 442 +++++++++++++----- 1 file changed, 338 insertions(+), 104 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index d561de946..abb797aac 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -1,23 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import '../../../providers/providers.dart'; +import '../../../providers/ui/address_book_providers/address_book_filter_provider.dart'; + class DesktopAddressBook extends ConsumerStatefulWidget { const DesktopAddressBook({Key? key}) : super(key: key); @@ -69,6 +77,46 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { _searchController = TextEditingController(); _searchFocusNode = FocusNode(); + ref.refresh(addressBookFilterProvider); + + // if (widget.coin == null) { + List<Coin> coins = Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + coins.remove(Coin.firoTestNet); + + bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + + if (showTestNet) { + ref.read(addressBookFilterProvider).addAll(coins, false); + } else { + ref.read(addressBookFilterProvider).addAll( + coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + } + // } else { + // ref.read(addressBookFilterProvider).add(widget.coin!, false); + // } + + WidgetsBinding.instance.addPostFrameCallback((_) async { + List<ContactAddressEntry> addresses = []; + final managers = ref.read(walletsChangeNotifierProvider).managers; + for (final manager in managers) { + addresses.add( + ContactAddressEntry( + coin: manager.coin, + address: await manager.currentReceivingAddress, + label: "Current Receiving", + other: manager.walletName, + ), + ); + } + final self = Contact( + name: "My Stack", + addresses: addresses, + isFavorite: true, + id: "default", + ); + await ref.read(addressBookServiceProvider).editContact(self); + }); + super.initState(); } @@ -83,6 +131,32 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); + + final allContacts = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + ref.read(addressBookServiceProvider).matches(_searchTerm, e)); + + final favorites = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .where((element) => element.isFavorite); + + print("========================================================="); + print("contacts: ${contacts.length}"); + print("favorites: ${favorites.length}"); + print("allContacts: ${allContacts.length}"); + print("========================================================="); return DesktopScaffold( appBar: DesktopAppBar( @@ -100,121 +174,281 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), body: Padding( - padding: const EdgeInsets.all(24), - child: Row( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: DesktopAddressBookScaffold( + controlsLeft: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + controlsRight: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + width: 184, + label: "Filter", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + onPressed: selectCryptocurrency, + ), + const SizedBox( + width: 20, + ), + PrimaryButton( + width: 184, + label: "Add new", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.circlePlus, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + onPressed: newContact, + ), + ], + ), + filterItems: Container(), + upperLabel: favorites.isEmpty && allContacts.isEmpty + ? null + : Text( + favorites.isEmpty ? "All contacts" : "Favorites", + style: STextStyles.smallMed12(context), + ), + lowerLabel: favorites.isEmpty + ? null + : Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + ), + favorites: favorites.isNotEmpty + ? RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...favorites.map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ) + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + all: allContacts.isNotEmpty + ? Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...allContacts.map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + ), + ], + ) + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + details: Container( + color: Colors.purple, + ), + ), + ), + ); + } +} + +class DesktopAddressBookScaffold extends StatelessWidget { + const DesktopAddressBookScaffold({ + Key? key, + required this.controlsLeft, + required this.controlsRight, + required this.filterItems, + required this.upperLabel, + required this.lowerLabel, + required this.favorites, + required this.all, + required this.details, + }) : super(key: key); + + final Widget? controlsLeft; + final Widget? controlsRight; + final Widget? filterItems; + final Widget? upperLabel; + final Widget? lowerLabel; + final Widget? favorites; + final Widget? all; + final Widget? details; + + static const double weirdRowHeight = 30; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( flex: 6, - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 24, - ), - Expanded( - child: AddressBookView( - filterTerm: _searchTerm, - ), - ), - ], - ), + child: controlsLeft ?? Container(), ), const SizedBox( width: 20, ), Expanded( flex: 5, - child: Column( - children: [ - Row( - children: [ - SecondaryButton( - width: 184, - label: "Filter", - desktopMed: true, - icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - onPressed: selectCryptocurrency, - ), - const SizedBox( - width: 20, - ), - PrimaryButton( - width: 184, - label: "Add new", - desktopMed: true, - icon: SvgPicture.asset( - Assets.svg.circlePlus, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary, - ), - onPressed: newContact, - ), - ], - ), - ], - ), + child: controlsRight ?? Container(), ), ], ), - ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: filterItems ?? Container(), + ), + ], + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 6, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: weirdRowHeight, + child: upperLabel, + ), + favorites ?? Container(), + lowerLabel ?? Container(), + all ?? Container(), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + const SizedBox( + height: weirdRowHeight, + ), + Expanded( + child: details ?? Container(), + ), + ], + ), + ), + ], + ), + ) + ], ); } } From 8c0a6f5669de3d0bf4aa8b6c5329ff4d09e6bc30 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 09:04:54 -0600 Subject: [PATCH 006/100] address book search fixes --- .../address_book_views/address_book_view.dart | 1 + .../desktop_address_book.dart | 51 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 35e2601e2..cdc9fb5b7 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -261,6 +261,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { onTap: () async { setState(() { _searchController.text = ""; + _searchTerm = ""; }); }, ), diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index abb797aac..51b075284 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -152,12 +152,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ref.read(addressBookServiceProvider).matches(_searchTerm, e)) .where((element) => element.isFavorite); - print("========================================================="); - print("contacts: ${contacts.length}"); - print("favorites: ${favorites.length}"); - print("allContacts: ${allContacts.length}"); - print("========================================================="); - return DesktopScaffold( appBar: DesktopAppBar( isCompactHeight: true, @@ -222,6 +216,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { onTap: () async { setState(() { _searchController.text = ""; + _searchTerm = ""; }); }, ), @@ -284,8 +279,18 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { style: STextStyles.smallMed12(context), ), ), - favorites: favorites.isNotEmpty - ? RoundedWhiteContainer( + favorites: favorites.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: Column( children: [ @@ -297,17 +302,19 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ], ), - ) - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), ), - all: allContacts.isNotEmpty - ? Column( + all: allContacts.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : Column( children: [ RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -326,14 +333,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), ], - ) - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), ), details: Container( color: Colors.purple, From 72248d6a644807d6f7410c31529ae3a57869066f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:19 -0600 Subject: [PATCH 007/100] expandable fix --- .../wallet_network_settings_view.dart | 10 +++++----- .../sub_widgets/contact_list_item.dart | 2 +- lib/widgets/expandable.dart | 6 +++--- lib/widgets/node_card.dart | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index accf244eb..3044467aa 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -77,7 +77,7 @@ class _WalletNetworkSettingsViewState late double _percent; late int _blocksRemaining; - bool _advancedIsExpanded = true; + bool _advancedIsExpanded = false; Future<void> _attemptRescan() async { if (!Platform.isLinux) await Wakelock.enable(); @@ -855,8 +855,8 @@ class _WalletNetworkSettingsViewState ), SvgPicture.asset( _advancedIsExpanded - ? Assets.svg.chevronDown - : Assets.svg.chevronUp, + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, width: 12, height: 6, color: Theme.of(context) @@ -877,11 +877,11 @@ class _WalletNetworkSettingsViewState text: "Rescan", onTap: () async { await Navigator.of(context).push( - FadePageRoute<void>( + FadePageRoute<void>( ConfirmFullRescanDialog( onConfirm: _attemptRescan, ), - const RouteSettings(), + const RouteSettings(), ), ); // await showDialog<dynamic>( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index e030f9882..7acfaae9e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -58,7 +58,7 @@ class _ContactListItemState extends ConsumerState<ContactListItem> { ), child: AddressBookCard( contactId: contactId, - indicatorDown: _state == ExpandableState.expanded, + indicatorDown: _state, ), ), body: Column( diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index 47726d6d6..737f4ce7d 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; enum ExpandableState { - expanded, collapsed, + expanded, } class ExpandableController { @@ -45,11 +45,11 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { Future<void> toggle() async { if (animation.isDismissed) { await animationController.forward(); - _toggleState = ExpandableState.collapsed; + _toggleState = ExpandableState.expanded; widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { await animationController.reverse(); - _toggleState = ExpandableState.expanded; + _toggleState = ExpandableState.collapsed; widget.onExpandChanged?.call(_toggleState); } controller?.state = _toggleState; diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 1da7e9012..c3fb36c70 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -46,7 +46,7 @@ class NodeCard extends ConsumerStatefulWidget { class _NodeCardState extends ConsumerState<NodeCard> { String _status = "Disconnected"; late final String nodeId; - bool _advancedIsExpanded = true; + bool _advancedIsExpanded = false; Future<void> _notifyWalletsOfUpdatedNode(WidgetRef ref) async { final managers = ref @@ -367,8 +367,8 @@ class _NodeCardState extends ConsumerState<NodeCard> { if (isDesktop) SvgPicture.asset( _advancedIsExpanded - ? Assets.svg.chevronDown - : Assets.svg.chevronUp, + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, width: 12, height: 6, color: Theme.of(context) From df810c2a1449458e046aa994960b2ccdd5cbeecb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:38 -0600 Subject: [PATCH 008/100] "send from" contacts fix --- .../address_book_address_chooser.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart index 9f309a08e..92dd9f6fc 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -200,12 +200,19 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { final favorites = pullOutFavorites(contacts); - return ListView.builder( + final totalLength = favorites.length + + contacts.length + + 2; // +2 for "fav" and "all" headers + + return ListView.separated( primary: false, shrinkWrap: true, - itemCount: favorites.length + - contacts.length + - 2, // +2 for "fav" and "all" headers + itemCount: totalLength, + separatorBuilder: (context, index) { + return const SizedBox( + height: 10, + ); + }, itemBuilder: (context, index) { if (index == 0) { return Padding( @@ -220,7 +227,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { STextStyles.desktopTextExtraExtraSmall(context), ), ); - } else if (index <= favorites.length) { + } else if (index < favorites.length + 1) { final id = favorites[index - 1].id; return ContactListItem( key: Key("contactContactListItem_${id}_key"), @@ -241,7 +248,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { ), ); } else { - final id = contacts[index - favorites.length - 1].id; + final id = contacts[index - favorites.length - 2].id; return ContactListItem( key: Key("contactContactListItem_${id}_key"), contactId: id, From b988342bb146d1085f64527b4ce28f7765532c6f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:59 -0600 Subject: [PATCH 009/100] "send from" contact card fix --- lib/widgets/address_book_card.dart | 185 +++++++++++++++-------------- 1 file changed, 95 insertions(+), 90 deletions(-) diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index c9ac86052..329e35fdf 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -9,6 +9,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { @@ -19,7 +21,7 @@ class AddressBookCard extends ConsumerStatefulWidget { }) : super(key: key); final String contactId; - final bool? indicatorDown; + final ExpandableState? indicatorDown; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -58,108 +60,111 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { } } - return RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showDialog<void>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => ContactPopUp( - contactId: contact.id, + return ConditionalParent( + condition: !isDesktop, + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Theme.of(context) + .extension<StackColors>()! + .myStackContactIconBG + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Theme.of(context) - .extension<StackColors>()! - .myStackContactIconBG - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 20, - ), + child: Text(contact.emojiChar!), ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), - ), - const SizedBox( - width: 12, - ), - if (isDesktop) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 12, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + coinsString, + style: STextStyles.label(context), + ), + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( contact.name, style: STextStyles.itemSubtitle12(context), ), - if (isDesktop) const SizedBox( - width: 16, + height: 4, ), - if (isDesktop) Text( coinsString, style: STextStyles.label(context), ), - if (!isDesktop) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - contact.name, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 4, - ), - Text( - coinsString, - style: STextStyles.label(context), - ), - ], - ), - if (isDesktop) const Spacer(), - // if (isDesktop) - // SvgPicture.asset( - // widget.indicatorDown == true - // ? Assets.svg.chevronDown - // : Assets.svg.chevronUp, - // width: 10, - // height: 5, - // color: - // Theme.of(context).extension<StackColors>()!.textSubtitle2, - // ), - ], + ], + ), + if (isDesktop) const Spacer(), + if (isDesktop) + SvgPicture.asset( + widget.indicatorDown == ExpandableState.collapsed + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textSubtitle2, + ), + ], + ), + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showDialog<void>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => ContactPopUp( + contactId: contact.id, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: child, ), ), ), From b6e4357c3c63a6567fbdf2809ffb0b9c4b22b4a7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:23:12 -0600 Subject: [PATCH 010/100] wallet overview syncing/loading balance text color fix for darkmode --- .../wallet_view/sub_widgets/desktop_wallet_summary.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 7a9e93467..f4bfed976 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -243,7 +243,7 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { fontSize: 24, color: Theme.of(context) .extension<StackColors>()! - .textFavoriteCard, + .textDark, ), ), if (externalCalls) From 81d5f757b3d039528f523df3af8b8d1cca21a81e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 11:10:26 -0600 Subject: [PATCH 011/100] WIP: desktop contact details --- .../desktop_address_book.dart | 252 ++++++++---------- .../desktop_address_book_scaffold.dart | 111 ++++++++ .../subwidgets/desktop_contact_details.dart | 191 +++++++++++++ lib/widgets/address_book_card.dart | 9 +- 4 files changed, 427 insertions(+), 136 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 51b075284..5e22a6089 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -5,7 +5,11 @@ import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -19,13 +23,11 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../providers/providers.dart'; -import '../../../providers/ui/address_book_providers/address_book_filter_provider.dart'; - class DesktopAddressBook extends ConsumerStatefulWidget { const DesktopAddressBook({Key? key}) : super(key: key); @@ -42,6 +44,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { String _searchTerm = ""; + String? currentContactId; + Future<void> selectCryptocurrency() async { await showDialog<dynamic>( context: context, @@ -139,8 +143,9 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { .where((e) => ref.watch(addressBookFilterProvider .select((value) => value.coins.contains(e.coin)))) .isNotEmpty) - .where((e) => - ref.read(addressBookServiceProvider).matches(_searchTerm, e)); + .where( + (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .toList(); final favorites = contacts .where((element) => element.addresses @@ -150,7 +155,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { .where((e) => e.isFavorite && ref.read(addressBookServiceProvider).matches(_searchTerm, e)) - .where((element) => element.isFavorite); + .where((element) => element.isFavorite) + .toList(); return DesktopScaffold( appBar: DesktopAppBar( @@ -294,12 +300,56 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { padding: const EdgeInsets.all(0), child: Column( children: [ - ...favorites.map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, + for (int i = 0; i < favorites.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == favorites[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = favorites[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${favorites[i].id}_key"), + contactId: favorites[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], ), - ), ], ), ), @@ -318,136 +368,70 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { children: [ RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...allContacts.map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, - ), + child: Column( + children: [ + for (int i = 0; i < allContacts.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == allContacts[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = allContacts[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${allContacts[i].id}_key"), + contactId: allContacts[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], ), - ], - ), + ], ), ), ], ), - details: Container( - color: Colors.purple, - ), + details: currentContactId == null + ? Container() + : DesktopContactDetails( + contactId: currentContactId!, + ), ), ), ); } } - -class DesktopAddressBookScaffold extends StatelessWidget { - const DesktopAddressBookScaffold({ - Key? key, - required this.controlsLeft, - required this.controlsRight, - required this.filterItems, - required this.upperLabel, - required this.lowerLabel, - required this.favorites, - required this.all, - required this.details, - }) : super(key: key); - - final Widget? controlsLeft; - final Widget? controlsRight; - final Widget? filterItems; - final Widget? upperLabel; - final Widget? lowerLabel; - final Widget? favorites; - final Widget? all; - final Widget? details; - - static const double weirdRowHeight = 30; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - flex: 6, - child: controlsLeft ?? Container(), - ), - const SizedBox( - width: 20, - ), - Expanded( - flex: 5, - child: controlsRight ?? Container(), - ), - ], - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - Expanded( - child: filterItems ?? Container(), - ), - ], - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 6, - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: weirdRowHeight, - child: upperLabel, - ), - favorites ?? Container(), - lowerLabel ?? Container(), - all ?? Container(), - ], - ), - ), - ), - ); - }, - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - flex: 5, - child: Column( - children: [ - const SizedBox( - height: weirdRowHeight, - ), - Expanded( - child: details ?? Container(), - ), - ], - ), - ), - ], - ), - ) - ], - ); - } -} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart new file mode 100644 index 000000000..f32ea1f7f --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart @@ -0,0 +1,111 @@ +import 'package:flutter/widgets.dart'; + +class DesktopAddressBookScaffold extends StatelessWidget { + const DesktopAddressBookScaffold({ + Key? key, + required this.controlsLeft, + required this.controlsRight, + required this.filterItems, + required this.upperLabel, + required this.lowerLabel, + required this.favorites, + required this.all, + required this.details, + }) : super(key: key); + + final Widget? controlsLeft; + final Widget? controlsRight; + final Widget? filterItems; + final Widget? upperLabel; + final Widget? lowerLabel; + final Widget? favorites; + final Widget? all; + final Widget? details; + + static const double weirdRowHeight = 30; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + flex: 6, + child: controlsLeft ?? Container(), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: controlsRight ?? Container(), + ), + ], + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: filterItems ?? Container(), + ), + ], + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 6, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + primary: false, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: weirdRowHeight, + child: upperLabel, + ), + favorites ?? Container(), + lowerLabel ?? Container(), + all ?? Container(), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + const SizedBox( + height: weirdRowHeight, + ), + Expanded( + child: details ?? Container(), + ), + ], + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart new file mode 100644 index 000000000..5184b2293 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopContactDetails extends ConsumerStatefulWidget { + const DesktopContactDetails({ + Key? key, + required this.contactId, + }) : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactDetails> createState() => + _DesktopContactDetailsState(); +} + +class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { + @override + Widget build(BuildContext context) { + final contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(widget.contactId))); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 16, + ), + Text( + contact.name, + style: STextStyles.desktopTextSmall(context), + ), + ], + ), + SecondaryButton( + label: "Options", + onPressed: () {}, + ), + ], + ), + const SizedBox( + height: 24, + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + BlueTextButton( + text: "Add new", + onTap: () {}, + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...contact.addresses + .map((e) => AddressCard(entry: e)), + ], + ) + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } +} + +class AddressCard extends StatelessWidget { + const AddressCard({ + Key? key, + required this.entry, + }) : super(key: key); + + final ContactAddressEntry entry; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: entry.coin, + ), + height: 32, + width: 32, + ), + const SizedBox( + width: 16, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "${entry.label} ${entry.coin.ticker}", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + entry.address, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + BlueTextButton( + text: "Copy", + onTap: () {}, + ), + const SizedBox( + width: 16, + ), + BlueTextButton( + text: "Edit", + onTap: () {}, + ), + ], + ) + ], + ), + ], + ); + } +} diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index 329e35fdf..b79f89662 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -18,10 +18,12 @@ class AddressBookCard extends ConsumerStatefulWidget { Key? key, required this.contactId, this.indicatorDown, + this.desktopSendFrom = true, }) : super(key: key); final String contactId; final ExpandableState? indicatorDown; + final bool desktopSendFrom; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -30,10 +32,12 @@ class AddressBookCard extends ConsumerStatefulWidget { class _AddressBookCardState extends ConsumerState<AddressBookCard> { late final String contactId; late final bool isDesktop; + late final bool desktopSendFrom; @override void initState() { contactId = widget.contactId; + desktopSendFrom = widget.desktopSendFrom; isDesktop = Util.isDesktop; super.initState(); } @@ -107,6 +111,7 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { const SizedBox( width: 16, ), + if (isDesktop && !desktopSendFrom) const Spacer(), if (isDesktop) Text( coinsString, @@ -129,8 +134,8 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { ), ], ), - if (isDesktop) const Spacer(), - if (isDesktop) + if (isDesktop && desktopSendFrom) const Spacer(), + if (isDesktop && desktopSendFrom) SvgPicture.asset( widget.indicatorDown == ExpandableState.collapsed ? Assets.svg.chevronDown From 9063749eadcaf1ab414b37db7591d55049efcfe3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 10:42:11 -0700 Subject: [PATCH 012/100] anonymize button added to firo wallet --- .../wallet_view/desktop_wallet_view.dart | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 81b531632..769f22157 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,6 +16,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_vie import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; @@ -27,8 +29,11 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -165,6 +170,75 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } } + Future<void> attemptAnonymize() async { + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + final firoWallet = ref.read(managerProvider).wallet as FiroWallet; + + final publicBalance = await firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "No funds available to anonymize!", + context: context, + ), + ); + } + return; + } + + try { + await firoWallet.anonymizeAllPublicFunds(); + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), + ); + } + } + } + @override void initState() { controller = TextEditingController(); @@ -333,6 +407,67 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { : WalletSyncStatus.synced, ), const Spacer(), + if (coin == Coin.firo) const SizedBox(width: 10), + if (coin == Coin.firo) + SecondaryButton( + width: 180, + desktopMed: true, + label: "Anonymize funds", + onPressed: () async { + await showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: 210, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 20), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "You're about to anonymize all of your public funds.", + style: + STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 180, + desktopMed: true, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 180, + desktopMed: true, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + ) + ], + ), + ], + ), + ), + ), + ); + }, + ), + if (coin == Coin.firo) const SizedBox(width: 16), SecondaryButton( width: 180, desktopMed: true, From 9e7c1ccf9dc20e08ac74120e7faf866e1db3f103 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 11:46:44 -0600 Subject: [PATCH 013/100] button size enum --- .../subviews/address_book_filter_view.dart | 4 +- .../generate_receiving_uri_qr_code_view.dart | 5 +- .../send_view/confirm_transaction_view.dart | 4 +- .../building_transaction_dialog.dart | 2 +- .../global_settings_view/currency_view.dart | 4 +- .../add_edit_node_view.dart | 8 +- .../manage_nodes_views/node_details_view.dart | 4 +- .../create_backup_view.dart | 8 +- .../dialogs/cancel_stack_restore_dialog.dart | 4 +- .../edit_auto_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../stack_restore_progress_view.dart | 8 +- .../sub_widgets/confirm_full_rescan.dart | 4 +- .../all_transactions_view.dart | 2 +- .../transaction_search_filter_view.dart | 4 +- .../desktop_address_book.dart | 4 +- .../wallet_view/desktop_wallet_view.dart | 2 +- .../sub_widgets/desktop_auth_send.dart | 4 +- .../sub_widgets/desktop_receive.dart | 2 +- .../wallet_view/sub_widgets/desktop_send.dart | 10 +-- .../backup_and_restore_settings.dart | 14 +-- .../create_auto_backup.dart | 7 +- .../enable_backup_dialog.dart | 4 +- .../currency_settings/currency_settings.dart | 2 +- .../language_settings/language_settings.dart | 2 +- .../home/settings_menu/security_settings.dart | 4 +- .../syncing_preferences_settings.dart | 2 +- lib/widgets/desktop/custom_text_button.dart | 10 +++ lib/widgets/desktop/primary_button.dart | 86 +++++++++++++++--- lib/widgets/desktop/secondary_button.dart | 89 ++++++++++++++++--- 30 files changed, 225 insertions(+), 86 deletions(-) diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index df779331e..c129251d5 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -159,7 +159,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { children: [ SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel", onPressed: () { @@ -169,7 +169,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { // const SizedBox(width: 16), PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Apply", onPressed: () { diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 981def830..05cedb148 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -530,7 +530,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { }); } : onGeneratePressed, - desktopMed: true, + buttonHeight: ButtonHeight.l, ), if (isDesktop && didGenerate) Row( @@ -586,7 +586,6 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { if (!isDesktop) SecondaryButton( width: 170, - desktopMed: true, onPressed: () async { await _capturePng(false); }, @@ -606,7 +605,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { ), PrimaryButton( width: 170, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { // TODO: add save functionality instead of share // save works on linux at the moment diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 0f1692c08..8f7afb0bb 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -148,7 +148,7 @@ class _ConfirmTransactionViewState const Spacer(), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Ok", onPressed: Navigator.of(context).pop, ), @@ -780,7 +780,7 @@ class _ConfirmTransactionViewState : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { final dynamic unlocked; diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index 045218e54..1f6c95df6 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -77,7 +77,7 @@ class _RestoringDialogState extends State<BuildingTransactionDialog> height: 40, ), SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { onCancel.call(); diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index 4e8fd5f6e..dccf2d61b 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -189,7 +189,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -199,7 +199,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { Expanded( child: PrimaryButton( label: "Save changes", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { ref.read(prefsChangeNotifierProvider).currency = current; diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 890953caf..606c4481f 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -238,7 +238,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -251,7 +251,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: PrimaryButton( label: "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -561,7 +561,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { child: SecondaryButton( label: "Test connection", enabled: testConnectionEnabled, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: testConnectionEnabled ? () async { await _testConnection(); @@ -578,7 +578,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { child: PrimaryButton( label: "Save", enabled: saveEnabled, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: saveEnabled ? attemptSave : null, ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index a80a64147..3d49ae6f7 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -349,7 +349,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { Expanded( child: SecondaryButton( label: "Test connection", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { await _testConnection(ref, context); }, @@ -364,7 +364,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { child: !nodeId.startsWith("default") ? PrimaryButton( label: _desktopReadOnly ? "Edit" : "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { final shouldSave = _desktopReadOnly == false; setState(() { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 02556beb9..0609e4b1b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -562,7 +562,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { Consumer(builder: (context, ref, __) { return PrimaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Create backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -735,7 +735,9 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { child: PrimaryButton( label: "Ok", - desktopMed: true, + buttonHeight: + ButtonHeight + .l, onPressed: () { int count = 0; Navigator.of( @@ -778,7 +780,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), SecondaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () {}, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart index a9f4e134d..905cdea72 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart @@ -85,7 +85,7 @@ class CancelStackRestoreDialog extends StatelessWidget { children: [ SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Keep restoring", onPressed: () { @@ -95,7 +95,7 @@ class CancelStackRestoreDialog extends StatelessWidget { const SizedBox(width: 20), PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel anyway", onPressed: () { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 76d280980..310be9f2b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -754,7 +754,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -764,7 +764,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Expanded( child: PrimaryButton( label: "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: shouldEnableCreate, onPressed: onSavePressed, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index c5ccfa6b3..9be6af4cb 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -389,7 +389,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { children: [ PrimaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Restore", enabled: !(passwordController.text.isEmpty || fileLocationController.text.isEmpty), @@ -566,7 +566,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), SecondaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () {}, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart index e34def23d..c7f53378d 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart @@ -108,7 +108,7 @@ class _StackRestoreProgressViewState // children: [ // SecondaryButton( // width: 248, - // desktopMed: true, + // buttonHeight: ButtonHeight.l, // enabled: true, // label: "Keep restoring", // onPressed: () { @@ -118,7 +118,7 @@ class _StackRestoreProgressViewState // const SizedBox(width: 16), // PrimaryButton( // width: 248, - // desktopMed: true, + // buttonHeight: ButtonHeight.l, // enabled: true, // label: "Cancel anyway", // onPressed: () { @@ -681,7 +681,7 @@ class _StackRestoreProgressViewState _success ? PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Done", onPressed: () async { @@ -690,7 +690,7 @@ class _StackRestoreProgressViewState ) : SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel restore process", onPressed: () async { diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart index 950d8d79e..150af6ac5 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart @@ -58,7 +58,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, label: "Cancel", ), @@ -68,7 +68,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { Navigator.of(context).pop(); onConfirm.call(); diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index d41877a9a..95dcc8126 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -385,7 +385,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ), if (isDesktop) SecondaryButton( - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, width: 200, label: "Filter", icon: SvgPicture.asset( diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index abebb71e4..d135ea276 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -869,7 +869,7 @@ class _TransactionSearchViewState Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, onPressed: () async { if (!isDesktop) { if (FocusScope.of(context).hasFocus) { @@ -919,7 +919,7 @@ class _TransactionSearchViewState ), Expanded( child: PrimaryButton( - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, onPressed: () async { await _onApplyPressed(); }, diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 5e22a6089..f028a3424 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -240,7 +240,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { SecondaryButton( width: 184, label: "Filter", - desktopMed: true, + buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.filter, color: Theme.of(context) @@ -255,7 +255,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { PrimaryButton( width: 184, label: "Add new", - desktopMed: true, + buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.circlePlus, color: Theme.of(context) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 769f22157..952194244 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -470,7 +470,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (coin == Coin.firo) const SizedBox(width: 16), SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { _onExchangePressed(context); }, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index 566a82b35..9f863c8a4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -142,7 +142,7 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -153,7 +153,7 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { child: PrimaryButton( enabled: _confirmEnabled, label: "Confirm", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { // TODO show spinner while verifying passphrase diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 9a59c3ec1..3de4ed1e3 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -199,7 +199,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { ), if (coin != Coin.epicCash) SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: generateNewAddress, label: "Generate new address", ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index af2c2517a..336dd7b4e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -145,7 +145,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { right: 32, ), child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Ok", onPressed: () { Navigator.of(context).pop(); @@ -232,7 +232,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(false); @@ -244,7 +244,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Yes", onPressed: () { Navigator.of(context).pop(true); @@ -399,7 +399,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), child: Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Yes", onPressed: () { Navigator.of( @@ -1385,7 +1385,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { height: 36, ), PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Preview send", enabled: ref.watch(previewTxButtonStateProvider.state).state, onPressed: ref.watch(previewTxButtonStateProvider.state).state diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 7d70d4d0f..37f3f35a6 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -240,7 +240,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: Navigator.of(context).pop, ), @@ -248,7 +248,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { const SizedBox(width: 16), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Disable", onPressed: () { ref @@ -422,7 +422,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { padding: const EdgeInsets.all(10), child: !isEnabledAutoBackup ? PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Enable auto backup", onPressed: () { @@ -467,7 +467,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { Row( children: [ PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 190, label: "Disable auto backup", onPressed: () { @@ -476,7 +476,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), const SizedBox(width: 16), SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 190, label: "Edit auto backup", onPressed: () { @@ -560,7 +560,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: CreateBackupView(), ) : PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Create manual backup", onPressed: () { @@ -642,7 +642,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: RestoreFromFileView(), ) : PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Restore backup", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 663136dba..df80da732 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -556,7 +556,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -565,7 +565,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Enable Auto Backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -792,7 +792,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Expanded( child: PrimaryButton( label: "Ok", - desktopMed: true, + buttonHeight: + ButtonHeight.l, onPressed: () { Navigator.of(context) .pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 6496253d5..df9f18b52 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -59,7 +59,7 @@ class EnableBackupDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(); @@ -71,7 +71,7 @@ class EnableBackupDialog extends StatelessWidget { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { Navigator.of(context).pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index 4c4225ce4..0740157ad 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -108,7 +108,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change currency", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 08aeb9bc3..db636ba17 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -85,7 +85,7 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change language", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 9f870440b..f2853e6f5 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -485,7 +485,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { const SizedBox(height: 20), PrimaryButton( width: 160, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: shouldEnableSave, label: "Save changes", onPressed: () async { @@ -503,7 +503,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ) : PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Set up new password", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 408b93e15..ae11f5582 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -83,7 +83,7 @@ class _SyncingPreferencesSettings ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change preferences", onPressed: () {}, diff --git a/lib/widgets/desktop/custom_text_button.dart b/lib/widgets/desktop/custom_text_button.dart index b96a697b8..90b75c459 100644 --- a/lib/widgets/desktop/custom_text_button.dart +++ b/lib/widgets/desktop/custom_text_button.dart @@ -1,6 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/util.dart'; +enum ButtonHeight { + xxs, + xs, + s, + m, + l, + xl, + xxl, +} + class CustomTextButtonBase extends StatelessWidget { const CustomTextButtonBase({ Key? key, diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index f3c900c34..134ff36c3 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class PrimaryButton extends StatelessWidget { const PrimaryButton({ Key? key, @@ -13,7 +15,7 @@ class PrimaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, - this.desktopMed = false, + this.buttonHeight, }) : super(key: key); final double? width; @@ -22,23 +24,44 @@ class PrimaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; - final bool desktopMed; + final ButtonHeight? buttonHeight; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { - if (desktopMed) { - return STextStyles.desktopTextExtraSmall(context).copyWith( - color: enabled - ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ); - } else { + if (buttonHeight == null) { return enabled ? STextStyles.desktopButtonEnabled(context) : STextStyles.desktopButtonDisabled(context); } + + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } } else { return STextStyles.button(context).copyWith( color: enabled @@ -50,12 +73,51 @@ class PrimaryButton extends StatelessWidget { } } + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 28; + case ButtonHeight.xs: + return 32; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: desktopMed ? 56 : height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 8d5eae0ce..7cf8e9f72 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class SecondaryButton extends StatelessWidget { const SecondaryButton({ Key? key, @@ -13,7 +15,7 @@ class SecondaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, - this.desktopMed = false, + this.buttonHeight, }) : super(key: key); final double? width; @@ -22,23 +24,47 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; - final bool desktopMed; + final ButtonHeight? buttonHeight; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { - if (desktopMed) { - return STextStyles.desktopTextExtraSmall(context).copyWith( - color: enabled - ? Theme.of(context).extension<StackColors>()!.buttonTextSecondary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondaryDisabled, - ); - } else { + if (buttonHeight == null) { return enabled ? STextStyles.desktopButtonSecondaryEnabled(context) : STextStyles.desktopButtonSecondaryDisabled(context); } + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context); + } } else { return STextStyles.button(context).copyWith( color: enabled @@ -50,12 +76,51 @@ class SecondaryButton extends StatelessWidget { } } + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 28; + case ButtonHeight.xs: + return 32; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: desktopMed ? 56 : height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, From 1d238c29f09207b975968c27707df5097de218ac Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:01:52 -0600 Subject: [PATCH 014/100] WIP: centralize button heights --- .../create_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../wallet_view/desktop_wallet_view.dart | 6 +- .../advanced_settings/advanced_settings.dart | 111 +++++------------- .../backup_and_restore_settings.dart | 10 +- .../currency_settings/currency_settings.dart | 2 +- .../language_settings/language_settings.dart | 4 +- .../home/settings_menu/security_settings.dart | 2 +- .../syncing_preferences_settings.dart | 2 +- lib/widgets/desktop/primary_button.dart | 4 +- lib/widgets/desktop/secondary_button.dart | 4 +- 11 files changed, 52 insertions(+), 101 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 0609e4b1b..a6241d25a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -562,7 +562,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { Consumer(builder: (context, ref, __) { return PrimaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Create backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -780,7 +780,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), SecondaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Cancel", onPressed: () {}, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 9be6af4cb..d6571967d 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -389,7 +389,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { children: [ PrimaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Restore", enabled: !(passwordController.text.isEmpty || fileLocationController.text.isEmpty), @@ -566,7 +566,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), SecondaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Cancel", onPressed: () {}, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 952194244..d21a19aee 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -411,7 +411,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (coin == Coin.firo) SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Anonymize funds", onPressed: () async { await showDialog<void>( @@ -441,7 +441,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { children: [ SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(); @@ -450,7 +450,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox(width: 20), PrimaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { Navigator.of(context).pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index b4ff3fe6a..621683e65 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'debug_info_dialog.dart'; @@ -143,7 +144,21 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ), ], ), - const StackPrivacyButton(), + PrimaryButton( + label: "Change", + buttonHeight: ButtonHeight.xs, + width: 86, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackPrivacyDialog(); + }, + ); + }, + ) ], ), ); @@ -172,7 +187,21 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { .textDark), textAlign: TextAlign.left, ), - ShowLogsButton(), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Show logs", + width: 101, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + }, + ), ], ), ), @@ -184,81 +213,3 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ); } } - -class StackPrivacyButton extends ConsumerWidget { - const StackPrivacyButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> changePrivacySettings() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackPrivacyDialog(); - }, - ); - } - - return SizedBox( - width: 84, - height: 37, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - // Navigator.of(context).pushNamed( - // StackPrivacyCalls.routeName, - // arguments: false, - // ); - changePrivacySettings(); - }, - child: Text( - "Change", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith(color: Colors.white), - ), - ), - ); - } -} - -class ShowLogsButton extends ConsumerWidget { - const ShowLogsButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> viewDebugLogs() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const DebugInfoDialog(); - }, - ); - } - - return SizedBox( - width: 101, - height: 37, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - viewDebugLogs(); - }, - child: Text( - "Show logs", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith(color: Colors.white), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 37f3f35a6..c82b5f923 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -422,7 +422,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { padding: const EdgeInsets.all(10), child: !isEnabledAutoBackup ? PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Enable auto backup", onPressed: () { @@ -467,7 +467,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { Row( children: [ PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 190, label: "Disable auto backup", onPressed: () { @@ -476,7 +476,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), const SizedBox(width: 16), SecondaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 190, label: "Edit auto backup", onPressed: () { @@ -560,7 +560,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: CreateBackupView(), ) : PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Create manual backup", onPressed: () { @@ -642,7 +642,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: RestoreFromFileView(), ) : PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Restore backup", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index 0740157ad..d9c20d8fa 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -108,7 +108,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change currency", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index db636ba17..acddcb055 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -80,12 +80,12 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change language", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index f2853e6f5..f6762afa1 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -503,7 +503,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ) : PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Set up new password", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index ae11f5582..815e506db 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -83,7 +83,7 @@ class _SyncingPreferencesSettings ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change preferences", onPressed: () {}, diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index 134ff36c3..9441168e7 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -81,9 +81,9 @@ class PrimaryButton extends StatelessWidget { if (Util.isDesktop) { switch (buttonHeight!) { case ButtonHeight.xxs: - return 28; - case ButtonHeight.xs: return 32; + case ButtonHeight.xs: + return 37; case ButtonHeight.s: return 40; case ButtonHeight.m: diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 7cf8e9f72..62bd900dd 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -84,9 +84,9 @@ class SecondaryButton extends StatelessWidget { if (Util.isDesktop) { switch (buttonHeight!) { case ButtonHeight.xxs: - return 28; - case ButtonHeight.xs: return 32; + case ButtonHeight.xs: + return 37; case ButtonHeight.s: return 40; case ButtonHeight.m: From 95a9fade38c065fb0d578b8ea92f4c83ca6b1a1a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:17:55 -0600 Subject: [PATCH 015/100] desktop contact address details --- .../desktop_address_book.dart | 7 +- .../subwidgets/desktop_address_card.dart | 75 ++++++++++++ .../subwidgets/desktop_contact_details.dart | 108 ++++++------------ 3 files changed, 116 insertions(+), 74 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index f028a3424..fd25617e7 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -427,8 +427,11 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), details: currentContactId == null ? Container() - : DesktopContactDetails( - contactId: currentContactId!, + : RoundedWhiteContainer( + padding: const EdgeInsets.all(24), + child: DesktopContactDetails( + contactId: currentContactId!, + ), ), ), ), diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart new file mode 100644 index 000000000..49b75a4a8 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; + +class DesktopAddressCard extends StatelessWidget { + const DesktopAddressCard({ + Key? key, + required this.entry, + }) : super(key: key); + + final ContactAddressEntry entry; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: entry.coin, + ), + height: 32, + width: 32, + ), + const SizedBox( + width: 16, + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "${entry.label} (${entry.coin.ticker})", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + entry.address, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + BlueTextButton( + text: "Copy", + onTap: () {}, + ), + const SizedBox( + width: 16, + ), + BlueTextButton( + text: "Edit", + onTap: () {}, + ), + ], + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 5184b2293..bc19fe3bd 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ @@ -74,6 +74,8 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), SecondaryButton( label: "Options", + width: 86, + buttonHeight: ButtonHeight.xxs, onPressed: () {}, ), ], @@ -106,12 +108,38 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), ], ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...contact.addresses - .map((e) => AddressCard(entry: e)), - ], + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < contact.addresses.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(18), + child: DesktopAddressCard( + entry: contact.addresses[i], + ), + ), + ], + ), + ], + ), ) ], ), @@ -125,67 +153,3 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } - -class AddressCard extends StatelessWidget { - const AddressCard({ - Key? key, - required this.entry, - }) : super(key: key); - - final ContactAddressEntry entry; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor( - coin: entry.coin, - ), - height: 32, - width: 32, - ), - const SizedBox( - width: 16, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "${entry.label} ${entry.coin.ticker}", - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - ), - const SizedBox( - height: 2, - ), - SelectableText( - entry.address, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - BlueTextButton( - text: "Copy", - onTap: () {}, - ), - const SizedBox( - width: 16, - ), - BlueTextButton( - text: "Edit", - onTap: () {}, - ), - ], - ) - ], - ), - ], - ); - } -} From 682966dab83a391ef3224c67833cc0d333a0cba9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 14:08:06 -0700 Subject: [PATCH 016/100] desktop block explorer dialog --- .../transaction_details_view.dart | 200 ++++++++++++------ .../wallet_view/desktop_wallet_view.dart | 4 +- 2 files changed, 142 insertions(+), 62 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 1c2fb8e5d..dc4e41152 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -30,6 +30,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/copy_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -154,60 +156,136 @@ class _TransactionDetailsViewState Future<bool> showExplorerWarning(String explorer) async { final bool? shouldContinue = await showDialog<bool>( - context: context, - barrierDismissible: false, - builder: (_) => StackDialog( - title: "Attention", - message: - "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", - icon: Row( - children: [ - Consumer(builder: (_, ref, __) { - return Checkbox( - value: ref.watch(prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning)), - onChanged: (value) { - if (value is bool) { - ref - .read(prefsChangeNotifierProvider) - .hideBlockExplorerWarning = value; - setState(() {}); - } + context: context, + barrierDismissible: false, + builder: (_) { + if (!isDesktop) { + return StackDialog( + title: "Attention", + message: + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + icon: Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(false); }, - ); - }), - Text( - "Never show again", - style: STextStyles.smallMed14(context), - ) - ], - ), - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ); + } else { + return DesktopDialog( + maxWidth: 550, + maxHeight: 300, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Attention", + style: STextStyles.desktopH2(context), + ), + Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => + value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + ], + ), + const SizedBox(height: 16), + Text( + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 35), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ], + ), + ), + ); + } + }); return shouldContinue ?? false; } @@ -995,15 +1073,17 @@ class _TransactionDetailsViewState .externalApplication, ); } catch (_) { - unawaited(showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", + unawaited( + showDialog<void>( + context: context, + builder: (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), - )); + ); } finally { // Future<void>.delayed( // const Duration(seconds: 1), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index d21a19aee..a7de8fdf4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -440,7 +440,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { mainAxisAlignment: MainAxisAlignment.center, children: [ SecondaryButton( - width: 180, + width: 200, buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { @@ -449,7 +449,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ), const SizedBox(width: 20), PrimaryButton( - width: 180, + width: 200, buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { From 11735cdaf7d8e5393687c2ec89893a72908d7d8a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:56:10 -0600 Subject: [PATCH 017/100] desktop emoji select --- .../subviews/add_address_book_entry_view.dart | 132 ++++-------- lib/widgets/emoji_select_sheet.dart | 188 ++++++++++-------- 2 files changed, 151 insertions(+), 169 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 5835c80cd..0007f3d81 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -191,33 +191,33 @@ class _AddAddressBookEntryViewState style: STextStyles.desktopH3(context), textAlign: TextAlign.center, ), - const SizedBox(width: 10), - AppBarIconButton( - key: - const Key("addAddressBookEntryFavoriteButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension<StackColors>()! - .background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, - ), - onPressed: () { - setState(() { - _isFavorite = !_isFavorite; - }); - }, - ), + // const SizedBox(width: 10), + // AppBarIconButton( + // key: + // const Key("addAddressBookEntryFavoriteButtonKey"), + // size: 36, + // shadows: const [], + // color: Theme.of(context) + // .extension<StackColors>()! + // .background, + // icon: SvgPicture.asset( + // Assets.svg.star, + // color: _isFavorite + // ? Theme.of(context) + // .extension<StackColors>()! + // .favoriteStarActive + // : Theme.of(context) + // .extension<StackColors>()! + // .favoriteStarInactive, + // width: 20, + // height: 20, + // ), + // onPressed: () { + // setState(() { + // _isFavorite = !_isFavorite; + // }); + // }, + // ), ], ), ), @@ -225,10 +225,11 @@ class _AddAddressBookEntryViewState ], ), Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: child, - )), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: child, + ), + ), ], ); }, @@ -292,66 +293,17 @@ class _AddAddressBookEntryViewState : showDialog<dynamic>( context: context, builder: (context) { - return DesktopDialog( + return const DesktopDialog( maxHeight: 700, - maxWidth: 700, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: - const EdgeInsets - .all(32), - child: Text( - "Select emoji", - style: STextStyles - .desktopH3( - context), - textAlign: - TextAlign - .center, - ), - ), - ], - ), - Expanded( - child: LayoutBuilder( - builder: (context, - constraints) { - return SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: - ConstrainedBox( - constraints: - BoxConstraints( - minHeight: - constraints - .maxHeight, - minWidth: - constraints - .maxWidth, - ), - child: - IntrinsicHeight( - child: Column( - children: const [ - Padding( - padding: - EdgeInsets.symmetric(horizontal: 32), - // child: - // EmojiSelectSheet(), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ], + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), ), ); }).then((value) { diff --git a/lib/widgets/emoji_select_sheet.dart b/lib/widgets/emoji_select_sheet.dart index 85a90fec8..7bf02e967 100644 --- a/lib/widgets/emoji_select_sheet.dart +++ b/lib/widgets/emoji_select_sheet.dart @@ -4,6 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class EmojiSelectSheet extends ConsumerWidget { const EmojiSelectSheet({ @@ -16,7 +19,9 @@ class EmojiSelectSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final size = MediaQuery.of(context).size; + final isDesktop = Util.isDesktop; + + final size = isDesktop ? const Size(600, 700) : MediaQuery.of(context).size; final double maxHeight = size.height * 0.60; final double availableWidth = size.width - (2 * horizontalPadding); final int emojisPerRow = @@ -24,90 +29,115 @@ class EmojiSelectSheet extends ConsumerWidget { final itemCount = Emoji.all().length; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: LimitedBox( + maxHeight: maxHeight, + child: Padding( + padding: EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + top: 10, + bottom: 0, + ), + child: child, + ), ), ), - child: LimitedBox( - maxHeight: maxHeight, - child: Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - top: 10, - bottom: 0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - width: 60, - height: 4, ), + width: 60, + height: 4, ), - const SizedBox( - height: 36, - ), - Text( - "Select emoji", - style: STextStyles.pageTitleH2(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16, - ), - Flexible( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: GridView.builder( - itemCount: itemCount, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: emojisPerRow, - ), - itemBuilder: (context, index) { - final emoji = Emoji.all()[index]; - return GestureDetector( - onTap: () { - Navigator.of(context).pop(emoji); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(emoji.char), - ), - ), - ); - }, - ), - ) - ], - ), - ), - const SizedBox( - height: 24, - ), - ], + ), + if (!isDesktop) + const SizedBox( + height: 36, + ), + Text( + "Select emoji", + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + textAlign: TextAlign.left, ), - ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: GridView.builder( + itemCount: itemCount, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: emojisPerRow, + ), + itemBuilder: (context, index) { + final emoji = Emoji.all()[index]; + return GestureDetector( + onTap: () { + Navigator.of(context).pop(emoji); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + emoji.char, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : null, + ), + ), + ), + ); + }, + ), + ) + ], + ), + ), + SizedBox( + height: isDesktop ? 20 : 24, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + label: "Cancel", + width: 248, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ], ), ); } From 134087bfc4c18c3482e49d6b1e7a16b9f0a78032 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 13:15:57 -0600 Subject: [PATCH 018/100] desktop add contact popup tweaks --- .../subviews/add_address_book_entry_view.dart | 375 ++++++------------ .../new_contact_address_entry_form.dart | 2 +- 2 files changed, 121 insertions(+), 256 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 0007f3d81..bb93c68d8 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -21,6 +21,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -191,33 +193,6 @@ class _AddAddressBookEntryViewState style: STextStyles.desktopH3(context), textAlign: TextAlign.center, ), - // const SizedBox(width: 10), - // AppBarIconButton( - // key: - // const Key("addAddressBookEntryFavoriteButtonKey"), - // size: 36, - // shadows: const [], - // color: Theme.of(context) - // .extension<StackColors>()! - // .background, - // icon: SvgPicture.asset( - // Assets.svg.star, - // color: _isFavorite - // ? Theme.of(context) - // .extension<StackColors>()! - // .favoriteStarActive - // : Theme.of(context) - // .extension<StackColors>()! - // .favoriteStarInactive, - // width: 20, - // height: 20, - // ), - // onPressed: () { - // setState(() { - // _isFavorite = !_isFavorite; - // }); - // }, - // ), ], ), ), @@ -226,7 +201,11 @@ class _AddAddressBookEntryViewState ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.only( + left: 10, + right: 10, + bottom: 32, + ), child: child, ), ), @@ -239,16 +218,17 @@ class _AddAddressBookEntryViewState padding: const EdgeInsets.symmetric(horizontal: 12), child: SingleChildScrollView( controller: scrollController, - padding: const EdgeInsets.only( + padding: EdgeInsets.only( // top: 8, left: 4, right: 4, - bottom: 16, + bottom: isDesktop ? 0 : 16, ), child: ConstrainedBox( constraints: BoxConstraints( // subtract top and bottom padding set in parent - minHeight: constraint.maxHeight - 16, // - 8, + minHeight: + constraint.maxHeight - (isDesktop ? 0 : 16), // - 8, ), child: IntrinsicHeight( child: Column( @@ -259,38 +239,21 @@ class _AddAddressBookEntryViewState mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } + SizedBox( + height: 56, + width: 56, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } - ///TODO if desktop make dialog - !isDesktop - ? showModalBottomSheet<dynamic>( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }) - : showDialog<dynamic>( + showDialog<dynamic>( context: context, builder: (context) { return const DesktopDialog( @@ -307,77 +270,80 @@ class _AddAddressBookEntryViewState ), ); }).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 56, - width: 56, - child: Stack( - children: [ - Container( - height: 56, - width: 56, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 30, - width: 30, - ) - : Text( - _selectedEmoji!.char, - style: STextStyles - .pageTitleH1(context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: Stack( + children: [ + Container( + height: 56, + width: 56, decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + borderRadius: + BorderRadius.circular(100), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), child: Center( child: _selectedEmoji == null ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, - width: 12, - height: 12, + Assets.svg.user, + height: 30, + width: 30, ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, - width: 8, - height: 8, + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1( + context), ), ), ), - ) - ], + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 14), + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), ), ), ), @@ -453,100 +419,23 @@ class _AddAddressBookEntryViewState return; } - ///TODO if desktop make dialog - !isDesktop - ? showModalBottomSheet<dynamic>( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }) - : showDialog<dynamic>( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 700, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: - const EdgeInsets - .all(32), - child: Text( - "Select emoji", - style: STextStyles - .desktopH3( - context), - textAlign: - TextAlign - .center, - ), - ), - ], - ), - Expanded( - child: LayoutBuilder( - builder: (context, - constraints) { - return SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: - ConstrainedBox( - constraints: - BoxConstraints( - minHeight: - constraints - .maxHeight, - minWidth: - constraints - .maxWidth, - ), - child: - IntrinsicHeight( - child: Column( - children: const [ - Padding( - padding: - EdgeInsets.symmetric(horizontal: 32), - // child: - // EmojiSelectSheet(), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ], - ), - ); - }).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); }, child: SizedBox( height: 48, @@ -734,22 +623,16 @@ class _AddAddressBookEntryViewState Row( children: [ Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.m : null, onPressed: () async { - if (FocusScope.of(context).hasFocus) { + if (!isDesktop && + FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future<void>.delayed( - const Duration(milliseconds: 75)); + const Duration(milliseconds: 75), + ); } if (mounted) { Navigator.of(context).pop(); @@ -776,16 +659,11 @@ class _AddAddressBookEntryViewState bool shouldEnableSave = validForms && nameExists; - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), + return PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.m : null, + enabled: shouldEnableSave, onPressed: shouldEnableSave ? () async { if (FocusScope.of(context) @@ -827,19 +705,6 @@ class _AddAddressBookEntryViewState } } : null, - child: Text( - "Save", - style: - STextStyles.button(context).copyWith( - color: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ), - ), ); }, ), diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index b6cf0aad4..f49547858 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -253,7 +253,7 @@ class _NewContactAddressEntryFormState }, child: const ClipboardIcon(), ), - if (ref.watch(addressEntryDataProvider(widget.id) + if (!Util.isDesktop && ref.watch(addressEntryDataProvider(widget.id) .select((value) => value.address)) == null) TextFieldIconButton( From 0503999fa708aa0b7ab38d39c044d87435e045de Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 15:24:28 -0600 Subject: [PATCH 019/100] WIP coin dropdown --- .../new_contact_address_entry_form.dart | 245 ++++++++++++------ 1 file changed, 172 insertions(+), 73 deletions(-) diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index f49547858..25cff073b 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -1,8 +1,10 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/address_book_views/subviews/coin_select_sheet.dart'; +import 'package:stackwallet/providers/providers.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -47,6 +49,8 @@ class _NewContactAddressEntryFormState late final FocusNode addressLabelFocusNode; late final FocusNode addressFocusNode; + List<Coin> coins = []; + @override void initState() { addressLabelController = TextEditingController() @@ -55,6 +59,7 @@ class _NewContactAddressEntryFormState ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); + coins = [...Coin.values]; super.initState(); } @@ -70,86 +75,179 @@ class _NewContactAddressEntryFormState @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + if (isDesktop) { + coins = [...Coin.values]; + + coins.remove(Coin.firoTestNet); + if (showTestNet) { + coins = coins.sublist(0, coins.length - kTestNetCoinCount); + } + } + return Column( children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Select cryptocurrency", - hintStyle: STextStyles.fieldLabel(context), - prefixIcon: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (isDesktop) + DropdownButtonHideUnderline( + child: DropdownButton2<Coin>( + hint: Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ), + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)), + onChanged: (value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = value; + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + items: [ + ...coins.map( + (coin) => DropdownMenuItem<Coin>( + value: coin, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + height: 24, + width: 24, + ), + const SizedBox( + width: 12, + ), + Text( + coin.prettyName, + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), ), ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => const CoinSelectSheet(), - ).then((value) { - if (value is Coin) { - ref.read(addressEntryDataProvider(widget.id)).coin = - value; - } - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin)) == - null - ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) - : Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor( - coin: ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin))!), - height: 20, - width: 20, - ), - const SizedBox( - width: 12, - ), - Text( - ref - .watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin))! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, + ), + ], + ), + ), + if (!isDesktop) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Select cryptocurrency", + hintStyle: STextStyles.fieldLabel(context), + prefixIcon: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => const CoinSelectSheet(), + ).then((value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = + value; + } + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)) == + null + ? Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) + : Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: ref.watch( + addressEntryDataProvider(widget.id) + .select( + (value) => value.coin))!), + height: 20, + width: 20, + ), + const SizedBox( + width: 12, + ), + Text( + ref + .watch( + addressEntryDataProvider(widget.id) + .select((value) => value.coin))! + .prettyName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), ), ), ), ), ), - ), const SizedBox( height: 8, ), @@ -253,9 +351,10 @@ class _NewContactAddressEntryFormState }, child: const ClipboardIcon(), ), - if (!Util.isDesktop && ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.address)) == - null) + if (!Util.isDesktop && + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.address)) == + null) TextFieldIconButton( key: const Key("addAddressBookEntryScanQrButtonKey"), onTap: () async { From 8799a9cfa2a993379e34a91cef9427b4fe272740 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 15:34:48 -0600 Subject: [PATCH 020/100] my stack contact tweaks --- .../subviews/add_address_book_entry_view.dart | 2 +- .../subwidgets/desktop_address_card.dart | 18 +++++++++++------- .../subwidgets/desktop_contact_details.dart | 11 +++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index bb93c68d8..2759e9cb1 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -559,7 +559,7 @@ class _AddAddressBookEntryViewState ), ], ), - if (!isDesktop) const SizedBox(height: 8), + const SizedBox(height: 8), if (forms.length <= 1) const SizedBox( height: 8, diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 49b75a4a8..405b9107c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -11,9 +11,11 @@ class DesktopAddressCard extends StatelessWidget { const DesktopAddressCard({ Key? key, required this.entry, + required this.contactId, }) : super(key: key); final ContactAddressEntry entry; + final String contactId; @override Widget build(BuildContext context) { @@ -57,13 +59,15 @@ class DesktopAddressCard extends StatelessWidget { text: "Copy", onTap: () {}, ), - const SizedBox( - width: 16, - ), - BlueTextButton( - text: "Edit", - onTap: () {}, - ), + if (contactId != "default") + const SizedBox( + width: 16, + ), + if (contactId != "default") + BlueTextButton( + text: "Edit", + onTap: () {}, + ), ], ) ], diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index bc19fe3bd..ada330a14 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -40,16 +40,18 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { width: 32, height: 32, decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, + color: contact.id == "default" + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular(32), ), child: contact.id == "default" ? Center( child: SvgPicture.asset( Assets.svg.stackIcon(context), - width: 20, + width: 32, ), ) : contact.emojiChar != null @@ -134,6 +136,7 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { padding: const EdgeInsets.all(18), child: DesktopAddressCard( entry: contact.addresses[i], + contactId: contact.id, ), ), ], From 51c98f90e94d9bbdeb8fe75828ec8f3a848ece29 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:01:11 -0600 Subject: [PATCH 021/100] contact tx history --- .../desktop_address_book.dart | 7 +- .../desktop_address_book_scaffold.dart | 3 +- .../subwidgets/desktop_address_card.dart | 18 +- .../subwidgets/desktop_contact_details.dart | 334 ++++++++++++------ 4 files changed, 248 insertions(+), 114 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index fd25617e7..f028a3424 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -427,11 +427,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), details: currentContactId == null ? Container() - : RoundedWhiteContainer( - padding: const EdgeInsets.all(24), - child: DesktopContactDetails( - contactId: currentContactId!, - ), + : DesktopContactDetails( + contactId: currentContactId!, ), ), ), diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart index f32ea1f7f..36e44e6d4 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart @@ -56,6 +56,7 @@ class DesktopAddressBookScaffold extends StatelessWidget { ), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 6, @@ -96,7 +97,7 @@ class DesktopAddressBookScaffold extends StatelessWidget { const SizedBox( height: weirdRowHeight, ), - Expanded( + Flexible( child: details ?? Container(), ), ], diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 405b9107c..f00e0c137 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -12,10 +16,12 @@ class DesktopAddressCard extends StatelessWidget { Key? key, required this.entry, required this.contactId, + this.clipboard = const ClipboardWrapper(), }) : super(key: key); final ContactAddressEntry entry; final String contactId; + final ClipboardInterface clipboard; @override Widget build(BuildContext context) { @@ -57,7 +63,17 @@ class DesktopAddressCard extends StatelessWidget { children: [ BlueTextButton( text: "Copy", - onTap: () {}, + onTap: () { + clipboard.setData( + ClipboardData(text: entry.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, ), if (contactId != "default") const SizedBox( diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index ada330a14..53597826c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -1,14 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ @@ -24,132 +31,245 @@ class DesktopContactDetails extends ConsumerStatefulWidget { } class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { + List<Tuple2<String, Transaction>> _cachedTransactions = []; + + bool _contactHasAddress(String address, Contact contact) { + for (final entry in contact.addresses) { + if (entry.address == address) { + return true; + } + } + return false; + } + + Future<List<Tuple2<String, Transaction>>> _filteredTransactionsByContact( + List<Manager> managers, + ) async { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // TODO: optimise + + List<Tuple2<String, Transaction>> result = []; + for (final manager in managers) { + final transactions = (await manager.transactionData) + .getAllTransactions() + .values + .toList() + .where((e) => _contactHasAddress(e.address, contact)); + + for (final tx in transactions) { + result.add(Tuple2(manager.walletId, tx)); + } + } + // sort by date + result.sort((a, b) => b.item2.timestamp - a.item2.timestamp); + + return result; + } + @override Widget build(BuildContext context) { final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(widget.contactId))); - return Column( + return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Colors.transparent - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" - ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 32, - ), - ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), - ), - const SizedBox( - width: 16, - ), - Text( - contact.name, - style: STextStyles.desktopTextSmall(context), - ), - ], - ), - SecondaryButton( - label: "Options", - width: 86, - buttonHeight: ButtonHeight.xxs, - onPressed: () {}, - ), - ], - ), - const SizedBox( - height: 24, - ), Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Addresses", - style: STextStyles.desktopTextExtraExtraSmall( - context), - ), - BlueTextButton( - text: "Add new", - onTap: () {}, - ), - ], + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 32, + ), + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), ), const SizedBox( - height: 12, + width: 16, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - borderColor: Theme.of(context) - .extension<StackColors>()! - .background, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 0; i < contact.addresses.length; i++) - Column( + Text( + contact.name, + style: STextStyles.desktopTextSmall(context), + ), + ], + ), + SecondaryButton( + label: "Options", + width: 86, + buttonHeight: ButtonHeight.xxs, + onPressed: () {}, + ), + ], + ), + const SizedBox( + height: 24, + ), + Flexible( + child: ListView( + primary: false, + shrinkWrap: true, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + BlueTextButton( + text: "Add new", + onTap: () {}, + ), + ], + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < contact.addresses.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(18), + child: DesktopAddressCard( + entry: contact.addresses[i], + contactId: contact.id, + ), + ), + ], + ), + ], + ), + ), + Text( + "Transaction history", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + FutureBuilder( + future: _filteredTransactionsByContact( + ref.watch(walletsChangeNotifierProvider).managers), + builder: (_, + AsyncSnapshot<List<Tuple2<String, Transaction>>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; + + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (i > 0) - Container( - color: Theme.of(context) - .extension<StackColors>()! - .background, - height: 1, - ), - Padding( - padding: const EdgeInsets.all(18), - child: DesktopAddressCard( - entry: contact.addresses[i], - contactId: contact.id, + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, ), ), ], ), - ], - ), - ) - ], - ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } else { + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } + } + }, + ), + ], ), ), - ); - }, + ], + ), ), ), ], From 494849364317cb55e46e8c7e40d786dc37ede959 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:18:00 -0600 Subject: [PATCH 022/100] contact tx history --- .../subwidgets/desktop_contact_details.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 53597826c..fb094d766 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -192,9 +192,16 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ], ), ), - Text( - "Transaction history", - style: STextStyles.desktopTextExtraExtraSmall(context), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "Transaction history", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), ), FutureBuilder( future: _filteredTransactionsByContact( From 318758f76850a471ce0dbfc0a1ecd6a190fb4e4a Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 16:55:34 -0700 Subject: [PATCH 023/100] copy receive address has pointer finger cursor --- .../sub_widgets/desktop_receive.dart | 142 +++++++++--------- 1 file changed, 73 insertions(+), 69 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 3de4ed1e3..1dd2e607e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -117,78 +117,82 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: receivingAddress), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).extension<StackColors>()!.background, - width: 2, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: receivingAddress), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).extension<StackColors>()!.background, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${coin.ticker} address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${coin.ticker} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: Text( + receivingAddress, + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), From cd19d776ae04631571a8d64b008d3cf52746d331 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:47:50 -0600 Subject: [PATCH 024/100] desktop edit contact address entry --- .../subviews/edit_contact_address_view.dart | 433 +++++++++--------- .../subwidgets/desktop_address_card.dart | 62 ++- 2 files changed, 268 insertions(+), 227 deletions(-) diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 618a41982..f0143d39d 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -12,7 +12,11 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class EditContactAddressView extends ConsumerStatefulWidget { const EditContactAddressView({ @@ -44,6 +48,42 @@ class _EditContactAddressViewState late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; + Future<void> save(Contact contact) async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses.toList(); + + final entry = entries.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + final index = entries.indexOf(entry); + entries.remove(entry); + + ContactAddressEntry editedEntry = + ref.read(addressEntryDataProvider(0)).buildAddressEntry(); + + entries.insert(index, editedEntry); + + Contact editedContact = contact.copyWith(addresses: entries); + + if (await ref.read(addressBookServiceProvider).editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + } + @override void initState() { contactId = widget.contactId; @@ -59,236 +99,181 @@ class _EditContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + final bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 24, - ), - GestureDetector( - onTap: () async { - // delete address - final _addresses = contact.addresses; - final entry = _addresses.firstWhere( - (e) => - e.label == addressEntry.label && - e.address == addressEntry.address && - e.coin == addressEntry.coin, - ); - - _addresses.remove(entry); - Contact editedContact = - contact.copyWith(addresses: _addresses); - if (await ref - .read(addressBookServiceProvider) - .editContact(editedContact)) { - Navigator.of(context).pop(); - // TODO show success notification - } else { - // TODO show error notification - } - }, - child: Text( - "Delete address", - style: STextStyles.link(context), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses.toList(); - - final entry = entries.firstWhere( - (e) => - e.label == - addressEntry.label && - e.address == - addressEntry.address && - e.coin == addressEntry.coin, - ); - - final index = - entries.indexOf(entry); - entries.remove(entry); - - ContactAddressEntry editedEntry = ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry(); - - entries.insert(index, editedEntry); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 24, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, ), - ); - }, + child: GestureDetector( + onTap: () async { + // delete address + final _addresses = contact.addresses; + final entry = _addresses.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + _addresses.remove(entry); + Contact editedContact = contact.copyWith(addresses: _addresses); + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + Navigator.of(context).pop(); + // TODO show success notification + } else { + // TODO show error notification + } + }, + child: Text( + "Delete address", + style: STextStyles.link(context), + ), + ), + ), + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + onPressed: () => save(contact), + buttonHeight: isDesktop ? ButtonHeight.l : null, + ), + ), + ], + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index f00e0c137..713c9e965 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -1,15 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; class DesktopAddressCard extends StatelessWidget { const DesktopAddressCard({ @@ -80,9 +85,60 @@ class DesktopAddressCard extends StatelessWidget { width: 16, ), if (contactId != "default") - BlueTextButton( - text: "Edit", - onTap: () {}, + Consumer( + builder: (context, ref, child) { + return BlueTextButton( + text: "Edit", + onTap: () async { + ref.read(addressEntryDataProvider(0)).address = + entry.address; + ref.read(addressEntryDataProvider(0)).addressLabel = + entry.label; + ref.read(addressEntryDataProvider(0)).coin = + entry.coin; + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Edit address", + style: STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactAddressView( + contactId: contactId, + addressEntry: entry, + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, ), ], ) From df64e48e1ef2985cc58363bf844c288bd71ebd5a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 17:00:06 -0600 Subject: [PATCH 025/100] desktop add new contact address entry --- .../add_new_contact_address_view.dart | 342 +++++++++--------- .../subwidgets/desktop_address_card.dart | 2 + .../subwidgets/desktop_contact_details.dart | 49 ++- 3 files changed, 213 insertions(+), 180 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index e5dbaa7b9..dc25c3dc1 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -12,7 +12,11 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class AddNewContactAddressView extends ConsumerStatefulWidget { const AddNewContactAddressView({ @@ -55,190 +59,170 @@ class _AddNewContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Add new address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Add new address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses; - - entries.add(ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry()); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), - ), - ); - }, + ); + }, + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses; + + entries.add(ref + .read(addressEntryDataProvider(0)) + .buildAddressEntry()); + + Contact editedContact = + contact.copyWith(addresses: entries); + + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 713c9e965..4d58dc474 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -90,6 +90,8 @@ class DesktopAddressCard extends StatelessWidget { return BlueTextButton( text: "Edit", onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); ref.read(addressEntryDataProvider(0)).address = entry.address; ref.read(addressEntryDataProvider(0)).addressLabel = diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index fb094d766..1c27b6836 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,14 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -154,7 +158,50 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), BlueTextButton( text: "Add new", - onTap: () {}, + onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Add new address", + style: + STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: AddNewContactAddressView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + }, ), ], ), From e70f5b0709eea46f42cfd4ed8dd9f7ba2aa24b20 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:14:30 -0600 Subject: [PATCH 026/100] WIP desktop contact options context popup menu --- .../subwidgets/desktop_contact_details.dart | 262 +++++++++++++++++- 1 file changed, 260 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 1c27b6836..2cd20a839 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -131,9 +131,19 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), SecondaryButton( label: "Options", - width: 86, + width: 96, buttonHeight: ButtonHeight.xxs, - onPressed: () {}, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopContactOptionsMenuPopup( + contactId: contact.id, + ); + }, + ); + }, ), ], ), @@ -330,3 +340,251 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } + +class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { + const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) + : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactOptionsMenuPopup> createState() => + _DesktopContactOptionsMenuPopupState(); +} + +class _DesktopContactOptionsMenuPopupState + extends ConsumerState<DesktopContactOptionsMenuPopup> { + bool hoveredOnStar = false; + bool hoveredOnPencil = false; + bool hoveredOnTrash = false; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 210, + left: MediaQuery.of(context).size.width - 280, + child: Container( + width: 270, + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnStar = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnStar = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = + ref.read(addressBookServiceProvider).getContactById( + widget.contactId, + ); + ref.read(addressBookServiceProvider).editContact( + contact.copyWith( + isFavorite: !contact.isFavorite, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.star, + width: 24, + height: 22, + color: hoveredOnStar + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch(addressBookServiceProvider.select( + (value) => value + .getContactById(widget.contactId) + .isFavorite)) + ? "Remove from favorites" + : "Add to favorites", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + const SizedBox( + height: 2, + ), + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + const SizedBox( + height: 2, + ), + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should delete contact"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} From 38251dc5edb0d5e2f6a771e53deb98aa0e96a68e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:21:04 -0600 Subject: [PATCH 027/100] textStyles prep for ocean theme --- lib/utilities/text_styles.dart | 54 ++++++++++++++++++++++++++++ lib/utilities/theme/color_theme.dart | 1 + 2 files changed, 55 insertions(+) diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 63aa19afb..db4764459 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -15,6 +15,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -32,6 +33,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 18, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -49,6 +51,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -66,6 +69,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -83,6 +87,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -100,6 +105,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -117,6 +123,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -134,6 +141,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -151,6 +159,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -168,6 +177,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -185,6 +195,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -202,6 +213,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -219,6 +231,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -237,6 +250,7 @@ class STextStyles { fontSize: 14, height: 14 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textFieldActiveSearchIconRight, @@ -255,6 +269,7 @@ class STextStyles { fontWeight: FontWeight.w700, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -272,6 +287,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemLabel, @@ -289,6 +305,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -306,6 +323,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -324,6 +342,7 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -343,6 +362,7 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -361,6 +381,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -378,6 +399,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorRed, @@ -395,6 +417,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemIcons, @@ -412,6 +435,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorBlue, @@ -429,6 +453,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -446,6 +471,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -463,6 +489,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -480,6 +507,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textError, @@ -497,6 +525,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -517,6 +546,7 @@ class STextStyles { fontSize: 40, height: 40 / 40, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -536,6 +566,7 @@ class STextStyles { fontSize: 32, height: 32 / 32, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -555,6 +586,7 @@ class STextStyles { fontSize: 24, height: 24 / 24, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -574,6 +606,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -593,6 +626,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -612,6 +646,7 @@ class STextStyles { fontSize: 20, height: 28 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -631,6 +666,7 @@ class STextStyles { fontSize: 24, height: 33 / 24, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -650,6 +686,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -669,6 +706,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -688,6 +726,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -707,6 +746,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, @@ -726,6 +766,7 @@ class STextStyles { fontSize: 18, height: 27 / 18, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -745,6 +786,7 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -764,6 +806,7 @@ class STextStyles { fontSize: 14, height: 21 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -783,6 +826,7 @@ class STextStyles { fontSize: 14, height: 21 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -802,6 +846,7 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -821,6 +866,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -840,6 +886,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), @@ -859,6 +906,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -878,6 +926,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), @@ -897,6 +946,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -915,6 +965,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 8, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).textDark, @@ -932,6 +983,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 26, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, @@ -950,6 +1002,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, @@ -969,6 +1022,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 49fd41a6e..852e2f586 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum ThemeType { light, dark, + oceanBreeze, } abstract class StackColorTheme { From 390c3f186ff8266479a38ce90684840b0f554042 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:49:56 -0600 Subject: [PATCH 028/100] temporarily disable in wallet exchange button --- .../wallet_view/desktop_wallet_view.dart | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index a7de8fdf4..d08864eee 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -467,36 +467,36 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ); }, ), - if (coin == Coin.firo) const SizedBox(width: 16), - SecondaryButton( - width: 180, - buttonHeight: ButtonHeight.l, - onPressed: () { - _onExchangePressed(context); - }, - label: "Exchange", - icon: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackPrimary - .withOpacity(0.2), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 14, - height: 14, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), - ), - ), + // if (coin == Coin.firo) const SizedBox(width: 16), + // SecondaryButton( + // width: 180, + // buttonHeight: ButtonHeight.l, + // onPressed: () { + // _onExchangePressed(context); + // }, + // label: "Exchange", + // icon: Container( + // width: 24, + // height: 24, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(24), + // color: Theme.of(context) + // .extension<StackColors>()! + // .buttonBackPrimary + // .withOpacity(0.2), + // ), + // child: Center( + // child: SvgPicture.asset( + // Assets.svg.arrowRotate2, + // width: 14, + // height: 14, + // color: Theme.of(context) + // .extension<StackColors>()! + // .buttonTextSecondary, + // ), + // ), + // ), + // ), ], ), ), From 4eed147f10f978125f7ebaf1b57b7410732fe287 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 19:23:15 -0600 Subject: [PATCH 029/100] update main to check for ocean breeze theme --- lib/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index b1f917f58..42b4ee718 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -301,6 +301,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> case "dark": themeType = ThemeType.dark; break; + case "oceanBreeze": + themeType = ThemeType.oceanBreeze; + break; case "light": default: themeType = ThemeType.light; From 2137cffd8459b750c9bd7b056eb46c93e0f60418 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 20:04:02 -0700 Subject: [PATCH 030/100] ocean breeze theme colors added --- .../appearance_settings_view.dart | 5 +- lib/utilities/text_styles.dart | 296 +++++++++++++++++ lib/utilities/theme/ocean_breeze_colors.dart | 306 ++++++++++++++++++ 3 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 lib/utilities/theme/ocean_breeze_colors.dart diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index b0cf35a84..3a1b842f6 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -141,7 +141,10 @@ class AppearanceSettingsView extends ConsumerWidget { key: "colorScheme", value: (newValue ? ThemeType.dark - : ThemeType.light) + : (newValue + ? ThemeType.light + : ThemeType + .oceanBreeze)) .name, ); ref diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index db4764459..c9dd15e1d 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -16,6 +16,11 @@ class STextStyles { fontSize: 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -34,6 +39,11 @@ class STextStyles { fontSize: 18, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -52,6 +62,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -70,6 +85,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -88,6 +108,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -106,6 +131,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -124,6 +154,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -142,6 +177,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -160,6 +200,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -178,6 +223,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -196,6 +246,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -214,6 +269,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -232,6 +292,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -251,6 +316,12 @@ class STextStyles { height: 14 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textFieldActiveSearchIconRight, @@ -270,6 +341,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w700, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -288,6 +364,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemLabel, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemLabel, @@ -306,6 +387,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -324,6 +410,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -343,6 +434,12 @@ class STextStyles { height: 1.5, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -363,6 +460,12 @@ class STextStyles { height: 1.5, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -382,6 +485,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -400,6 +508,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorRed, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorRed, @@ -418,6 +531,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemIcons, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemIcons, @@ -436,6 +554,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorBlue, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorBlue, @@ -454,6 +577,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -472,6 +600,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -490,6 +623,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -508,6 +646,11 @@ class STextStyles { fontSize: 10, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textError, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textError, @@ -526,6 +669,11 @@ class STextStyles { fontSize: 10, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -547,6 +695,12 @@ class STextStyles { height: 40 / 40, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -567,6 +721,12 @@ class STextStyles { height: 32 / 32, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 32, + height: 32 / 32, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -587,6 +747,12 @@ class STextStyles { height: 24 / 24, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 24, + height: 24 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -607,6 +773,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -627,6 +799,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -647,6 +825,12 @@ class STextStyles { height: 28 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 28 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -667,6 +851,12 @@ class STextStyles { height: 33 / 24, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 24, + height: 33 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -687,6 +877,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -707,6 +903,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -727,6 +929,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -747,6 +955,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, @@ -767,6 +981,12 @@ class STextStyles { height: 27 / 18, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -787,6 +1007,12 @@ class STextStyles { height: 24 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -807,6 +1033,12 @@ class STextStyles { height: 21 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 21 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -827,6 +1059,12 @@ class STextStyles { height: 21 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -847,6 +1085,12 @@ class STextStyles { height: 24 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -867,6 +1111,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -887,6 +1137,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), @@ -907,6 +1163,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -927,6 +1189,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.5), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), @@ -947,6 +1215,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -966,6 +1240,11 @@ class STextStyles { fontSize: 8, ); case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 8, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).textDark, @@ -984,6 +1263,11 @@ class STextStyles { fontSize: 26, ); case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).numberTextDefault, + fontWeight: FontWeight.w400, + fontSize: 26, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, @@ -1003,6 +1287,12 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w400, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, @@ -1023,6 +1313,12 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart new file mode 100644 index 000000000..ff2f4e85e --- /dev/null +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; + +class OceanBreezeColors extends StackColorTheme { + @override + ThemeType get themeType => ThemeType.oceanBreeze; + + @override + Color get background => const Color(0xFFF3F7FA); + @override + Color get overlay => const Color(0xFF111215); + + @override + Color get accentColorBlue => const Color(0xFF077CBE); + @override + Color get accentColorGreen => const Color(0xFF00A591); + @override + Color get accentColorYellow => const Color(0xFFF4C517); + @override + Color get accentColorRed => const Color(0xFFD1382D); + @override + Color get accentColorOrange => const Color(0xFFFF985F); + @override + Color get accentColorDark => const Color(0xFF232323); + + @override + Color get shadow => const Color(0xFF388192); + + @override + Color get textDark => const Color(0xFF232323); + @override + Color get textDark2 => const Color(0xFF333333); + @override + Color get textDark3 => const Color(0xFF696B6C); + @override + Color get textSubtitle1 => const Color(0xFF7E8284); + @override + Color get textSubtitle2 => const Color(0xFF919393); + @override + Color get textSubtitle3 => const Color(0xFFB0B2B2); + @override + Color get textSubtitle4 => const Color(0xFFD1D3D3); + @override + Color get textSubtitle5 => const Color(0xFFDEDFE1); + @override + Color get textSubtitle6 => const Color(0xFFF1F1F1); + @override + Color get textWhite => const Color(0xFFFFFFFF); + @override + Color get textFavoriteCard => const Color(0xFF232323); + @override + Color get textError => const Color(0xFF8D0006); + + // button background + @override + Color get buttonBackPrimary => const Color(0xFF227386); + @override + Color get buttonBackSecondary => const Color(0xFFC2DAE2); + @override + Color get buttonBackPrimaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonBackSecondaryDisabled => const Color(0xFFBDBDBD); + @override + Color get buttonBackBorder => const Color(0xFF227386); + @override + Color get buttonBackBorderDisabled => const Color(0xFFBDD5DB); + + @override + Color get numberBackDefault => const Color(0xFFFFFFFF); + @override + Color get numpadBackDefault => const Color(0xFF227386); + @override + Color get bottomNavBack => const Color(0xFFFFFFFF); + + // button text/element + @override + Color get buttonTextPrimary => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondary => const Color(0xFF232323); + @override + Color get buttonTextPrimaryDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonTextBorder => const Color(0xFF227386); + @override + Color get buttonTextDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextBorderless => const Color(0xFF056EC6); + @override + Color get buttonTextBorderlessDisabled => const Color(0xFFB6B6B6); + @override + Color get numberTextDefault => const Color(0xFF232323); + @override + Color get numpadTextDefault => const Color(0xFFFFFFFF); + @override + Color get bottomNavText => const Color(0xFF232323); + + // switch + @override + Color get switchBGOn => const Color(0xFF056EC6); + @override + Color get switchBGOff => const Color(0xFFCCDBF9); + @override + Color get switchBGDisabled => const Color(0xFFC5C6C9); + @override + Color get switchCircleOn => const Color(0xFFDAE2FF); + @override + Color get switchCircleOff => const Color(0xFFFBFCFF); + @override + Color get switchCircleDisabled => const Color(0xFFFBFCFF); + + // step indicator background + @override + Color get stepIndicatorBGCheck => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGNumber => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGInactive => const Color(0xFFA6C7D1); + @override + Color get stepIndicatorBGLines => const Color(0xFF90B8DC); + @override + Color get stepIndicatorBGLinesInactive => const Color(0xFFBCD4EA); + @override + Color get stepIndicatorIconText => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconNumber => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconInactive => const Color(0xFFD4DFFF); + + // checkbox + @override + Color get checkboxBGChecked => const Color(0xFF056EC6); + @override + Color get checkboxBorderEmpty => const Color(0xFF8C8F90); + @override + Color get checkboxBGDisabled => const Color(0xFFB0C9ED); + @override + Color get checkboxIconChecked => const Color(0xFFFFFFFF); + @override + Color get checkboxIconDisabled => const Color(0xFFFFFFFF); + @override + Color get checkboxTextLabel => const Color(0xFF232323); + + // snack bar + @override + Color get snackBarBackSuccess => const Color(0xFFADD6D2); + @override + Color get snackBarBackError => const Color(0xFFF6C7C3); + @override + Color get snackBarBackInfo => const Color(0xFFCCD7FF); + @override + Color get snackBarTextSuccess => const Color(0xFF075547); + @override + Color get snackBarTextError => const Color(0xFF8D0006); + @override + Color get snackBarTextInfo => const Color(0xFF002569); + + // icons + @override + Color get bottomNavIconBack => const Color(0xFFA7C7CF); + @override + Color get bottomNavIconIcon => const Color(0xFF227386); + + @override + Color get topNavIconPrimary => const Color(0xFF227386); + @override + Color get topNavIconGreen => const Color(0xFF00A591); + @override + Color get topNavIconYellow => const Color(0xFFFDD33A); + @override + Color get topNavIconRed => const Color(0xFFEA4649); + + @override + Color get settingsIconBack => const Color(0xFFE0E3E3); + @override + Color get settingsIconIcon => const Color(0xFF232323); + @override + Color get settingsIconBack2 => const Color(0xFF80D2C8); + @override + Color get settingsIconElement => const Color(0xFF00A591); + + // text field + @override + Color get textFieldActiveBG => const Color(0xFFD3E3E7); + @override + Color get textFieldDefaultBG => const Color(0xFFD8E7EB); + @override + Color get textFieldErrorBG => const Color(0xFFF6C7C3); + @override + Color get textFieldSuccessBG => const Color(0xFFADD6D2); + + @override + Color get textFieldActiveSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldDefaultSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldErrorSearchIconLeft => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconLeft => const Color(0xFF006C4D); + + @override + Color get textFieldActiveText => const Color(0xFF232323); + @override + Color get textFieldDefaultText => const Color(0xFF86898C); + @override + Color get textFieldErrorText => const Color(0xFF000000); + @override + Color get textFieldSuccessText => const Color(0xFF000000); + + @override + Color get textFieldActiveLabel => const Color(0xFF86898C); + @override + Color get textFieldErrorLabel => const Color(0xFF8D0006); + @override + Color get textFieldSuccessLabel => const Color(0xFF077C6E); + + @override + Color get textFieldActiveSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldDefaultSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldErrorSearchIconRight => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconRight => const Color(0xFF077C6E); + + // settings item level2 + @override + Color get settingsItem2ActiveBG => const Color(0xFFFFFFFF); + @override + Color get settingsItem2ActiveText => const Color(0xFF232323); + @override + Color get settingsItem2ActiveSub => const Color(0xFF8C8F90); + + // radio buttons + @override + Color get radioButtonIconBorder => const Color(0xFF056EC6); + @override + Color get radioButtonIconBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonBorderEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonIconCircle => const Color(0xFF056EC6); + @override + Color get radioButtonIconEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonTextEnabled => const Color(0xFF42444B); + @override + Color get radioButtonTextDisabled => const Color(0xFF42444B); + @override + Color get radioButtonLabelEnabled => const Color(0xFF8C8F90); + @override + Color get radioButtonLabelDisabled => const Color(0xFF8C8F90); + + // info text + @override + Color get infoItemBG => const Color(0xFFFFFFFF); + @override + Color get infoItemLabel => const Color(0xFF838788); + @override + Color get infoItemText => const Color(0xFF232323); + @override + Color get infoItemIcons => const Color(0xFF056EC6); + + // popup + @override + Color get popupBG => const Color(0xFFFFFFFF); + + // currency list + @override + Color get currencyListItemBG => const Color(0xFFF0F5F7); + + // bottom nav + @override + Color get stackWalletBG => const Color(0xFFFFFFFF); + @override + Color get stackWalletMid => const Color(0xFFFFFFFF); + @override + Color get stackWalletBottom => const Color(0xFF232323); + @override + Color get bottomNavShadow => const Color(0xFF388192); + + @override + Color get favoriteStarActive => const Color(0xFFF4C517); + @override + Color get favoriteStarInactive => const Color(0xFFB0B2B2); + + @override + Color get splash => const Color(0xFF8E9192); + @override + Color get highlight => const Color(0xFFA9ACAC); + @override + Color get warningForeground => const Color(0xFF232323); + @override + Color get warningBackground => const Color(0xFFF6C7C3); + @override + Color get loadingOverlayTextColor => const Color(0xFFF7F7F7); + @override + Color get myStackContactIconBG => const Color(0xFFD8E7EB); + @override + Color get textConfirmTotalAmount => const Color(0xFF232323); + @override + Color get textSelectedWordTableItem => const Color(0xFF232323); +} From 02dadd432ca70d30197ca510474b8ee4bb29ba1b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 20:05:29 -0700 Subject: [PATCH 031/100] ocean breeze added --- lib/main.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 42b4ee718..2d1bab160 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,6 +57,7 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:window_size/window_size.dart'; @@ -317,8 +318,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - themeType == ThemeType.dark ? DarkColors() : LightColors()); + StackColors.fromStackColorTheme(themeType == ThemeType.dark + ? DarkColors() + : (themeType == ThemeType.light + ? LightColors() + : OceanBreezeColors())); if (Platform.isAndroid) { // fetch open file if it exists From 17e4976a899b3a0f5ceecd20edf10d1495a0568c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:07:11 -0600 Subject: [PATCH 032/100] include flushbartype in flushbar import --- lib/notifications/show_flush_bar.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/notifications/show_flush_bar.dart b/lib/notifications/show_flush_bar.dart index 5320c8a9d..47cea682a 100644 --- a/lib/notifications/show_flush_bar.dart +++ b/lib/notifications/show_flush_bar.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +export 'package:stackwallet/utilities/enums/flush_bar_type.dart'; + Future<dynamic> showFloatingFlushBar({ required FlushBarType type, required String message, From aa966a106dd3c603da01bf3d24b3f6358441399c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:07:31 -0600 Subject: [PATCH 033/100] add delete contact functionality for desktop --- .../subwidgets/desktop_contact_details.dart | 382 ++++++++++++------ lib/widgets/address_book_card.dart | 13 +- 2 files changed, 260 insertions(+), 135 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 2cd20a839..96eed4835 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; @@ -15,6 +16,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -74,8 +77,16 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { @override Widget build(BuildContext context) { - final contact = ref.watch(addressBookServiceProvider - .select((value) => value.getContactById(widget.contactId))); + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(widget.contactId))); + } catch (_) { + return Container(); + } + + final contact = _contact!; return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -129,22 +140,23 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), ], ), - SecondaryButton( - label: "Options", - width: 96, - buttonHeight: ButtonHeight.xxs, - onPressed: () async { - await showDialog<void>( - context: context, - barrierColor: Colors.transparent, - builder: (context) { - return DesktopContactOptionsMenuPopup( - contactId: contact.id, - ); - }, - ); - }, - ), + if (widget.contactId != "default") + SecondaryButton( + label: "Options", + width: 96, + buttonHeight: ButtonHeight.xxs, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopContactOptionsMenuPopup( + contactId: contact.id, + ); + }, + ); + }, + ), ], ), const SizedBox( @@ -453,132 +465,238 @@ class _DesktopContactOptionsMenuPopupState ), ), ), - const SizedBox( - height: 2, - ), - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnPencil = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnPencil = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should go to edit"); + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 24, - height: 22, - color: hoveredOnPencil - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Edit contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, ), - ) - ], + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), ), ), ), - ), - const SizedBox( - height: 2, - ), - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnTrash = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnTrash = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should delete contact"); + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.trash, - width: 24, - height: 22, - color: hoveredOnTrash - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Delete contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + onPressed: () { + final contact = ref + .read(addressBookServiceProvider) + .getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall( + context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => + PrimaryButton( + label: "Delete", + buttonHeight: + ButtonHeight.l, + onPressed: () { + ref + .read( + addressBookServiceProvider) + .removeContact( + contact.id); + Navigator.of(context) + .pop(); + showFloatingFlushBar( + type: FlushBarType + .success, + message: + "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], ), - ) - ], + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), ), ), ), - ), ], ), ), diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index b79f89662..dfa655f86 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -44,10 +45,16 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { @override Widget build(BuildContext context) { - // final isTiny = SizingUtilities.isTinyWidth(context); + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(contactId))); + } catch (_) { + return Container(); + } - final contact = ref.watch(addressBookServiceProvider - .select((value) => value.getContactById(contactId))); + final contact = _contact!; final List<Coin> coins = []; for (var element in contact.addresses) { From 07f229f2a0089a8a566ea846fa1e1d00b0fcc181 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:19:48 -0600 Subject: [PATCH 034/100] refactor popup --- .../subwidgets/desktop_contact_details.dart | 358 +---------------- .../desktop_contact_options_menu_popup.dart | 366 ++++++++++++++++++ 2 files changed, 367 insertions(+), 357 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 96eed4835..62cd993af 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,9 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; @@ -16,8 +16,6 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -352,357 +350,3 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } - -class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { - const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) - : super(key: key); - - final String contactId; - - @override - ConsumerState<DesktopContactOptionsMenuPopup> createState() => - _DesktopContactOptionsMenuPopupState(); -} - -class _DesktopContactOptionsMenuPopupState - extends ConsumerState<DesktopContactOptionsMenuPopup> { - bool hoveredOnStar = false; - bool hoveredOnPencil = false; - bool hoveredOnTrash = false; - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned( - top: 210, - left: MediaQuery.of(context).size.width - 280, - child: Container( - width: 270, - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: BorderRadius.circular( - 20, - ), - boxShadow: [ - Theme.of(context).extension<StackColors>()!.standardBoxShadow, - ], - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnStar = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnStar = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - final contact = - ref.read(addressBookServiceProvider).getContactById( - widget.contactId, - ); - ref.read(addressBookServiceProvider).editContact( - contact.copyWith( - isFavorite: !contact.isFavorite, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.star, - width: 24, - height: 22, - color: hoveredOnStar - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - ref.watch(addressBookServiceProvider.select( - (value) => value - .getContactById(widget.contactId) - .isFavorite)) - ? "Remove from favorites" - : "Add to favorites", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - if (widget.contactId != "default") - const SizedBox( - height: 2, - ), - if (widget.contactId != "default") - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnPencil = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnPencil = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should go to edit"); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 24, - height: 22, - color: hoveredOnPencil - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Edit contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - if (widget.contactId != "default") - const SizedBox( - height: 2, - ), - if (widget.contactId != "default") - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnTrash = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnTrash = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - final contact = ref - .read(addressBookServiceProvider) - .getContactById(widget.contactId); - - // pop context menu - Navigator.of(context).pop(); - - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => DesktopDialog( - maxWidth: 500, - maxHeight: 300, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Delete ${contact.name}?", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Spacer( - flex: 1, - ), - Text( - "Contact will be deleted permanently!", - style: STextStyles.desktopTextSmall( - context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context).pop, - buttonHeight: ButtonHeight.l, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Consumer( - builder: (context, ref, __) => - PrimaryButton( - label: "Delete", - buttonHeight: - ButtonHeight.l, - onPressed: () { - ref - .read( - addressBookServiceProvider) - .removeContact( - contact.id); - Navigator.of(context) - .pop(); - showFloatingFlushBar( - type: FlushBarType - .success, - message: - "${contact.name} deleted", - context: context, - ); - }, - ), - ), - ), - ], - ) - ], - ), - ), - ), - ], - ), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.trash, - width: 24, - height: 22, - color: hoveredOnTrash - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Delete contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart new file mode 100644 index 000000000..88bd6abf0 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { + const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) + : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactOptionsMenuPopup> createState() => + _DesktopContactOptionsMenuPopupState(); +} + +class _DesktopContactOptionsMenuPopupState + extends ConsumerState<DesktopContactOptionsMenuPopup> { + bool hoveredOnStar = false; + bool hoveredOnPencil = false; + bool hoveredOnTrash = false; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 210, + left: MediaQuery.of(context).size.width - 280, + child: Container( + width: 270, + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnStar = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnStar = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = + ref.read(addressBookServiceProvider).getContactById( + widget.contactId, + ); + ref.read(addressBookServiceProvider).editContact( + contact.copyWith( + isFavorite: !contact.isFavorite, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.star, + width: 24, + height: 22, + color: hoveredOnStar + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch(addressBookServiceProvider.select( + (value) => value + .getContactById(widget.contactId) + .isFavorite)) + ? "Remove from favorites" + : "Add to favorites", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = ref + .read(addressBookServiceProvider) + .getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall( + context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => + PrimaryButton( + label: "Delete", + buttonHeight: + ButtonHeight.l, + onPressed: () { + ref + .read( + addressBookServiceProvider) + .removeContact( + contact.id); + Navigator.of(context) + .pop(); + showFloatingFlushBar( + type: FlushBarType + .success, + message: + "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} From d4b7ec0f174101e406796f4a2bc6162a0c9f0f58 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:54:26 -0600 Subject: [PATCH 035/100] desktop edit contact --- .../edit_contact_name_emoji_view.dart | 576 ++++++++++-------- .../desktop_contact_options_menu_popup.dart | 252 ++++---- 2 files changed, 462 insertions(+), 366 deletions(-) diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index fff01eee3..a9b264b3c 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,14 +9,17 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class EditContactNameEmojiView extends ConsumerStatefulWidget { const EditContactNameEmojiView({ Key? key, @@ -69,268 +74,323 @@ class _EditContactNameEmojiViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit contact", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _selectedEmoji!.char, - style: STextStyles.pageTitleH1( - context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, - ), - ), - ), - ) - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - nameController.text.isNotEmpty; + final isDesktop = Util.isDesktop; + final double emojiSize = isDesktop ? 56 : 48; - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - final editedContact = - contact.copyWith( - shouldCopyEmojiWithNull: true, - name: nameController.text, - emojiChar: _selectedEmoji == null - ? null - : _selectedEmoji!.char, - ); - ref - .read( - addressBookServiceProvider) - .editContact( - editedContact, - ); - if (mounted) { - Navigator.of(context).pop(); - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit contact", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + if (isDesktop) { + showDialog<dynamic>( + barrierColor: Colors.transparent, + context: context, + builder: (context) { + return const DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } else { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } + }, + child: SizedBox( + height: emojiSize, + width: emojiSize, + child: Stack( + children: [ + Container( + height: emojiSize, + width: emojiSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(emojiSize / 2), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: emojiSize / 2, + width: emojiSize / 2, + ) + : Text( + _selectedEmoji!.char, + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH1(context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), + if (isDesktop) + const SizedBox( + width: 8, + ), + if (isDesktop) + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 8, ), - ); - }, + if (!isDesktop) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: nameController.text.isNotEmpty, + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + final editedContact = contact.copyWith( + shouldCopyEmojiWithNull: true, + name: nameController.text, + emojiChar: + _selectedEmoji == null ? null : _selectedEmoji!.char, + ); + unawaited( + ref.read(addressBookServiceProvider).editContact( + editedContact, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart index 88bd6abf0..690d1be98 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -28,6 +29,147 @@ class _DesktopContactOptionsMenuPopupState bool hoveredOnPencil = false; bool hoveredOnTrash = false; + void editContact() { + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 580, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Edit contact", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactNameEmojiView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + } + + void attemptDeleteContact() { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => PrimaryButton( + label: "Delete", + buttonHeight: ButtonHeight.l, + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(contact.id); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Stack( @@ -148,9 +290,7 @@ class _DesktopContactOptionsMenuPopupState 1000, ), ), - onPressed: () { - print("should go to edit"); - }, + onPressed: editContact, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 25, @@ -213,111 +353,7 @@ class _DesktopContactOptionsMenuPopupState 1000, ), ), - onPressed: () { - final contact = ref - .read(addressBookServiceProvider) - .getContactById(widget.contactId); - - // pop context menu - Navigator.of(context).pop(); - - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => DesktopDialog( - maxWidth: 500, - maxHeight: 300, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Delete ${contact.name}?", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Spacer( - flex: 1, - ), - Text( - "Contact will be deleted permanently!", - style: STextStyles.desktopTextSmall( - context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context).pop, - buttonHeight: ButtonHeight.l, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Consumer( - builder: (context, ref, __) => - PrimaryButton( - label: "Delete", - buttonHeight: - ButtonHeight.l, - onPressed: () { - ref - .read( - addressBookServiceProvider) - .removeContact( - contact.id); - Navigator.of(context) - .pop(); - showFloatingFlushBar( - type: FlushBarType - .success, - message: - "${contact.name} deleted", - context: context, - ); - }, - ), - ), - ), - ], - ) - ], - ), - ), - ), - ], - ), - ), - ); - }, + onPressed: attemptDeleteContact, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 25, From 4d17c1db5f732acb00b1726a530125318889f706 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 10:22:34 -0600 Subject: [PATCH 036/100] addressbook filter coins list fix --- lib/pages/address_book_views/address_book_view.dart | 8 ++++---- .../subviews/address_book_filter_view.dart | 2 +- .../home/address_book_view/desktop_address_book.dart | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index cdc9fb5b7..c87906870 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -51,8 +51,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ref.refresh(addressBookFilterProvider); if (widget.coin == null) { - List<Coin> coins = - Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + List<Coin> coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -60,8 +59,9 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( - coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); } } else { ref.read(addressBookFilterProvider).add(widget.coin!, false); diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index c129251d5..55c3d47ac 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -38,7 +38,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { } else { _coins = coins .toList(growable: false) - .getRange(0, coins.length - kTestNetCoinCount + 1) + .getRange(0, coins.length - kTestNetCoinCount) .toList(growable: false); } super.initState(); diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index f028a3424..e5432081c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -84,7 +84,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ref.refresh(addressBookFilterProvider); // if (widget.coin == null) { - List<Coin> coins = Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + List<Coin> coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -92,8 +92,9 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( - coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); } // } else { // ref.read(addressBookFilterProvider).add(widget.coin!, false); From 8207474d0973387b9fa7d32d4eb510132b34cc72 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 09:30:21 -0700 Subject: [PATCH 037/100] ocean breeze selector + functionality added --- assets/svg/ocean-breeze-theme.svg | 28 +++++ lib/main.dart | 2 +- .../settings_menu/appearance_settings.dart | 105 +++++++++++++++++- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 assets/svg/ocean-breeze-theme.svg diff --git a/assets/svg/ocean-breeze-theme.svg b/assets/svg/ocean-breeze-theme.svg new file mode 100644 index 000000000..0deb96ec8 --- /dev/null +++ b/assets/svg/ocean-breeze-theme.svg @@ -0,0 +1,28 @@ +<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_518_22068)"> +<rect width="200" height="162" rx="8" fill="url(#paint0_linear_518_22068)"/> +<rect x="10" y="10" width="180" height="20" rx="2" fill="#C2DAE2"/> +<rect x="16" y="16" width="106" height="8" rx="1" fill="#227386"/> +<rect x="10" y="40" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="46" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="62" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="68" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="84" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="90" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="106" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="112" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="128" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="134" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="150" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="156" width="106" height="8" rx="1" fill="#BDD5DB"/> +</g> +<defs> +<linearGradient id="paint0_linear_518_22068" x1="100" y1="0" x2="100" y2="162" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F3F7FA"/> +<stop offset="1" stop-color="#E8F2F9"/> +</linearGradient> +<clipPath id="clip0_518_22068"> +<rect width="200" height="162" rx="8" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/main.dart b/lib/main.dart index 2d1bab160..66b3bb974 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1200, 1100)); + setWindowMinSize(const Size(1220, 1100)); setWindowMaxSize(Size.infinite); } diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index 7a9ed557f..bfd5f3b61 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -291,7 +292,109 @@ class _ThemeToggle extends ConsumerState<ThemeToggle> { ), ), const SizedBox( - width: 20, + width: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "oceanBreeze" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeOcean, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + width: 10, ), MaterialButton( splashColor: Colors.transparent, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 38d720969..d7d2ebc6f 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index bba5f6ed2..94670af1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -316,6 +316,7 @@ flutter: - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg - assets/svg/configuration.svg + - assets/svg/ocean-breeze-theme.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From 9508afbd5b84823a264e3c091cab9768292e9e01 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:26:27 -0600 Subject: [PATCH 038/100] add ocean breeze specific assets --- assets/svg/{dark => }/dark-theme.svg | 0 assets/svg/{light => }/light-mode.svg | 0 assets/svg/oceanBreeze/bell-new.svg | 5 ++ assets/svg/oceanBreeze/buy-coins-icon.svg | 18 +++++ assets/svg/oceanBreeze/exchange-2.svg | 4 + assets/svg/oceanBreeze/stack-icon1.svg | 5 ++ .../oceanBreeze/tx-exchange-icon-failed.svg | 7 ++ .../oceanBreeze/tx-exchange-icon-pending.svg | 6 ++ assets/svg/oceanBreeze/tx-exchange-icon.svg | 4 + .../oceanBreeze/tx-icon-receive-failed.svg | 7 ++ .../oceanBreeze/tx-icon-receive-pending.svg | 5 ++ assets/svg/oceanBreeze/tx-icon-receive.svg | 4 + .../svg/oceanBreeze/tx-icon-send-failed.svg | 7 ++ .../svg/oceanBreeze/tx-icon-send-pending.svg | 6 ++ assets/svg/oceanBreeze/tx-icon-send.svg | 4 + lib/utilities/assets.dart | 5 +- pubspec.yaml | 77 ++++++++++++------- 17 files changed, 135 insertions(+), 29 deletions(-) rename assets/svg/{dark => }/dark-theme.svg (100%) rename assets/svg/{light => }/light-mode.svg (100%) create mode 100644 assets/svg/oceanBreeze/bell-new.svg create mode 100644 assets/svg/oceanBreeze/buy-coins-icon.svg create mode 100644 assets/svg/oceanBreeze/exchange-2.svg create mode 100644 assets/svg/oceanBreeze/stack-icon1.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send.svg diff --git a/assets/svg/dark/dark-theme.svg b/assets/svg/dark-theme.svg similarity index 100% rename from assets/svg/dark/dark-theme.svg rename to assets/svg/dark-theme.svg diff --git a/assets/svg/light/light-mode.svg b/assets/svg/light-mode.svg similarity index 100% rename from assets/svg/light/light-mode.svg rename to assets/svg/light-mode.svg diff --git a/assets/svg/oceanBreeze/bell-new.svg b/assets/svg/oceanBreeze/bell-new.svg new file mode 100644 index 000000000..8cef32715 --- /dev/null +++ b/assets/svg/oceanBreeze/bell-new.svg @@ -0,0 +1,5 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.5 17.5C12.5 17.9193 12.2383 18.3672 11.7695 18.6797C11.3008 18.9922 10.6289 19.1667 10 19.1667C9.33594 19.1667 8.69922 18.9922 8.23047 18.6797C7.76172 18.3672 7.5 17.9193 7.5 17.5H12.5Z" fill="#227386"/> +<path d="M11.1903 1.98716V2.67947C13.9059 3.2142 15.9519 5.54245 15.9519 8.33331V9.0112C15.9519 10.7095 16.5955 12.3429 17.7561 13.6122L18.0314 13.9114C18.3439 14.254 18.422 14.7372 18.2286 15.1518C18.0351 15.5665 17.611 15.8333 17.1423 15.8333H2.85739C2.38867 15.8333 1.96351 15.5665 1.77148 15.1518C1.57945 14.7372 1.65626 14.254 1.96771 13.9114L2.24359 13.6122C3.40573 12.3429 4.0478 10.7095 4.0478 9.0112V8.33331C4.0478 5.54245 6.06034 3.2142 8.80945 2.67947V1.98716C8.80945 1.35002 9.34141 0.833313 9.99986 0.833313C10.6583 0.833313 11.1903 1.35002 11.1903 1.98716Z" fill="#227386"/> +<ellipse cx="17.0833" cy="2.91665" rx="2.08333" ry="2.08333" fill="#D34E50"/> +</svg> diff --git a/assets/svg/oceanBreeze/buy-coins-icon.svg b/assets/svg/oceanBreeze/buy-coins-icon.svg new file mode 100644 index 000000000..d9613bccb --- /dev/null +++ b/assets/svg/oceanBreeze/buy-coins-icon.svg @@ -0,0 +1,18 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_519_18707)"> +<g opacity="0.4"> +<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#227386"/> +<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#227386"/> +<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#227386"/> +</g> +<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#227386"/> +<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#227386"/> +<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#227386"/> +<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#227386"/> +</g> +<defs> +<clipPath id="clip0_519_18707"> +<rect width="24" height="24" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/oceanBreeze/exchange-2.svg b/assets/svg/oceanBreeze/exchange-2.svg new file mode 100644 index 000000000..7baeaf87f --- /dev/null +++ b/assets/svg/oceanBreeze/exchange-2.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M19.5 6.5L20.4343 7.33045C20.8552 6.85685 20.8552 6.14315 20.4343 5.66955L19.5 6.5ZM16.4343 1.16955C15.9756 0.653567 15.1855 0.607091 14.6695 1.06574C14.1536 1.52439 14.1071 2.31448 14.5657 2.83045L16.4343 1.16955ZM14.5657 10.1695C14.1071 10.6855 14.1536 11.4756 14.6695 11.9343C15.1855 12.3929 15.9756 12.3464 16.4343 11.8305L14.5657 10.1695ZM0.75 10.5C0.75 11.1904 1.30964 11.75 2 11.75C2.69036 11.75 3.25 11.1904 3.25 10.5H0.75ZM6 7.75H19.5V5.25H6V7.75ZM14.5657 2.83045L18.5657 7.33045L20.4343 5.66955L16.4343 1.16955L14.5657 2.83045ZM16.4343 11.8305L20.4343 7.33045L18.5657 5.66955L14.5657 10.1695L16.4343 11.8305ZM3.25 10.5C3.25 8.98122 4.48122 7.75 6 7.75V5.25C3.10051 5.25 0.75 7.60051 0.75 10.5H3.25Z" fill="#227386"/> +<path opacity="0.4" d="M4.5 18L3.56574 17.1695C3.14475 17.6432 3.14475 18.3568 3.56574 18.8305L4.5 18ZM7.56574 23.3305C8.02439 23.8464 8.81448 23.8929 9.33045 23.4343C9.84643 22.9756 9.89291 22.1855 9.43426 21.6695L7.56574 23.3305ZM9.43426 14.3305C9.89291 13.8145 9.84643 13.0244 9.33046 12.5657C8.81448 12.1071 8.02439 12.1536 7.56574 12.6695L9.43426 14.3305ZM23.25 14C23.25 13.3096 22.6904 12.75 22 12.75C21.3096 12.75 20.75 13.3096 20.75 14L23.25 14ZM18 16.75L4.5 16.75L4.5 19.25L18 19.25L18 16.75ZM9.43426 21.6695L5.43426 17.1695L3.56574 18.8305L7.56574 23.3305L9.43426 21.6695ZM7.56574 12.6695L3.56574 17.1695L5.43426 18.8305L9.43426 14.3305L7.56574 12.6695ZM20.75 14C20.75 15.5188 19.5188 16.75 18 16.75L18 19.25C20.8995 19.25 23.25 16.8995 23.25 14L20.75 14Z" fill="#227386"/> +</svg> diff --git a/assets/svg/oceanBreeze/stack-icon1.svg b/assets/svg/oceanBreeze/stack-icon1.svg new file mode 100644 index 000000000..f316012d7 --- /dev/null +++ b/assets/svg/oceanBreeze/stack-icon1.svg @@ -0,0 +1,5 @@ +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M41.3715 9.57675C37.2965 7.22564 32.2041 10.1695 32.2041 14.8717C32.2041 19.5739 34.4762 23.6489 38.2004 26.163L53.9717 35.3057L54.0112 35.2908L69.9948 26.0543L41.3715 9.57675Z" fill="#B3B3B3"/> +<path d="M38.2014 26.163C34.4771 23.6489 32.205 19.4159 32.205 14.8717C32.205 12.6342 33.3757 10.7671 35.0402 9.7101C34.9612 9.75455 35.1192 9.66564 35.0402 9.7101L10.0917 23.7279L6.08593 26.1481L3.35449 27.7188C5.07337 26.8446 7.22692 26.7754 9.14831 27.8917L16.0189 31.8037L22.0399 35.2859L38.0236 44.5076L53.9677 35.2958L38.1964 26.1531L38.2014 26.163Z" fill="#666666"/> +<path d="M70 44.5187L38.0278 62.9917L31.992 59.5095L31.9673 59.4848L6.06054 44.5187C4.28733 43.3629 2.84505 41.7872 1.82755 40.014C0.642111 37.9691 0 35.618 0 33.1829C0 30.9899 1.10147 29.1771 2.70181 28.1004C2.91914 27.967 3.13153 27.8435 3.35874 27.725C5.07762 26.8507 7.23116 26.7816 9.15256 27.8979L15.9836 31.8394L22.0047 35.3068L22.0442 35.292L38.0278 44.5137L53.9719 35.3019L70 44.5137V44.5187Z" fill="#232323"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg new file mode 100644 index 000000000..a54836bba --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg new file mode 100644 index 000000000..5f9aa4256 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85605 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon.svg b/assets/svg/oceanBreeze/tx-exchange-icon.svg new file mode 100644 index 000000000..fcd3ef9dc --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-failed.svg b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg new file mode 100644 index 000000000..189bd15c9 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-pending.svg b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg new file mode 100644 index 000000000..64ea8da3d --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5 24C22.433 24 24 22.433 24 20.5C24 18.567 22.433 17 20.5 17C18.567 17 17 18.567 17 20.5C17 22.433 18.567 24 20.5 24ZM21 19C21 18.7239 20.7761 18.5 20.5 18.5C20.2239 18.5 20 18.7239 20 19V20.5C20 20.7761 20.2239 21 20.5 21H21.5C21.7761 21 22 20.7761 22 20.5C22 20.2239 21.7761 20 21.5 20H21V19Z" fill="#F4C517"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive.svg b/assets/svg/oceanBreeze/tx-icon-receive.svg new file mode 100644 index 000000000..1076d8d57 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-failed.svg b/assets/svg/oceanBreeze/tx-icon-send-failed.svg new file mode 100644 index 000000000..9751b61e8 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-pending.svg b/assets/svg/oceanBreeze/tx-icon-send-pending.svg new file mode 100644 index 000000000..e4ec777e3 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send.svg b/assets/svg/oceanBreeze/tx-icon-send.svg new file mode 100644 index 000000000..ee32aa6b4 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index d7d2ebc6f..c423ec491 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -60,12 +60,13 @@ class _SVG { "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; + String get themeLight => "assets/svg/light-mode.svg"; + String get themeDark => "assets/svg/dark-theme.svg"; + String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; String get framedAddressBook => "assets/svg/framed-address-book.svg"; - String get themeLight => "assets/svg/light/light-mode.svg"; - String get themeDark => "assets/svg/dark/dark-theme.svg"; String get circleNode => "assets/svg/node-circle.svg"; String get circleSun => "assets/svg/sun-circle.svg"; String get circleArrowRotate => "assets/svg/rotate-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 94670af1a..6a721c5c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -205,9 +205,6 @@ flutter: - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg - - assets/svg/light/bell-new.svg - - assets/svg/dark/bell-new.svg - - assets/svg/stack-icon1.svg - assets/svg/arrow-left-fa.svg - assets/svg/copy-fa.svg - assets/svg/star.svg @@ -220,10 +217,7 @@ flutter: - assets/svg/bars.svg - assets/svg/filter.svg - assets/svg/pending.svg - - assets/svg/dark/exchange-2.svg - - assets/svg/light/exchange-2.svg - assets/svg/signal-stream.svg - - assets/svg/buy-coins-icon.svg - assets/svg/Ellipse-43.svg - assets/svg/Ellipse-42.svg - assets/svg/arrow-rotate.svg @@ -265,25 +259,7 @@ flutter: - assets/svg/ellipsis-vertical1.svg - assets/svg/dice-alt.svg - assets/svg/circle-arrow-up-right2.svg - - assets/svg/dark/tx-exchange-icon.svg - - assets/svg/light/tx-exchange-icon.svg - - assets/svg/dark/tx-exchange-icon-pending.svg - - assets/svg/light/tx-exchange-icon-pending.svg - - assets/svg/dark/tx-exchange-icon-failed.svg - - assets/svg/light/tx-exchange-icon-failed.svg - assets/svg/loader.svg - - assets/svg/dark/tx-icon-send.svg - - assets/svg/light/tx-icon-send.svg - - assets/svg/dark/tx-icon-send-pending.svg - - assets/svg/light/tx-icon-send-pending.svg - - assets/svg/dark/tx-icon-send-failed.svg - - assets/svg/light/tx-icon-send-failed.svg - - assets/svg/dark/tx-icon-receive.svg - - assets/svg/light/tx-icon-receive.svg - - assets/svg/dark/tx-icon-receive-pending.svg - - assets/svg/light/tx-icon-receive-pending.svg - - assets/svg/dark/tx-icon-receive-failed.svg - - assets/svg/light/tx-icon-receive-failed.svg - assets/svg/add-backup.svg - assets/svg/auto-backup.svg - assets/svg/restore-backup.svg @@ -305,8 +281,6 @@ flutter: - assets/svg/rotate-circle.svg - assets/svg/sun-circle.svg - assets/svg/node-circle.svg - - assets/svg/dark/dark-theme.svg - - assets/svg/light/light-mode.svg - assets/svg/address-book-desktop.svg - assets/svg/about-desktop.svg - assets/svg/exchange-desktop.svg @@ -316,7 +290,6 @@ flutter: - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg - assets/svg/configuration.svg - - assets/svg/ocean-breeze-theme.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg @@ -348,6 +321,56 @@ flutter: - assets/svg/exchange_icons/change_now_logo_1.svg - assets/svg/exchange_icons/simpleswap-icon.svg + # theme selectors + - assets/svg/dark-theme.svg + - assets/svg/light-mode.svg + - assets/svg/ocean-breeze-theme.svg + + # light theme specific + - assets/svg/light/tx-exchange-icon.svg + - assets/svg/light/tx-exchange-icon-pending.svg + - assets/svg/light/tx-exchange-icon-failed.svg + - assets/svg/light/tx-icon-send.svg + - assets/svg/light/tx-icon-send-pending.svg + - assets/svg/light/tx-icon-send-failed.svg + - assets/svg/light/tx-icon-receive.svg + - assets/svg/light/tx-icon-receive-pending.svg + - assets/svg/light/tx-icon-receive-failed.svg + - assets/svg/light/exchange-2.svg + - assets/svg/light/bell-new.svg + - assets/svg/light/stack-icon1.svg + - assets/svg/light/buy-coins-icon.svg + + # dark theme specific + - assets/svg/dark/tx-exchange-icon.svg + - assets/svg/dark/tx-exchange-icon-pending.svg + - assets/svg/dark/tx-exchange-icon-failed.svg + - assets/svg/dark/tx-icon-send.svg + - assets/svg/dark/tx-icon-send-pending.svg + - assets/svg/dark/tx-icon-send-failed.svg + - assets/svg/dark/tx-icon-receive.svg + - assets/svg/dark/tx-icon-receive-pending.svg + - assets/svg/dark/tx-icon-receive-failed.svg + - assets/svg/dark/exchange-2.svg + - assets/svg/dark/bell-new.svg + - assets/svg/dark/stack-icon1.svg + - assets/svg/dark/buy-coins-icon.svg + + # light theme specific + - assets/svg/oceanBreeze/tx-exchange-icon.svg + - assets/svg/oceanBreeze/tx-exchange-icon-pending.svg + - assets/svg/oceanBreeze/tx-exchange-icon-failed.svg + - assets/svg/oceanBreeze/tx-icon-send.svg + - assets/svg/oceanBreeze/tx-icon-send-pending.svg + - assets/svg/oceanBreeze/tx-icon-send-failed.svg + - assets/svg/oceanBreeze/tx-icon-receive.svg + - assets/svg/oceanBreeze/tx-icon-receive-pending.svg + - assets/svg/oceanBreeze/tx-icon-receive-failed.svg + - assets/svg/oceanBreeze/exchange-2.svg + - assets/svg/oceanBreeze/bell-new.svg + - assets/svg/oceanBreeze/stack-icon1.svg + - assets/svg/oceanBreeze/buy-coins-icon.svg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see From 792b91b7c4d27c7212b251150ad4fafef39b96a8 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 11:26:27 -0700 Subject: [PATCH 039/100] syncing pref options show on button press + shows card w current syncing prefs --- .../syncing_preferences_settings.dart | 116 ++++++++++++++---- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 815e506db..720d77b8b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -3,9 +3,13 @@ import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class SyncingPreferencesSettings extends ConsumerStatefulWidget { @@ -20,6 +24,19 @@ class SyncingPreferencesSettings extends ConsumerStatefulWidget { class _SyncingPreferencesSettings extends ConsumerState<SyncingPreferencesSettings> { + String _currentTypeDescription(SyncingType type) { + switch (type) { + case SyncingType.currentWalletOnly: + return "Sync only currently open wallet"; + case SyncingType.selectedWalletsAtStartup: + return "Sync only selected wallets at startup"; + case SyncingType.allWalletsOnStartup: + return "Sync all wallets at startup"; + } + } + + late bool changePrefs = false; + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -34,13 +51,40 @@ class _SyncingPreferencesSettings child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - Assets.svg.circleArrowRotate, - width: 48, - height: 48, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleArrowRotate, + width: 48, + height: 48, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondaryDisabled, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _currentTypeDescription(ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType))), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2), + textAlign: TextAlign.left, + ), + ), + ), + ), + ], ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -67,28 +111,50 @@ class _SyncingPreferencesSettings ), ], ), - - ///TODO: ONLY SHOW SYNC OPTIONS ON BUTTON PRESS - Column( - children: const [ - SyncingOptionsView(), - ], - ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all( - 10, - ), - child: PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: true, - label: "Change preferences", - onPressed: () {}, - ), - ), + padding: const EdgeInsets.all( + 10, + ), + child: changePrefs + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SyncingOptionsView(), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Save", + onPressed: () { + setState(() { + changePrefs = false; + }); + }, + ), + ], + ), + ) + : Column( + children: [ + const SizedBox(height: 10), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Change preferences", + onPressed: () { + setState(() { + changePrefs = true; + }); + }, + ), + ], + )), ], ), ], From 7ef31cbf87a80598478c2ef68a4ff98cbe3c130a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:34:25 -0600 Subject: [PATCH 040/100] add back exchange menu option and adjust icon color --- .../home/desktop_menu.dart | 76 ++++++++++--------- lib/utilities/theme/ocean_breeze_colors.dart | 2 +- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index bdaa1d6ce..60a424a06 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -104,10 +104,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "My Stack", @@ -120,29 +120,33 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { const SizedBox( height: 2, ), - // DesktopMenuItem( - // icon: SvgPicture.asset( - // Assets.svg.exchangeDesktop, - // width: 20, - // height: 20, - // color: DesktopMenuItemId.exchange == ref.watch(currentDesktopMenuItemProvider.state).state - // ? Theme.of(context) - // .extension<StackColors>()! - // .textDark - // : Theme.of(context) - // .extension<StackColors>()! - // .textDark - // .withOpacity(0.8), - // ), - // label: "Exchange", - // value: DesktopMenuItemId.exchange, - // group: ref.watch(currentDesktopMenuItemProvider.state).state, - // onChanged: updateSelectedMenuItem, - // iconOnly: _width == minimizedWidth, - // ), - // const SizedBox( - // height: 2, - // ), + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.exchangeDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.exchange == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Exchange", + value: DesktopMenuItemId.exchange, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, + ), + const SizedBox( + height: 2, + ), DesktopMenuItem( icon: SvgPicture.asset( Assets.svg.bell, @@ -154,10 +158,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Notifications", @@ -181,10 +185,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Address Book", @@ -208,10 +212,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Settings", @@ -235,10 +239,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Support", @@ -262,10 +266,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "About", @@ -283,7 +287,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { height: 20, color: Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Exit", diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index ff2f4e85e..1eb06e068 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -21,7 +21,7 @@ class OceanBreezeColors extends StackColorTheme { @override Color get accentColorOrange => const Color(0xFFFF985F); @override - Color get accentColorDark => const Color(0xFF232323); + Color get accentColorDark => const Color(0xFF227386); @override Color get shadow => const Color(0xFF388192); From ea143d9ffa901d30cf9f2631b0362fab80cfee71 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:45:42 -0600 Subject: [PATCH 041/100] basic desktop exchange layout --- .../desktop_exchange_view.dart | 89 +++++++++++++++ .../subwidgets/desktop_trade_history.dart | 103 ++++++++++++++++++ .../home/desktop_home_view.dart | 10 +- lib/route_generator.dart | 7 ++ 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart new file mode 100644 index 000000000..0f44eb59b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopExchangeView extends StatefulWidget { + const DesktopExchangeView({Key? key}) : super(key: key); + + static const String routeName = "/desktopExchange"; + + @override + State<DesktopExchangeView> createState() => _DesktopExchangeViewState(); +} + +class _DesktopExchangeViewState extends State<DesktopExchangeView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: Text( + "Exchange", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + const RoundedWhiteContainer( + padding: EdgeInsets.all(24), + child: ExchangeForm(), + ), + ], + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + const RoundedWhiteContainer( + padding: EdgeInsets.all(0), + child: DesktopTradeHistory(), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart new file mode 100644 index 000000000..40eeb8c1b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; +import 'package:tuple/tuple.dart'; + +class DesktopTradeHistory extends ConsumerStatefulWidget { + const DesktopTradeHistory({Key? key}) : super(key: key); + + @override + ConsumerState<DesktopTradeHistory> createState() => + _DesktopTradeHistoryState(); +} + +class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { + @override + Widget build(BuildContext context) { + final trades = + ref.watch(tradesServiceProvider.select((value) => value.trades)); + + final tradeCount = trades.length; + final hasHistory = tradeCount > 0; + + if (hasHistory) { + return ListView.separated( + itemBuilder: (context, index) { + return TradeCard( + key: Key("tradeCard_${trades[index].uuid}"), + trade: trades[index], + onTap: () async { + final String tradeId = trades[index].tradeId; + + final lookup = ref.read(tradeSentFromStackLookupProvider).all; + + debugPrint("ALL: $lookup"); + + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); + final List<String>? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + if (txid != null && walletIds != null && walletIds.isNotEmpty) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds.first); + + debugPrint("name: ${manager.walletName}"); + + // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData + final txData = await manager.transactionData; + + final tx = txData.getAllTransactions()[txid]; + + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + tradeId, tx, walletIds.first, manager.walletName), + ), + ); + } + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4(tradeId, null, walletIds?.first, null), + ), + ); + } + }, + ); + }, + separatorBuilder: (context, index) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + }, + itemCount: tradeCount, + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Trades will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ); + } + } +} diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index b1c35f00b..9791cd867 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; @@ -29,10 +30,11 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ), - // Container( - // // todo: exchange - // color: Colors.green, - // ), + DesktopMenuItemId.exchange: const Navigator( + key: Key("desktopExchangeHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopExchangeView.routeName, + ), DesktopMenuItemId.notifications: const Navigator( key: Key("desktopNotificationsHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d7865d013..8ccc923bc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,6 +85,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; @@ -1019,6 +1020,12 @@ class RouteGenerator { builder: (_) => const DesktopNotificationsView(), settings: RouteSettings(name: settings.name)); + case DesktopExchangeView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopExchangeView(), + settings: RouteSettings(name: settings.name)); + case DesktopSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From 83e2554b545e62a6cf4af323093a150dfe054812 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 12:42:12 -0700 Subject: [PATCH 042/100] mobile theme radio buttons --- .../appearance_settings_view.dart | 364 +++++++++++++++--- 1 file changed, 309 insertions(+), 55 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index 3a1b842f6..d1e893802 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; @@ -18,6 +19,17 @@ class AppearanceSettingsView extends ConsumerWidget { static const String routeName = "/appearanceSettings"; + String chooseThemeType(ThemeType type) { + switch (type) { + case ThemeType.light: + return "Light theme"; + case ThemeType.oceanBreeze: + return "Ocean theme"; + case ThemeType.dark: + return "Dark theme"; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( @@ -100,68 +112,39 @@ class AppearanceSettingsView extends ConsumerWidget { height: 10, ), RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Enable dark mode", + "Choose Theme", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: (DB.instance.get<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme") - as String?) == - "dark", - onValueChanged: (newValue) { - DB.instance.put<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: (newValue - ? ThemeType.dark - : (newValue - ? ThemeType.light - : ThemeType - .oceanBreeze)) - .name, - ); - ref - .read(colorThemeProvider.state) - .state = - StackColors.fromStackColorTheme( - newValue - ? DarkColors() - : LightColors()); - }, - ), - ) + const Padding( + padding: EdgeInsets.all(10), + child: ThemeOptionsView(), + ), ], ), - ), - ); - }, + ], + ), + ), ), ), ], @@ -175,3 +158,274 @@ class AppearanceSettingsView extends ConsumerWidget { ); } } + +class ThemeOptionsView extends ConsumerStatefulWidget { + const ThemeOptionsView({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<ThemeOptionsView> createState() => _ThemeOptionsView(); +} + +class _ThemeOptionsView extends ConsumerState<ThemeOptionsView> { + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} From 9956a497df081a54a67a90ae68f93ee5d6e32cc3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 13:26:17 -0700 Subject: [PATCH 043/100] ocean breeze shadow color fix --- lib/utilities/theme/ocean_breeze_colors.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index 1eb06e068..665eaa0c3 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -24,7 +24,7 @@ class OceanBreezeColors extends StackColorTheme { Color get accentColorDark => const Color(0xFF227386); @override - Color get shadow => const Color(0xFF388192); + Color get shadow => const Color(0x0F2D3132); @override Color get textDark => const Color(0xFF232323); From e665926b1bc229e6abee16bdda38debe242191e4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 14:59:53 -0600 Subject: [PATCH 044/100] firo anonymize navigation fix --- .../wallet_view/desktop_wallet_view.dart | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index d08864eee..85dde4aba 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -193,6 +193,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (publicBalance <= Decimal.zero) { shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); @@ -211,6 +212,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { await firoWallet.anonymizeAllPublicFunds(); shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); @@ -225,14 +227,53 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } catch (e) { shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); await showDialog<dynamic>( context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", + builder: (_) => DesktopDialog( + maxWidth: 400, + maxHeight: 300, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Anonymize all failed", + style: STextStyles.desktopH3(context), + ), + const Spacer( + flex: 1, + ), + Text( + "Reason: $e", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ), + ], + ) + ], + ), + ), ), ); } From 9ba83f36eb480ff0dab6af8ca4a2ebed5d37642d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:05:15 -0600 Subject: [PATCH 045/100] desktop exchange rate toggle style --- assets/svg/lock-open.svg | 3 + .../sub_widgets/rate_type_toggle.dart | 115 +++++++++++++----- pubspec.yaml | 1 + 3 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 assets/svg/lock-open.svg diff --git a/assets/svg/lock-open.svg b/assets/svg/lock-open.svg new file mode 100644 index 000000000..f2b00f341 --- /dev/null +++ b/assets/svg/lock-open.svg @@ -0,0 +1,3 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 1.75C5.79141 1.75 4.8125 2.72945 4.8125 3.9375V5.25H11.375C12.3402 5.25 13.125 6.03477 13.125 7V12.25C13.125 13.2152 12.3402 14 11.375 14H2.625C1.6584 14 0.875 13.2152 0.875 12.25V7C0.875 6.03477 1.6584 5.25 2.625 5.25H3.0625V3.9375C3.0625 1.76285 4.82617 0 7 0C8.57227 0 9.92578 0.921211 10.5574 2.24957C10.7652 2.68598 10.5793 3.20742 10.1199 3.41523C9.68242 3.62305 9.18476 3.43711 8.97695 2.99961C8.62422 2.25941 7.87227 1.75 7 1.75ZM7.875 10.5C8.35898 10.5 8.75 10.109 8.75 9.625C8.75 9.14102 8.35898 8.75 7.875 8.75H6.125C5.64102 8.75 5.25 9.14102 5.25 9.625C5.25 10.109 5.64102 10.5 6.125 10.5H7.875Z" fill="#0056D2"/> +</svg> diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 31460c75f..9697710e8 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -7,8 +7,8 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; class RateTypeToggle extends ConsumerWidget { const RateTypeToggle({ @@ -21,12 +21,17 @@ class RateTypeToggle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + final estimated = ref.watch(prefsChangeNotifierProvider .select((value) => value.exchangeRateType)) == ExchangeRateType.estimated; - return RoundedWhiteContainer( + return RoundedContainer( padding: const EdgeInsets.all(0), + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.buttonBackSecondary + : Theme.of(context).extension<StackColors>()!.popupBG, child: Row( children: [ Expanded( @@ -39,6 +44,9 @@ class RateTypeToggle extends ConsumerWidget { } }, child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), color: estimated ? Theme.of(context) .extension<StackColors>()! @@ -48,29 +56,50 @@ class RateTypeToggle extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( - Assets.svg.lock, + Assets.svg.lockOpen, width: 12, height: 14, - color: estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + color: isDesktop + ? estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), const SizedBox( width: 5, ), Text( "Estimate rate", - style: STextStyles.smallMed12(context).copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ], ), @@ -87,6 +116,9 @@ class RateTypeToggle extends ConsumerWidget { } }, child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), color: !estimated ? Theme.of(context) .extension<StackColors>()! @@ -99,26 +131,47 @@ class RateTypeToggle extends ConsumerWidget { Assets.svg.lock, width: 12, height: 14, - color: !estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + color: isDesktop + ? !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), const SizedBox( width: 5, ), Text( "Fixed rate", - style: STextStyles.smallMed12(context).copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 6a721c5c1..e8f417586 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -228,6 +228,7 @@ flutter: - assets/svg/chevron-down.svg - assets/svg/chevron-up.svg - assets/svg/lock-keyhole.svg + - assets/svg/lock-open.svg - assets/svg/rotate-exclamation.svg - assets/svg/folder-down.svg - assets/svg/network-wired.svg From 16113fd1d52319b855cb6e38044da84f810feb7a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:05:46 -0600 Subject: [PATCH 046/100] desktop exchange provider options dropdown style --- .../exchange_provider_options.dart | 706 ++++++++++-------- 1 file changed, 379 insertions(+), 327 deletions(-) diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 2113e199c..4dd768403 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -15,7 +15,9 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class ExchangeProviderOptions extends ConsumerWidget { @@ -38,353 +40,403 @@ class ExchangeProviderOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isDesktop = Util.isDesktop; return RoundedWhiteContainer( + padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, child: Column( children: [ - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - ChangeNowExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: ChangeNowExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + ChangeNowExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + ChangeNowExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: ChangeNowExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.changeNow, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ChangeNowExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: ChangeNowExchange().getEstimate( - from!, - to!, - reversed ? toAmount! : fromAmount!, - fixedRate, - reversed, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.changeNow, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ChangeNowExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = - (toAmount! / estimate.estimatedAmount) + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: ChangeNowExchange().getEstimate( + from!, + to!, + reversed ? toAmount! : fromAmount!, + fixedRate, + reversed, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate; + if (estimate.reversed) { + rate = (toAmount! / + estimate.estimatedAmount) .toDecimal( scaleOnInfinitePrecision: 12); + } else { + rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); + } + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: to!.toUpperCase() == + Coin.monero.ticker + .toUpperCase() + ? Constants.decimalPlacesMonero + : Constants.decimalPlaces, + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } } else { - rate = - (estimate.estimatedAmount / fromAmount!) - .toDecimal( - scaleOnInfinitePrecision: 12); - } - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - SimpleSwapExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - SimpleSwapExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SimpleSwapExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + if (isDesktop) + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + SimpleSwapExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + SimpleSwapExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SimpleSwapExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.simpleSwap, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - SimpleSwapExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: SimpleSwapExchange().getEstimate( - from!, - to!, - // reversed ? toAmount! : fromAmount!, - fromAmount!, - fixedRate, - // reversed, - false, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.simpleSwap, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + SimpleSwapExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate = (estimate.estimatedAmount / - fromAmount!) - .toDecimal(scaleOnInfinitePrecision: 12); + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: SimpleSwapExchange().getEstimate( + from!, + to!, + // reversed ? toAmount! : fromAmount!, + fromAmount!, + fixedRate, + // reversed, + false, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: to!.toUpperCase() == + Coin.monero.ticker + .toUpperCase() + ? Constants.decimalPlacesMonero + : Constants.decimalPlaces, + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - // if (!(from != null && - // to != null && - // (reversed - // ? toAmount != null && toAmount! > Decimal.zero - // : fromAmount != null && - // fromAmount! > Decimal.zero))) - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + // if (!(from != null && + // to != null && + // (reversed + // ? toAmount != null && toAmount! > Decimal.zero + // : fromAmount != null && + // fromAmount! > Decimal.zero))) + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), From 96453e90541ce78a9c7a2f6e57f81554fc1839c0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:06:03 -0600 Subject: [PATCH 047/100] missing asset declaration --- lib/utilities/assets.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index c423ec491..6fbe61005 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -107,6 +107,7 @@ class _SVG { String get swap => "assets/svg/swap.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; + String get lockOpen => "assets/svg/lock-open.svg"; String get network => "assets/svg/network-wired.svg"; String get networkWired => "assets/svg/network-wired-2.svg"; String get addressBook => "assets/svg/address-book.svg"; From 3ae38c582bdcd70c9ab68560ef832a6a7aad3fdf Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:06:29 -0600 Subject: [PATCH 048/100] desktop exchange form layout --- lib/pages/exchange_view/exchange_form.dart | 194 ++++++++++++++------- 1 file changed, 134 insertions(+), 60 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7b04f90b3..5ece5aba8 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -27,9 +27,12 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -54,6 +57,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { late final TextEditingController _sendController; late final TextEditingController _receiveController; + final isDesktop = Util.isDesktop; final FocusNode _sendFocusNode = FocusNode(); final FocusNode _receiveFocusNode = FocusNode(); @@ -960,8 +964,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { color: Theme.of(context).extension<StackColors>()!.textDark3, ), ), - const SizedBox( - height: 4, + SizedBox( + height: isDesktop ? 10 : 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( @@ -970,6 +974,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { focusNode: _sendFocusNode, controller: _sendController, textAlign: TextAlign.right, + enableSuggestions: false, + autocorrect: false, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; @@ -1100,68 +1106,122 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ), ), - const SizedBox( - height: 4, + + SizedBox( + height: isDesktop ? 10 : 4, ), - Stack( + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + !ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Positioned.fill( - child: Align( - alignment: Alignment.bottomLeft, - child: Text( - "You will receive", - style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), + Text( + "You will receive", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: RoundedContainer( + padding: const EdgeInsets.all(6), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: child, ), ), - ), - Center( - child: Column( - children: [ - const SizedBox( - height: 6, + child: GestureDetector( + onTap: () async { + await _swap(); + }, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg.swap, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, ), - GestureDetector( - onTap: () async { - await _swap(); - }, - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.swap, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - const SizedBox( - height: 6, - ), - ], - ), - ), - Positioned.fill( - child: Align( - alignment: ref.watch(exchangeFormStateProvider - .select((value) => value.reversed)) - ? Alignment.bottomRight - : Alignment.topRight, - child: Text( - ref.watch(exchangeFormStateProvider - .select((value) => value.warning)), - style: STextStyles.errorSmall(context), ), ), ), ], ), - const SizedBox( - height: 4, + // Stack( + // children: [ + // Positioned.fill( + // child: Align( + // alignment: Alignment.bottomLeft, + // child: Text( + // "You will receive", + // style: STextStyles.itemSubtitle(context).copyWith( + // color: + // Theme.of(context).extension<StackColors>()!.textDark3, + // ), + // ), + // ), + // ), + // Center( + // child: Column( + // children: [ + // const SizedBox( + // height: 6, + // ), + // GestureDetector( + // onTap: () async { + // await _swap(); + // }, + // child: Padding( + // padding: const EdgeInsets.all(4), + // child: SvgPicture.asset( + // Assets.svg.swap, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension<StackColors>()! + // .accentColorDark, + // ), + // ), + // ), + // const SizedBox( + // height: 6, + // ), + // ], + // ), + // ), + // Positioned.fill( + // child: Align( + // alignment: ref.watch(exchangeFormStateProvider + // .select((value) => value.reversed)) + // ? Alignment.bottomRight + // : Alignment.topRight, + // child: Text( + // ref.watch(exchangeFormStateProvider + // .select((value) => value.warning)), + // style: STextStyles.errorSmall(context), + // ), + // ), + // ), + // ], + // ), + SizedBox( + height: isDesktop ? 10 : 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( @@ -1169,6 +1229,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), focusNode: _receiveFocusNode, controller: _receiveController, + enableSuggestions: false, + autocorrect: false, readOnly: ref.watch(prefsChangeNotifierProvider .select((value) => value.exchangeRateType)) == ExchangeRateType.estimated || @@ -1304,16 +1366,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ), ), - const SizedBox( - height: 12, + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + SizedBox( + height: isDesktop ? 20 : 12, ), RateTypeToggle( onChanged: onRateTypeChanged, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) - const SizedBox( - height: 8, + SizedBox( + height: isDesktop ? 20 : 12, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) @@ -1328,10 +1401,11 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { reversed: ref.watch( exchangeFormStateProvider.select((value) => value.reversed)), ), - const SizedBox( - height: 12, + SizedBox( + height: isDesktop ? 20 : 12, ), PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, enabled: ref.watch( exchangeFormStateProvider.select((value) => value.canExchange)), onPressed: ref.watch(exchangeFormStateProvider From 51cfc3f4dfce57fd22832173bdd0f2a3daddbdf0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:14:27 -0600 Subject: [PATCH 049/100] light colors accent blue fix? --- lib/utilities/theme/light_colors.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index ea3a7cb92..896ae4e5e 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -11,7 +11,7 @@ class LightColors extends StackColorTheme { Color get overlay => const Color(0xFF111215); @override - Color get accentColorBlue => const Color(0xFF4C86E9); + Color get accentColorBlue => const Color(0xFF0052DF); @override Color get accentColorGreen => const Color(0xFF4CC0A0); @override From 80802727381722405be4bee6a06941b076d9510d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 16:19:38 -0700 Subject: [PATCH 050/100] WIP: delete wallet --- .../wallet_view/desktop_wallet_view.dart | 5 + .../sub_widgets/delete_wallet_button.dart | 238 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 85dde4aba..739dbe1a1 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; @@ -412,6 +413,10 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { WalletKeysButton( walletId: walletId, ), + const SizedBox( + width: 2, + ), + DeleteWalletButton(), const SizedBox( width: 12, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart new file mode 100644 index 000000000..f45367d0b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +class DeleteWalletButton extends ConsumerStatefulWidget { + const DeleteWalletButton({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<DeleteWalletButton> createState() => _DeleteWalletButton(); +} + +class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> attentionDelete() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + ), + ], + ), + ], + ), + ), + ); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + showDialog( + barrierDismissible: true, + context: context, + builder: (context) => DesktopDialog( + maxHeight: 475, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Delete wallet", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopDeleteWalletPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "desktopDeleteWalletShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = + passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + enabled: _continueEnabled, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + attentionDelete(); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.ellipsis, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ], + ), + ), + ); + } +} From 92da601fb80350706b412bc72bc2f952120446a0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 17:43:35 -0700 Subject: [PATCH 051/100] WIP: delete wallete Attention dialog --- .../sub_widgets/delete_wallet_button.dart | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index f45367d0b..96930c044 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; class DeleteWalletButton extends ConsumerStatefulWidget { @@ -34,7 +35,7 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { barrierDismissible: true, builder: (context) => DesktopDialog( maxWidth: 580, - maxHeight: 400, + maxHeight: 530, child: Column( children: [ Row( @@ -43,17 +44,62 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { DesktopDialogCloseButton(), ], ), - Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Text( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( "Attention!", style: STextStyles.desktopH2(context), ), - ), - ], + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ) + ], + ), ), ], ), @@ -84,7 +130,7 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { borderRadius: BorderRadius.circular(1000), ), onPressed: () { - showDialog( + showDialog<dynamic>( barrierDismissible: true, context: context, builder: (context) => DesktopDialog( From 5f1a485ed5b4a7f2db44bee6a06172774fafe5a6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Sat, 19 Nov 2022 11:00:15 -0700 Subject: [PATCH 052/100] WIP: delete wallete stateful widget + attention warning dialog --- .../wallet_view/desktop_wallet_view.dart | 4 +- .../sub_widgets/delete_wallet_button.dart | 253 ++------------- .../desktop_delete_wallet_dialog.dart | 299 ++++++++++++++++++ lib/route_generator.dart | 23 ++ 4 files changed, 350 insertions(+), 229 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 739dbe1a1..5996597b5 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -416,7 +416,9 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 2, ), - DeleteWalletButton(), + DeleteWalletButton( + walletId: walletId, + ), const SizedBox( width: 12, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index 96930c044..54f991c37 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -1,128 +1,37 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; -import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; + +import 'desktop_delete_wallet_dialog.dart'; class DeleteWalletButton extends ConsumerStatefulWidget { const DeleteWalletButton({ Key? key, + required this.walletId, }) : super(key: key); + final String walletId; + @override ConsumerState<DeleteWalletButton> createState() => _DeleteWalletButton(); } class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { - late final TextEditingController passwordController; - late final FocusNode passwordFocusNode; - - bool hidePassword = true; - bool _continueEnabled = false; - - Future<void> attentionDelete() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: 530, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackError, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " - "the only way you can have access to your funds is by using your backup key." - "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." - "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "View Backup Key", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } + late final String walletId; @override void initState() { - passwordController = TextEditingController(); - passwordFocusNode = FocusNode(); + walletId = widget.walletId; + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); super.initState(); } - @override - void dispose() { - passwordController.dispose(); - passwordFocusNode.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return RawMaterialButton( @@ -130,134 +39,22 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { borderRadius: BorderRadius.circular(1000), ), onPressed: () { - showDialog<dynamic>( - barrierDismissible: true, + showDialog<void>( context: context, - builder: (context) => DesktopDialog( - maxHeight: 475, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - const SizedBox(height: 16), - Text( - "Delete wallet", - style: STextStyles.desktopH2(context), - ), - const SizedBox(height: 16), - Text( - "Enter your password", - style: STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - const SizedBox(height: 24), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("desktopDeleteWalletPasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - labelStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: SizedBox( - height: 70, - child: Row( - children: [ - const SizedBox( - width: 24, - ), - GestureDetector( - key: const Key( - "desktopDeleteWalletShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 24, - ), - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - ), - onChanged: (newValue) { - setState(() { - _continueEnabled = - passwordController.text.isNotEmpty; - }); - }, - ), - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - enabled: _continueEnabled, - label: "Continue", - onPressed: () { - Navigator.of(context).pop(); - - attentionDelete(); - }, - ), - ], - ) - ], + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: walletId, ), - ), - ], - ), + ) + ]; + }, ), ); }, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart new file mode 100644 index 000000000..e2ab4fa86 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -0,0 +1,299 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../../../providers/global/wallets_provider.dart'; + +class DesktopDeleteWalletDialog extends ConsumerStatefulWidget { + const DesktopDeleteWalletDialog({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopDeleteWalletDialog"; + + @override + ConsumerState<DesktopDeleteWalletDialog> createState() => + _DesktopDeleteWalletDialog(); +} + +class _DesktopDeleteWalletDialog + extends ConsumerState<DesktopDeleteWalletDialog> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> attentionDelete() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 610, + maxHeight: 530, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () {}, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Delete wallet", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopDeleteWalletPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "desktopDeleteWalletShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + enabled: _continueEnabled, + label: "Continue", + onPressed: _continueEnabled + ? () async { + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + Navigator.of(context).pop(); + + attentionDelete(); + } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + : null, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 8ccc923bc..77b5e7d11 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -92,6 +92,7 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; @@ -1170,6 +1171,28 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopDeleteWalletDialog.routeName: + if (args is String) { + return FadePageRoute( + DesktopDeleteWalletDialog( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( From a8faa7b8e7b851c0e919b0678f9d066fc237120a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:20:43 -0600 Subject: [PATCH 053/100] exchange form desktop routing and dialogs --- lib/pages/exchange_view/exchange_form.dart | 223 +++++++++++++----- .../desktop/simple_desktop_dialog.dart | 65 +++++ 2 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 lib/widgets/desktop/simple_desktop_dialog.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 5ece5aba8..a6d736228 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -30,7 +30,11 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -139,14 +143,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); + return; } }, @@ -229,14 +246,26 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); return; } }, @@ -324,7 +353,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await ref.read(exchangeFormStateProvider).swap(); } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } _swapLock = false; } @@ -567,14 +596,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } if (!(fromTicker == "-" || toTicker == "-")) { unawaited( @@ -620,7 +649,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { true, ); if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; case SimpleSwapExchange.exchangeName: @@ -657,7 +686,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } @@ -669,7 +698,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } unawaited( showFloatingFlushBar( @@ -722,15 +751,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } if (!isAvailable) { - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Selected trade pair unavailable", - message: - "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: true, + builder: (_) { + if (isDesktop) { + return SimpleDesktopDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } else { + return StackDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } + }, ), - )); + ); return; } rate = @@ -744,37 +785,101 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { shouldCancel = await showDialog<bool?>( context: context, barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to update trade estimate", - message: - "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - // notify return to cancel - Navigator.of(context).pop(true); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Attempt", - style: STextStyles.button(context), - ), - onPressed: () { - // continue and try to attempt trade - Navigator.of(context).pop(false); - }, - ), - ), + builder: (_) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Failed to update trade estimate", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + estimate.warningMessage!, + style: STextStyles.desktopTextSmall(context), + ), + const Spacer(), + Text( + "Do you want to attempt trade anyways?", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Attempt", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + ], + ) + ], + ), + ); + } else { + return StackDialog( + title: "Failed to update trade estimate", + message: + "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + // notify return to cancel + Navigator.of(context).pop(true); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Attempt", + style: STextStyles.button(context), + ), + onPressed: () { + // continue and try to attempt trade + Navigator.of(context).pop(false); + }, + ), + ); + } + }, ); } diff --git a/lib/widgets/desktop/simple_desktop_dialog.dart b/lib/widgets/desktop/simple_desktop_dialog.dart new file mode 100644 index 000000000..cd066c221 --- /dev/null +++ b/lib/widgets/desktop/simple_desktop_dialog.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; + +class SimpleDesktopDialog extends StatelessWidget { + const SimpleDesktopDialog({ + Key? key, + required this.title, + required this.message, + }) : super(key: key); + + final String title; + final String message; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + message, + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ), + ], + ) + ], + ), + ); + } +} From b2ff99be19400374c71b9e2fbe6be4eb08bc8147 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:20:58 -0600 Subject: [PATCH 054/100] login loading indicator size --- lib/pages_desktop_specific/desktop_login_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index f60ce2240..f865fad47 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -49,8 +49,15 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { unawaited( showDialog( context: context, - builder: (context) => const LoadingIndicator( - width: 200, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], ), ), ); From cc4dc9e3c71d67745fd0294e38dd74ddd709f413 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:24:32 -0600 Subject: [PATCH 055/100] exchange rate type toggle mouse regions --- .../sub_widgets/rate_type_toggle.dart | 287 ++++++++++-------- 1 file changed, 153 insertions(+), 134 deletions(-) diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 9697710e8..31ee01ce2 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; class RateTypeToggle extends ConsumerWidget { @@ -35,145 +36,163 @@ class RateTypeToggle extends ConsumerWidget { child: Row( children: [ Expanded( - child: GestureDetector( - onTap: () { - if (!estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; - onChanged?.call(ExchangeRateType.estimated); - } - }, - child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lockOpen, - width: 12, - height: 14, - color: isDesktop - ? estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Estimate rate", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ) - : STextStyles.smallMed12(context).copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (!estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.estimated; + onChanged?.call(ExchangeRateType.estimated); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lockOpen, + width: 12, + height: 14, + color: isDesktop + ? estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + const SizedBox( + width: 5, + ), + Text( + "Estimate rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), ), Expanded( - child: GestureDetector( - onTap: () { - if (estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.fixed; - onChanged?.call(ExchangeRateType.fixed); - } - }, - child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lock, - width: 12, - height: 14, - color: isDesktop - ? !estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Fixed rate", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ) - : STextStyles.smallMed12(context).copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: !estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.fixed; + onChanged?.call(ExchangeRateType.fixed); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lock, + width: 12, + height: 14, + color: isDesktop + ? !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + const SizedBox( + width: 5, + ), + Text( + "Fixed rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), From 601001f96df83e182959782e1de05e90e782ce9a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 10:01:09 -0600 Subject: [PATCH 056/100] WIP: desktop exchange steps flow ui --- lib/pages/exchange_view/exchange_form.dart | 54 ++++++-- .../exchange_steps/step_scaffold.dart | 55 ++++++++ .../desktop_exchange_steps_indicator.dart | 121 ++++++++++++++++++ 3 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index a6d736228..2d89c7660 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view. import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_options.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; @@ -908,20 +909,49 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (walletInitiated) { ref.read(exchangeSendFromWalletIdStateProvider.state).state = Tuple2(walletId!, coin!); - unawaited( - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ), - ); + if (isDesktop) { + await showDialog<void>( + context: context, + builder: (context) { + return const DesktopDialog( + maxWidth: 700, + child: StepScaffold( + step: 1, + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model, + ), + ); + } } else { ref.read(exchangeSendFromWalletIdStateProvider.state).state = null; - unawaited( - Navigator.of(context).pushNamed( - Step1View.routeName, - arguments: model, - ), - ); + + if (isDesktop) { + await showDialog<void>( + context: context, + builder: (context) { + return const DesktopDialog( + maxWidth: 700, + child: StepScaffold( + step: 0, + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step1View.routeName, + arguments: model, + ), + ); + } } } } diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart new file mode 100644 index 000000000..09aea9dbf --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; + +class StepScaffold extends StatefulWidget { + const StepScaffold({Key? key, required this.step}) : super(key: key); + + final int step; + + @override + State<StepScaffold> createState() => _StepScaffoldState(); +} + +class _StepScaffoldState extends State<StepScaffold> { + int currentStep = 0; + + @override + void initState() { + currentStep = widget.step; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + const AppBarBackButton( + isCompact: true, + ), + Text( + "Exchange XXX to XXX", + style: STextStyles.desktopH3(context), + ), + ], + ), + const SizedBox( + height: 32, + ), + DesktopExchangeStepsIndicator( + currentStep: currentStep, + ), + const SizedBox( + height: 32, + ), + Container( + height: 200, + color: Colors.red, + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart new file mode 100644 index 000000000..44831bb4b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class DesktopExchangeStepsIndicator extends StatelessWidget { + const DesktopExchangeStepsIndicator({Key? key, required this.currentStep}) + : super(key: key); + + final int currentStep; + + Color getColor(BuildContext context, int step) { + if (currentStep > step) { + return Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + .withOpacity(0.5); + } else if (currentStep < step) { + return Theme.of(context).extension<StackColors>()!.textSubtitle3; + } else { + return Theme.of(context).extension<StackColors>()!.accentColorBlue; + } + } + + static const double verticalSpacing = 4; + static const double horizontalSpacing = 16; + static const double barHeight = 6; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 0), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 0), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Enter details", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 1), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 1), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Confirm details", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 2), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 2), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Complete exchange", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 3), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 3), + height: barHeight, + ), + ], + ), + ), + ], + ); + } +} From 90dc9e3116747ca80f0aa541bfce550bbee1113c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 11:32:18 -0600 Subject: [PATCH 057/100] mobile button height fix --- .../manage_nodes_views/node_details_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 3d49ae6f7..71d764135 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -349,7 +349,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { Expanded( child: SecondaryButton( label: "Test connection", - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { await _testConnection(ref, context); }, From d4d85259e1c89d6f418c044a48e432704c83501b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 12:52:32 -0600 Subject: [PATCH 058/100] logging fix --- lib/services/coins/wownero/wownero_wallet.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index e39d13005..3d5ae3ad6 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -863,7 +863,8 @@ class WowneroWallet extends CoinServiceAPI { await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, curIndex); - Logging.instance.log("xmr address in init existing: $newReceivingAddress", + Logging.instance.log( + "wownero address in init existing: $newReceivingAddress", level: LogLevel.Info); _currentReceivingAddress = Future(() => newReceivingAddress); } From 719c7abd49906cc621612aabd19e4e1fe1776f43 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 13:44:36 -0600 Subject: [PATCH 059/100] clean up logs --- lib/services/coins/monero/monero_wallet.dart | 4 ++-- lib/services/coins/wownero/wownero_wallet.dart | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index c35323d53..f94f0cd2a 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -185,8 +185,8 @@ class MoneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 3d5ae3ad6..e6a531b78 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -153,7 +153,7 @@ class WowneroWallet extends CoinServiceAPI { try { _height = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int blocksRemaining = -1; @@ -162,7 +162,7 @@ class WowneroWallet extends CoinServiceAPI { blocksRemaining = (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int currentHeight = _height + blocksRemaining; if (_height == -1 || blocksRemaining == -1) { @@ -186,8 +186,8 @@ class WowneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} @@ -195,7 +195,7 @@ class WowneroWallet extends CoinServiceAPI { try { syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } final cachedHeight = DB.instance.get<dynamic>(boxName: walletId, key: "storedSyncingHeight") @@ -418,7 +418,7 @@ class WowneroWallet extends CoinServiceAPI { try { progress = (walletBase!.syncStatus!).progress(); } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } await _fetchTransactionData(); From b333253287c44beb6bc86428cdfe3f55eb938df9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 15:12:08 -0600 Subject: [PATCH 060/100] reduce minimum window height --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 66b3bb974..6879b69c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1220, 1100)); + setWindowMinSize(const Size(1220, 1000)); setWindowMaxSize(Size.infinite); } From e2a172f7477550da845224003325da7bfbce35c5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 18:04:53 -0600 Subject: [PATCH 061/100] firo private/public balance desktop toggle --- assets/images/glasses-hidden.png | Bin 0 -> 1060 bytes assets/images/glasses.png | Bin 0 -> 1955 bytes .../desktop_balance_toggle_button.dart | 60 +++ .../sub_widgets/desktop_wallet_summary.dart | 375 +++++++----------- lib/utilities/assets.dart | 3 + pubspec.yaml | 2 + 6 files changed, 215 insertions(+), 225 deletions(-) create mode 100644 assets/images/glasses-hidden.png create mode 100644 assets/images/glasses.png create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart diff --git a/assets/images/glasses-hidden.png b/assets/images/glasses-hidden.png new file mode 100644 index 0000000000000000000000000000000000000000..9176cc69b914fb6b435fe881fd4987835a0fd5dc GIT binary patch literal 1060 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xg+b<JR45HFasD-dK5TavfC3&Vd9T(EcfWS|Ip ziKnkC`!gmfA#Qn#&!RvBm^D0I978hhy}fZZJ0?`(_(y+ZwT&*^T_Kt*8^v4}zBn|s zgS%Ys5A&U0;vGA}1Y9mG^**?Cg@Rs}NMN9W%SzsBZrvT7x2A`z{&seW=Pb{4-NkuD z#ygE?s+2G3e$L%)U7Y^zukHTNpXP`+DR3a69ZPS9*IE4fA^i0FUx~xAKX=J1OT4Qn z6V;A5CM2evDrp{g;LFLA`<hM~PATk3FnK-w>9?AEiM4&lZ%393nj6eNt<+`IRx3Jj z`A3<)2MX`bC>`Fx>nzLUYIgVBvFV#G?BCJk{ew5%pnLy@3IC%*dU&6A?PceGFtNtA zV|S5E^34|>eM>`g*PMxR*SY@DJne&>-9-6m@!y2z-3+yVc{a<e$~Jy_Q9+SvUz#)f zlDMn4THW;L9crF&z$vrB_Z46Bf~jX^Brg2^ooHDd^M09j1+)6U<qTZXcdIVUIrREc z#I;=}<>&UcAA4wHcRbO?MB!t0g?Ro$<;*$q(~6EN{17>x+%?6<db7#O{qlzoGjlu; z-J{l<9rUPDF#p!9mT#wY=1raS?3d5Rd6nA1$9eWiHQdY&S{5_c&NA|qXpr_FV}`x! zKL)pN*A4bP$q&@cKWFXEdPgZg-{ND(asu*q1PR-FUv{5-=0VZfzpEqHd)~^5D(`;C zwRzV2w9BuS?+cxi7x7xFZsqTVvmbWfED7H7QFnIso==rtn{8i}&$-?Cu(IZQ0>ks| zmG?{dHfipg9#vU=cxu%A2~o>9)vl<onPgdWbHcT#YpY{#@O-WKz*3j1(7?K9x%bo1 zWy|@Otjt~?b0FpE-CAk8?#H|=1w8R3+zSq|*yT99H;xqw3kRB1?|$pi+i9A6v~s1l zKVG)w+21t&D)EoodZ*qI+m(0p)30Ctnh#};-mUuHG0DlR>i9>dx-}t5x>LXYNnL4u zue~ty^$q^)OCPEw<-cEgy|ZrZoTi?~o;Gd2H~+lYb%Qtavc0;lf!}kZ{c~?h*<Skh z<6=zTg9#-P25PbrB9~0cTxZq!d7geKIrVa{tn@n}pVQBjr#X3fyv^B{-)wzBX4B6n zOKox^Pwwi?WOEc?furp<{~34v{Bv>IuQX?1Hc~BdjVMV;EJ?LWE=mPb3`Pb<M!E(@ zx<+Oph89+ahE_)A+6D$z1_t}Z-)}?Fkei>9nO2Eg!#S^TP<g=M>FVdQ&MBb@01M61 A`2YX_ literal 0 HcmV?d00001 diff --git a/assets/images/glasses.png b/assets/images/glasses.png new file mode 100644 index 0000000000000000000000000000000000000000..8c9e7dc276660d6b8284916943c78137a0d6ad2f GIT binary patch literal 1955 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 Fz~8{u`yl`T literal 0 HcmV?d00001 diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart new file mode 100644 index 000000000..9c890b223 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class DesktopBalanceToggleButton extends ConsumerWidget { + const DesktopBalanceToggleButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 22, + width: 22, + child: MaterialButton( + color: Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + onPressed: () { + if (ref.read(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + } else { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + } + onPressed?.call(); + }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Center( + child: Image( + image: AssetImage( + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available + ? Assets.png.glassesHidden + : Assets.png.glasses, + ), + width: 16, + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index f4bfed976..d6e99ce70 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -1,13 +1,15 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -33,19 +35,6 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { late final String walletId; late final ChangeNotifierProvider<Manager> managerProvider; - void showSheet() { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => WalletBalanceToggleSheet(walletId: walletId), - ); - } - Decimal? _balanceTotalCached; Decimal? _balanceCached; @@ -59,225 +48,161 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( + return Consumer( + builder: (context, ref, __) { + final Coin coin = + ref.watch(managerProvider.select((value) => value.coin)); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (_, ref, __) { - final Coin coin = - ref.watch(managerProvider.select((value) => value.coin)); - final externalCalls = ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls)); + Column( + children: [ + Consumer( + builder: (_, ref, __) { + final externalCalls = ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls)); - Future<Decimal>? totalBalanceFuture; - Future<Decimal>? availableBalanceFuture; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = - ref.watch(managerProvider.select((value) => value.wallet)) + Future<Decimal>? totalBalanceFuture; + Future<Decimal>? availableBalanceFuture; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref.watch( + managerProvider.select((value) => value.wallet)) as FiroWallet; - totalBalanceFuture = firoWallet.availablePublicBalance(); - availableBalanceFuture = firoWallet.availablePrivateBalance(); - } else { - totalBalanceFuture = ref.watch( - managerProvider.select((value) => value.totalBalance)); - - availableBalanceFuture = ref.watch(managerProvider - .select((value) => value.availableBalance)); - } - - final locale = ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)); - - final baseCurrency = ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)); - - final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); - - final _showAvailable = false; - // ref.watch(walletBalanceToggleStateProvider.state).state == - // WalletBalanceToggleState.available; - - return FutureBuilder( - future: _showAvailable - ? availableBalanceFuture - : totalBalanceFuture, - builder: (fbContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - if (_showAvailable) { - _balanceCached = snapshot.data!; - } else { - _balanceTotalCached = snapshot.data!; - } - } - Decimal? balanceToShow = - _showAvailable ? _balanceCached : _balanceTotalCached; - - if (balanceToShow != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // GestureDetector( - // onTap: showSheet, - // child: Row( - // children: [ - // if (coin == Coin.firo || - // coin == Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Private" : "Public"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // if (coin != Coin.firo && - // coin != Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Available" : "Full"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // const SizedBox( - // width: 4, - // ), - // SvgPicture.asset( - // Assets.svg.chevronDown, - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // width: 8, - // height: 4, - // ), - // ], - // ), - // ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "${Format.localizedStringAsFixed( - value: balanceToShow, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.desktopH3(context), - ), - ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ); + totalBalanceFuture = firoWallet.availablePublicBalance(); + availableBalanceFuture = + firoWallet.availablePrivateBalance(); } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // GestureDetector( - // onTap: showSheet, - // child: Row( - // children: [ - // if (coin == Coin.firo || - // coin == Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Private" : "Public"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // if (coin != Coin.firo && - // coin != Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Available" : "Full"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // const SizedBox( - // width: 4, - // ), - // SvgPicture.asset( - // Assets.svg.chevronDown, - // width: 8, - // height: 4, - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ], - // ), - // ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopH3(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (externalCalls) - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ); + totalBalanceFuture = ref.watch(managerProvider + .select((value) => value.totalBalance)); + + availableBalanceFuture = ref.watch(managerProvider + .select((value) => value.availableBalance)); } + + final locale = ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)); + + final baseCurrency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + final priceTuple = ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = ref + .watch(walletBalanceToggleStateProvider.state) + .state == + WalletBalanceToggleState.available; + + return FutureBuilder( + future: _showAvailable + ? availableBalanceFuture + : totalBalanceFuture, + builder: (fbContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + if (_showAvailable) { + _balanceCached = snapshot.data!; + } else { + _balanceTotalCached = snapshot.data!; + } + } + Decimal? balanceToShow = _showAvailable + ? _balanceCached + : _balanceTotalCached; + + if (balanceToShow != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.desktopH3(context), + ), + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopH3(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (externalCalls) + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } + }, + ); }, - ); - }, + ), + ], ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const SizedBox( + width: 8, + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const DesktopBalanceToggleButton(), + const SizedBox( + width: 8, + ), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ) ], - ), - const SizedBox( - width: 8, - ), - WalletRefreshButton( - walletId: walletId, - initialSyncStatus: widget.initialSyncStatus, - ) - ], + ); + }, ); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 6fbe61005..149d46b3c 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -231,6 +231,9 @@ class _PNG { String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; + String get glasses => "assets/images/glasses.png"; + String get glassesHidden => "assets/images/glasses-hidden.png"; + String imageFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: diff --git a/pubspec.yaml b/pubspec.yaml index e8f417586..af4370d99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -202,6 +202,8 @@ flutter: - assets/images/epic-cash.png - assets/images/bitcoincash.png - assets/images/namecoin.png + - assets/images/glasses.png + - assets/images/glasses-hidden.png - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg From 345ed958e080999ac2696ca24850d0d5944bbc39 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 07:44:45 -0600 Subject: [PATCH 062/100] initial window size linux --- lib/main.dart | 2 +- linux/my_application.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6879b69c5..728152951 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1220, 1000)); + setWindowMinSize(const Size(1220, 900)); setWindowMaxSize(Size.infinite); } diff --git a/linux/my_application.cc b/linux/my_application.cc index 280895e03..9cb3acebd 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -47,7 +47,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "Stack Wallet"); } - gtk_window_set_default_size(window, 720, 1280); + gtk_window_set_default_size(window, 1220, 900); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); From b22b4195d6bf3cd4f7bec097a58c0cef7fb737d9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:15:13 -0600 Subject: [PATCH 063/100] desktop exchange steps scaffolding --- lib/pages/exchange_view/exchange_form.dart | 23 ++-- .../exchange_steps/step_scaffold.dart | 28 +++-- .../subwidgets/desktop_step_1.dart | 104 ++++++++++++++++++ .../subwidgets/desktop_step_2.dart | 66 +++++++++++ .../subwidgets/desktop_step_3.dart | 91 +++++++++++++++ .../subwidgets/desktop_step_4.dart | 98 +++++++++++++++++ .../subwidgets/step_one_item.dart | 38 +++++++ .../desktop_exchange_steps_indicator.dart | 32 +++--- 8 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 2d89c7660..148c74920 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -19,12 +19,13 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_op import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -913,10 +914,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await showDialog<void>( context: context, builder: (context) { - return const DesktopDialog( - maxWidth: 700, + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, child: StepScaffold( - step: 1, + step: 2, + body: DesktopStep2( + model: model, + ), ), ); }, @@ -936,10 +941,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await showDialog<void>( context: context, builder: (context) { - return const DesktopDialog( - maxWidth: 700, + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, child: StepScaffold( - step: 0, + step: 1, + body: DesktopStep1( + model: model, + ), ), ); }, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 09aea9dbf..62a293c27 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -4,8 +4,13 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; class StepScaffold extends StatefulWidget { - const StepScaffold({Key? key, required this.step}) : super(key: key); + const StepScaffold({ + Key? key, + required this.body, + required this.step, + }) : super(key: key); + final Widget body; final int step; @override @@ -24,11 +29,13 @@ class _StepScaffoldState extends State<StepScaffold> { @override Widget build(BuildContext context) { return Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ Row( children: [ const AppBarBackButton( isCompact: true, + iconSize: 23, ), Text( "Exchange XXX to XXX", @@ -37,17 +44,24 @@ class _StepScaffoldState extends State<StepScaffold> { ], ), const SizedBox( - height: 32, + height: 12, ), - DesktopExchangeStepsIndicator( - currentStep: currentStep, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: DesktopExchangeStepsIndicator( + currentStep: currentStep, + ), ), const SizedBox( height: 32, ), - Container( - height: 200, - color: Colors.red, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: widget.body, ), ], ); diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart new file mode 100644 index 000000000..7334cae05 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep1 extends StatelessWidget { + const DesktopStep1({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart new file mode 100644 index 000000000..c9072cb76 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopStep2 extends StatelessWidget { + const DesktopStep2({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Enter exchange details", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + // + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart new file mode 100644 index 000000000..1e2743ef5 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep3 extends StatelessWidget { + const DesktopStep3({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm exchange details", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Confirm", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart new file mode 100644 index 000000000..8604e7c23 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep4 extends StatelessWidget { + const DesktopStep4({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Send from Stack Wallet", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Show QR code", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart new file mode 100644 index 000000000..001383a17 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class StepOneItem extends StatelessWidget { + const StepOneItem({ + Key? key, + required this.label, + required this.value, + this.padding = const EdgeInsets.all(16), + }) : super(key: key); + + final String label; + final String value; + final EdgeInsets padding; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart index 44831bb4b..ddcd2e6c4 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart @@ -22,9 +22,9 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { } } - static const double verticalSpacing = 4; + static const double verticalSpacing = 6; static const double horizontalSpacing = 16; - static const double barHeight = 6; + static const double barHeight = 4; @override Widget build(BuildContext context) { @@ -35,16 +35,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Confirm amount", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 0), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 1), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 0), + color: getColor(context, 1), height: barHeight, + width: double.infinity, ), ], ), @@ -57,16 +58,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Enter details", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 1), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 2), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 1), + color: getColor(context, 2), height: barHeight, + width: double.infinity, ), ], ), @@ -79,16 +81,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Confirm details", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 2), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 3), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 2), + color: getColor(context, 3), height: barHeight, + width: double.infinity, ), ], ), @@ -101,16 +104,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Complete exchange", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 3), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 4), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 3), + color: getColor(context, 4), height: barHeight, + width: double.infinity, ), ], ), From 11845b8b05b712cc6a9a931f44c5fb0ab7577de7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:23:11 -0600 Subject: [PATCH 064/100] populate desktop step one trade info --- .../subwidgets/desktop_step_1.dart | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 7334cae05..1f892dd52 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class DesktopStep1 extends StatelessWidget { +class DesktopStep1 extends ConsumerWidget { const DesktopStep1({ Key? key, required this.model, @@ -16,7 +19,7 @@ class DesktopStep1 extends StatelessWidget { final IncompleteExchangeModel model; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Column( children: [ Text( @@ -38,33 +41,37 @@ class DesktopStep1 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + StepOneItem( label: "Exchange", - value: "lol", + value: ref.watch(currentExchangeNameStateProvider.state).state, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + StepOneItem( label: "You send", - value: "lol", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + StepOneItem( label: "You receive", - value: "lol", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( - label: "Rate", - value: "lol", + StepOneItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, ), ], ), From 2654d50e407b74916ad5ef95b4f27327518247ca Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:41:16 -0600 Subject: [PATCH 065/100] populate desktop step two trade info --- .../subwidgets/desktop_step_2.dart | 423 +++++++++++++++++- 1 file changed, 421 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index c9072cb76..e1c5a5620 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -1,16 +1,199 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; +import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; +import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; +import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class DesktopStep2 extends StatelessWidget { +class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, required this.model, + this.clipboard = const ClipboardWrapper(), }) : super(key: key); final IncompleteExchangeModel model; + final ClipboardInterface clipboard; + + @override + ConsumerState<DesktopStep2> createState() => _DesktopStep2State(); +} + +class _DesktopStep2State extends ConsumerState<DesktopStep2> { + late final IncompleteExchangeModel model; + late final ClipboardInterface clipboard; + + late final TextEditingController _toController; + late final TextEditingController _refundController; + + late final FocusNode _toFocusNode; + late final FocusNode _refundFocusNode; + + bool isStackCoin(String ticker) { + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } + } + + void selectRecipientAddressFromStack() { + try { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(value); + + _toController.text = manager.walletName; + model.recipientAddress = await manager.currentReceivingAddress; + } + }); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + } + + void selectRefundAddressFromStack() { + try { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(value); + + _refundController.text = manager.walletName; + model.refundAddress = await manager.currentReceivingAddress; + } + }); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + } + + void selectRecipientFromAddressBook() { + ref.read(exchangeFlowIsActiveStateProvider.state).state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { + ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + + final address = + ref.read(exchangeFromAddressBookAddressStateProvider.state).state; + if (address.isNotEmpty) { + _toController.text = address; + model.recipientAddress = _toController.text; + ref.read(exchangeFromAddressBookAddressStateProvider.state).state = ""; + } + }); + } + + void selectRefundFromAddressBook() { + ref.read(exchangeFlowIsActiveStateProvider.state).state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then( + (_) { + ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + final address = + ref.read(exchangeFromAddressBookAddressStateProvider.state).state; + if (address.isNotEmpty) { + _refundController.text = address; + model.refundAddress = _refundController.text; + } + }, + ); + } + + @override + void initState() { + model = widget.model; + clipboard = widget.clipboard; + + _toController = TextEditingController(); + _refundController = TextEditingController(); + + _toFocusNode = FocusNode(); + _refundFocusNode = FocusNode(); + + final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; + if (tuple != null) { + if (model.receiveTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _toController.text = value; + model.recipientAddress = _toController.text; + }); + } else { + if (model.sendTicker.toUpperCase() == + tuple.item2.ticker.toUpperCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _refundController.text = value; + model.refundAddress = _refundController.text; + }); + } + } + } + + super.initState(); + } + + @override + void dispose() { + _toController.dispose(); + _refundController.dispose(); + + _toFocusNode.dispose(); + _refundFocusNode.dispose(); + + super.dispose(); + } @override Widget build(BuildContext context) { @@ -30,7 +213,243 @@ class DesktopStep2 extends StatelessWidget { const SizedBox( height: 20, ), - // + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient Wallet", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.receiveTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRecipientAddressFromStack, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + onTap: () {}, + key: const Key("recipientExchangeStep2ViewAddressFieldKey"), + controller: _toController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _toFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _toController.text = ""; + model.recipientAddress = _toController.text; + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; + model.recipientAddress = _toController.text; + setState(() {}); + } + }, + child: _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRecipientFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.sendTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRefundAddressFromStack, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _refundController.text = ""; + model.refundAddress = _refundController.text; + + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; + model.refundAddress = _refundController.text; + + setState(() {}); + } + }, + child: _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRefundFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), Padding( padding: const EdgeInsets.only( top: 20, From 648c896b9e9a8a38e00faf63762c1bba5c37280d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:49:27 -0600 Subject: [PATCH 066/100] refactor desktop step item --- .../subwidgets/desktop_step_1.dart | 10 ++-- .../subwidgets/desktop_step_3.dart | 10 ++-- .../subwidgets/desktop_step_4.dart | 10 ++-- .../subwidgets/desktop_step_item.dart | 59 +++++++++++++++++++ .../subwidgets/step_one_item.dart | 38 ------------ 5 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart delete mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 1f892dd52..942747ea2 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -41,7 +41,7 @@ class DesktopStep1 extends ConsumerWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - StepOneItem( + DesktopStepItem( label: "Exchange", value: ref.watch(currentExchangeNameStateProvider.state).state, ), @@ -49,7 +49,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: "You send", value: "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", @@ -58,7 +58,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: "You receive", value: "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", @@ -67,7 +67,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: model.rateType == ExchangeRateType.estimated ? "Estimated rate" : "Fixed rate", diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 1e2743ef5..655c3518e 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -25,7 +25,7 @@ class DesktopStep3 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + const DesktopStepItem( label: "Exchange", value: "lol", ), @@ -33,7 +33,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You send", value: "lol", ), @@ -41,7 +41,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You receive", value: "lol", ), @@ -49,7 +49,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "Rate", value: "lol", ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 8604e7c23..3b3853efb 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -32,7 +32,7 @@ class DesktopStep4 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + const DesktopStepItem( label: "Exchange", value: "lol", ), @@ -40,7 +40,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You send", value: "lol", ), @@ -48,7 +48,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You receive", value: "lol", ), @@ -56,7 +56,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "Rate", value: "lol", ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart new file mode 100644 index 000000000..7c777c2dd --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class DesktopStepItem extends StatelessWidget { + const DesktopStepItem( + {Key? key, + required this.label, + required this.value, + this.padding = const EdgeInsets.all(16), + this.vertical = false}) + : super(key: key); + + final String label; + final String value; + final EdgeInsets padding; + final bool vertical; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: ConditionalParent( + condition: vertical, + builder: (child) => Column( + children: [ + child, + const SizedBox( + height: 2, + ), + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + if (!vertical) + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart deleted file mode 100644 index 001383a17..000000000 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; - -class StepOneItem extends StatelessWidget { - const StepOneItem({ - Key? key, - required this.label, - required this.value, - this.padding = const EdgeInsets.all(16), - }) : super(key: key); - - final String label; - final String value; - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - ), - ], - ), - ); - } -} From c9e2c4abb7c3fe41f9ea4c7b8152d5e00f597b76 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 10:14:27 -0600 Subject: [PATCH 067/100] desktop trade steps 3 and 4 mostly laid out --- .../exchange_step_views/step_4_view.dart | 1 - .../subwidgets/desktop_step_3.dart | 170 ++++++++++++++++-- .../subwidgets/desktop_step_4.dart | 155 ++++++++++++++-- 3 files changed, 295 insertions(+), 31 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 0921f68e0..a8b403dcf 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -18,7 +18,6 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 655c3518e..65b6ed2b3 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -1,13 +1,135 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart'; +import 'package:stackwallet/providers/exchange/exchange_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -class DesktopStep3 extends StatelessWidget { - const DesktopStep3({Key? key}) : super(key: key); +class DesktopStep3 extends ConsumerStatefulWidget { + const DesktopStep3({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep3> createState() => _DesktopStep3State(); +} + +class _DesktopStep3State extends ConsumerState<DesktopStep3> { + late final IncompleteExchangeModel model; + + Future<void> createTrade() async { + unawaited( + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), + ), + ), + ), + ); + + final ExchangeResponse<Trade> response = + await ref.read(exchangeProvider).createTrade( + from: model.sendTicker, + to: model.receiveTicker, + fixedRate: model.rateType != ExchangeRateType.estimated, + amount: model.reversed ? model.receiveAmount : model.sendAmount, + addressTo: model.recipientAddress!, + extraId: null, + addressRefund: model.refundAddress!, + refundExtraId: "", + rateId: model.rateId, + reversed: model.reversed, + ); + + if (response.value == null) { + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Failed to create trade", + message: response.exception?.toString(), + ), + )); + return; + } + + // save trade to hive + await ref.read(tradesServiceProvider).add( + trade: response.value!, + shouldNotifyListeners: true, + ); + + String status = response.value!.status; + + model.trade = response.value!; + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", + )); + + if (mounted) { + unawaited(Navigator.of(context).pushNamed( + Step4View.routeName, + arguments: model, + )); + } + } + + @override + void initState() { + model = widget.model; + super.initState(); + } @override Widget build(BuildContext context) { @@ -25,33 +147,55 @@ class DesktopStep3 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const DesktopStepItem( + DesktopStepItem( label: "Exchange", - value: "lol", + value: ref.watch(currentExchangeNameStateProvider.state).state, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( + DesktopStepItem( label: "You send", - value: "lol", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( + DesktopStepItem( label: "You receive", - value: "lol", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "Rate", - value: "lol", + DesktopStepItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Recipient ${model.receiveTicker.toUpperCase()} address", + value: model.recipientAddress!, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Refund ${model.sendTicker.toUpperCase()} address", + value: model.refundAddress!, ), ], ), @@ -77,9 +221,7 @@ class DesktopStep3 extends StatelessWidget { child: PrimaryButton( label: "Confirm", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo - }, + onPressed: createTrade, ), ), ], diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 3b3853efb..ba9838086 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,64 +1,187 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class DesktopStep4 extends StatelessWidget { - const DesktopStep4({Key? key}) : super(key: key); +class DesktopStep4 extends ConsumerStatefulWidget { + const DesktopStep4({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep4> createState() => _DesktopStep4State(); +} + +class _DesktopStep4State extends ConsumerState<DesktopStep4> { + late final IncompleteExchangeModel model; + + String _statusString = "New"; + + Timer? _statusTimer; + + bool _isWalletCoinAndHasWallet(String ticker) { + try { + final coin = coinFromTickerCaseInsensitive(ticker); + return ref + .read(walletsChangeNotifierProvider) + .managers + .where((element) => element.coin == coin) + .isNotEmpty; + } catch (_) { + return false; + } + } + + Future<void> _updateStatus() async { + final statusResponse = + await ref.read(exchangeProvider).updateTrade(model.trade!); + String status = "Waiting"; + if (statusResponse.value != null) { + status = statusResponse.value!.status; + } + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + setState(() { + _statusString = status; + }); + } + } + + @override + void initState() { + model = widget.model; + + _statusTimer = Timer.periodic(const Duration(seconds: 60), (_) { + _updateStatus(); + }); + + super.initState(); + } + + @override + void dispose() { + _statusTimer?.cancel(); + _statusTimer = null; + super.dispose(); + } @override Widget build(BuildContext context) { return Column( children: [ Text( - "Confirm amount", + "Send ${model.sendTicker.toUpperCase()} to the address below", style: STextStyles.desktopTextMedium(context), ), const SizedBox( height: 8, ), Text( - "Network fees and other exchange charges are included in the rate.", + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", style: STextStyles.desktopTextExtraExtraSmall(context), ), const SizedBox( height: 20, ), + RoundedContainer( + color: Theme.of(context).extension<StackColors>()!.warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + children: [ + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), RoundedWhiteContainer( borderColor: Theme.of(context).extension<StackColors>()!.background, padding: const EdgeInsets.all(0), child: Column( children: [ - const DesktopStepItem( - label: "Exchange", - value: "lol", + DesktopStepItem( + vertical: true, + label: "Send ${model.sendTicker.toUpperCase()} to this address", + value: model.trade!.payInAddress, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "You send", - value: "lol", + DesktopStepItem( + label: "Amount", + value: + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "You receive", - value: "lol", + DesktopStepItem( + label: "Trade ID", + value: model.trade!.tradeId, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "Rate", - value: "lol", + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + Text( + _statusString, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .colorForStatus(_statusString), + ), + ), + ], + ), ), ], ), From 78186358b9de2fc28b989575e1d6bee1c62d1f38 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 10:50:53 -0700 Subject: [PATCH 068/100] WIP: wallet will be deleted dialog --- .../sub_widgets/delete_wallet_keys_popup.dart | 195 ++++++++++++++++++ .../desktop_attention_delete_wallet.dart | 122 +++++++++++ .../desktop_delete_wallet_dialog.dart | 90 +------- lib/route_generator.dart | 47 +++++ 4 files changed, 369 insertions(+), 85 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart new file mode 100644 index 000000000..5f46e0f2f --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -0,0 +1,195 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DeleteWalletKeysPopup extends ConsumerStatefulWidget { + const DeleteWalletKeysPopup({ + Key? key, + required this.walletId, + required this.words, + }) : super(key: key); + + final String walletId; + final List<String> words; + + static const String routeName = "/desktopDeleteWalletKeysPopup"; + + @override + ConsumerState<DeleteWalletKeysPopup> createState() => + _DeleteWalletKeysPopup(); +} + +class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: widget.words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () async { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + + unawaited( + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context) + .popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Column( + children: [ + Text( + "Thanks! " + "\n\nYour wallet will be deleted.", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context) + .popUntil( + (_) => count++ >= 2); + }), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: () async { + // final walletsInstance = + // ref.read(walletsChangeNotifierProvider); + // await ref + // .read(walletsServiceChangeNotifierProvider) + // .deleteWallet(walletId, true); + // + // if (mounted) { + // Navigator.of(context).popUntil( + // ModalRoute.withName(HomeView.routeName)); + // } + + // // wait for widget tree to dispose of any widgets watching the manager + // await Future<void>.delayed(const Duration(seconds: 1)); + // walletsInstance.removeWallet(walletId: walletId); + }), + ], + ) + ], + ), + ], + ), + ); + }), + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart new file mode 100644 index 000000000..30546f60b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:tuple/tuple.dart'; + +import 'delete_wallet_keys_popup.dart'; + +class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { + const DesktopAttentionDeleteWallet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopAttentionDeleteWallet"; + + @override + ConsumerState<DesktopAttentionDeleteWallet> createState() => + _DesktopAttentionDeleteWallet(); +} + +class _DesktopAttentionDeleteWallet + extends ConsumerState<DesktopAttentionDeleteWallet> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 610, + maxHeight: 530, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () async { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + await Navigator.of(context) + .pushNamed(DeleteWalletKeysPopup.routeName, + arguments: Tuple2( + widget.walletId, + words, + )); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index e2ab4fa86..087629673 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -12,7 +13,6 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; -import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; @@ -41,89 +41,6 @@ class _DesktopDeleteWalletDialog bool hidePassword = true; bool _continueEnabled = false; - Future<void> attentionDelete() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) => DesktopDialog( - maxWidth: 610, - maxHeight: 530, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - DesktopDialogCloseButton( - onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - }, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackError, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " - "the only way you can have access to your funds is by using your backup key." - "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." - "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "View Backup Key", - onPressed: () {}, - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } - @override void initState() { passwordController = TextEditingController(); @@ -273,7 +190,10 @@ class _DesktopDeleteWalletDialog if (mounted) { Navigator.of(context).pop(); - attentionDelete(); + await Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ); } } else { unawaited( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 77b5e7d11..cbc4cb343 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -92,6 +92,8 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; @@ -1193,6 +1195,51 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopAttentionDeleteWallet.routeName: + if (args is String) { + return FadePageRoute( + DesktopAttentionDeleteWallet( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DeleteWalletKeysPopup.routeName: + if (args is Tuple2<String, List<String>>) { + return FadePageRoute( + DeleteWalletKeysPopup( + walletId: args.item1, + words: args.item2, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( From 5c7cb8a3c5d033dab7f0614ce090f5d783d53b77 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 12:18:35 -0700 Subject: [PATCH 069/100] WIP: unmounted widget --- .../sub_widgets/delete_wallet_keys_popup.dart | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index 5f46e0f2f..f70c2eadf 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/global/wallets_service_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -28,6 +31,15 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { } class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { + late final String _walletId; + + @override + void initState() { + _walletId = widget.walletId; + + super.initState(); + } + @override Widget build(BuildContext context) { return DesktopDialog( @@ -113,6 +125,7 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { context: context, builder: (context) { return DesktopDialog( + maxHeight: 350, child: Column( children: [ Row( @@ -128,13 +141,16 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ], ), Column( + crossAxisAlignment: + CrossAxisAlignment.center, children: [ Text( "Thanks! " "\n\nYour wallet will be deleted.", style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, ), - const SizedBox(height: 20), + const SizedBox(height: 50), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -155,20 +171,43 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { buttonHeight: ButtonHeight.xl, label: "Continue", onPressed: () async { - // final walletsInstance = - // ref.read(walletsChangeNotifierProvider); - // await ref - // .read(walletsServiceChangeNotifierProvider) - // .deleteWallet(walletId, true); - // - // if (mounted) { - // Navigator.of(context).popUntil( - // ModalRoute.withName(HomeView.routeName)); - // } + // int count = 0; + // Navigator.of(context) + // .popUntil( + // (_) => count++ >= 2); - // // wait for widget tree to dispose of any widgets watching the manager - // await Future<void>.delayed(const Duration(seconds: 1)); - // walletsInstance.removeWallet(walletId: walletId); + final walletsInstance = ref.read( + walletsChangeNotifierProvider); + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(_walletId); + + final _managerWalletId = + manager.walletId; + + await ref + .read( + walletsServiceChangeNotifierProvider) + .deleteWallet( + manager.walletName, + true); + + if (mounted) { + Navigator.of(context) + .popUntil( + ModalRoute.withName( + MyStackView + .routeName)); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed( + const Duration( + seconds: 1)); + walletsInstance.removeWallet( + walletId: + _managerWalletId); }), ], ) From d06c4862b1685b19f48686b8c3c27a7542fedd16 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 11:21:44 -0600 Subject: [PATCH 070/100] desktop exchange coin selection ui --- .../fixed_rate_pair_coin_selection_view.dart | 319 +++++++++--------- ...floating_rate_currency_selection_view.dart | 317 ++++++++--------- lib/pages/exchange_view/exchange_form.dart | 143 +++++++- 3 files changed, 459 insertions(+), 320 deletions(-) diff --git a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart index 80bdcda62..779d99306 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart @@ -8,6 +8,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -16,8 +18,6 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - class FixedRateMarketPairCoinSelectionView extends ConsumerStatefulWidget { const FixedRateMarketPairCoinSelectionView({ Key? key, @@ -120,95 +120,106 @@ class _FixedRateMarketPairCoinSelectionViewState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _markets .where((e) => Coin.values .where((coin) => @@ -221,6 +232,7 @@ class _FixedRateMarketPairCoinSelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { final String ticker = @@ -282,84 +294,85 @@ class _FixedRateMarketPairCoinSelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _markets.length, - itemBuilder: (builderContext, index) { - final String ticker = - isFrom ? _markets[index].from : _markets[index].to; + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _markets.length, + itemBuilder: (builderContext, index) { + final String ticker = + isFrom ? _markets[index].from : _markets[index].to; - final tuple = _imageUrlAndNameFor(ticker); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(ticker); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + final tuple = _imageUrlAndNameFor(ticker); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(ticker); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + tuple.item1, width: 24, height: 24, - child: SvgPicture.network( - tuple.item1, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tuple.item2, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tuple.item2, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart index e1c1addd2..eb7a99299 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -13,8 +15,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class FloatingRateCurrencySelectionView extends StatefulWidget { const FloatingRateCurrencySelectionView({ Key? key, @@ -76,96 +76,109 @@ class _FloatingRateCurrencySelectionViewState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - filter(""); - }, - ), - ], - ), - ), - ) - : null, ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _currencies .where((e) => Coin.values .where((coin) => @@ -177,6 +190,7 @@ class _FloatingRateCurrencySelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { return Padding( @@ -234,80 +248,81 @@ class _FloatingRateCurrencySelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _currencies.length, - itemBuilder: (builderContext, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(_currencies[index]); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _currencies.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(_currencies[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + _currencies[index].image, width: 24, height: 24, - child: SvgPicture.network( - _currencies[index].image, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _currencies[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + _currencies[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _currencies[index].name, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - _currencies[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 148c74920..cdc6f16b9 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -410,13 +411,65 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }).toList(growable: false); - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FloatingRateCurrencySelectionView( - currencies: tickers, - ), - ), - ); + final result = isDesktop + ? await showDialog<Currency?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ); if (mounted && result is Currency) { onSelected(result); @@ -490,15 +543,73 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .toList(growable: false); } - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FixedRateMarketPairCoinSelectionView( - markets: marketsThatPairWithExcludedTicker, - currencies: ref.read(availableChangeNowCurrenciesProvider).currencies, - isFrom: excludedTicker != fromTicker, - ), - ), - ); + final result = isDesktop + ? await showDialog<String?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: ref + .read( + availableChangeNowCurrenciesProvider) + .currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: + ref.read(availableChangeNowCurrenciesProvider).currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ); if (mounted && result is String) { onSelected(result); From 3e9039ac90b724cb8886e17e6907c677fcdc36d2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 11:26:34 -0600 Subject: [PATCH 071/100] show to and from tickers in exchange steps flow --- lib/pages/exchange_view/exchange_form.dart | 2 ++ .../desktop_exchange/exchange_steps/step_scaffold.dart | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index cdc6f16b9..7faa83a35 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -1030,6 +1030,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { maxHeight: double.infinity, child: StepScaffold( step: 2, + model: model, body: DesktopStep2( model: model, ), @@ -1057,6 +1058,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { maxHeight: double.infinity, child: StepScaffold( step: 1, + model: model, body: DesktopStep1( model: model, ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 62a293c27..8dbaf9580 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -8,10 +9,12 @@ class StepScaffold extends StatefulWidget { Key? key, required this.body, required this.step, + required this.model, }) : super(key: key); final Widget body; final int step; + final IncompleteExchangeModel model; @override State<StepScaffold> createState() => _StepScaffoldState(); @@ -19,10 +22,12 @@ class StepScaffold extends StatefulWidget { class _StepScaffoldState extends State<StepScaffold> { int currentStep = 0; + late final IncompleteExchangeModel model; @override void initState() { currentStep = widget.step; + model = widget.model; super.initState(); } @@ -38,7 +43,7 @@ class _StepScaffoldState extends State<StepScaffold> { iconSize: 23, ), Text( - "Exchange XXX to XXX", + "Exchange ${model.sendTicker.toUpperCase()} to ${model.receiveTicker.toUpperCase()}", style: STextStyles.desktopH3(context), ), ], From 3fca3d8b1e9d7ce15a5610c60342f94363e93cf5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 12:03:15 -0600 Subject: [PATCH 072/100] desktop exchange flow styling and choose addresses from addressbook functionality --- .../subwidgets/desktop_step_1.dart | 23 ++- .../subwidgets/desktop_step_2.dart | 139 +++++++++++++----- 2 files changed, 121 insertions(+), 41 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 942747ea2..a97b722ef 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -97,8 +100,24 @@ class DesktopStep1 extends ConsumerWidget { child: PrimaryButton( label: "Next", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 2, + model: model, + body: DesktopStep2( + model: model, + ), + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index e1c5a5620..38c01822e 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; -import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; -import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; -import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; @@ -24,6 +22,10 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import '../../../../models/contact_address_entry.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; + class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, @@ -105,42 +107,96 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { } } - void selectRecipientFromAddressBook() { - ref.read(exchangeFlowIsActiveStateProvider.state).state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { - ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + void selectRecipientFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); - final address = - ref.read(exchangeFromAddressBookAddressStateProvider.state).state; - if (address.isNotEmpty) { - _toController.text = address; - model.recipientAddress = _toController.text; - ref.read(exchangeFromAddressBookAddressStateProvider.state).state = ""; - } - }); + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _toController.text = entry.address; + model.recipientAddress = entry.address; + setState(() {}); + } } - void selectRefundFromAddressBook() { - ref.read(exchangeFlowIsActiveStateProvider.state).state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then( - (_) { - ref.read(exchangeFlowIsActiveStateProvider.state).state = false; - final address = - ref.read(exchangeFromAddressBookAddressStateProvider.state).state; - if (address.isNotEmpty) { - _refundController.text = address; - model.refundAddress = _refundController.text; - } - }, + void selectRefundFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, ); + + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _refundController.text = entry.address; + model.refundAddress = entry.address; + setState(() {}); + } } @override @@ -198,10 +254,12 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( "Enter exchange details", style: STextStyles.desktopTextMedium(context), + textAlign: TextAlign.center, ), const SizedBox( height: 8, @@ -209,6 +267,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { Text( "Enter your recipient and refund addresses", style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, ), const SizedBox( height: 20, @@ -231,7 +290,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ], ), const SizedBox( - height: 4, + height: 10, ), ClipRRect( borderRadius: BorderRadius.circular( @@ -321,9 +380,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), const SizedBox( - height: 6, + height: 10, ), RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, child: Text( "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -350,7 +410,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ], ), const SizedBox( - height: 4, + height: 10, ), ClipRRect( borderRadius: BorderRadius.circular( @@ -380,6 +440,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { "Enter ${model.sendTicker.toUpperCase()} refund address", _refundFocusNode, context, + desktopMed: true, ).copyWith( contentPadding: const EdgeInsets.only( left: 16, @@ -441,7 +502,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), const SizedBox( - height: 6, + height: 10, ), RoundedWhiteContainer( borderColor: Theme.of(context).extension<StackColors>()!.background, From 7e8f0db96726f509bfb9f20ef18de7b1a5021ed8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 12:07:15 -0600 Subject: [PATCH 073/100] long address layout fix --- .../sub_widgets/contact_list_item.dart | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index 7acfaae9e..d7bfefb1f 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -87,35 +87,47 @@ class _ContactListItemState extends ConsumerState<ContactListItem> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - WalletInfoCoinIcon(coin: e.coin), - const SizedBox( - width: 12, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), + Flexible( + child: Row( + children: [ + WalletInfoCoinIcon(coin: e.coin), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + Row( + children: [ + Flexible( + child: Text( + e.address, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ), + ], + ), + ], ), - Text( - e.address, - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], - ), - ], + ), + ], + ), ), BlueTextButton( text: "Select wallet", From 04b982fb250156d3d96e3b44e955a2bd890dfd02 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 13:21:52 -0600 Subject: [PATCH 074/100] desktop exchange choose from stack address ui --- .../subwidgets/desktop_step_2.dart | 50 ++- .../subwidgets/desktop_choose_from_stack.dart | 329 ++++++++++++++++++ 2 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 38c01822e..270b98fc7 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; -import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -64,12 +64,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final coin = coinFromTickerCaseInsensitive( model.receiveTicker, ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { + + showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ).then((value) async { if (value is String) { final manager = ref.read(walletsChangeNotifierProvider).getManager(value); @@ -88,12 +97,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final coin = coinFromTickerCaseInsensitive( model.sendTicker, ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { + + showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ).then((value) async { if (value is String) { final manager = ref.read(walletsChangeNotifierProvider).getManager(value); @@ -366,7 +384,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ? const ClipboardIcon() : const XIcon(), ), - if (_toController.text.isEmpty) + if (_toController.text.isEmpty && + isStackCoin(model.receiveTicker)) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: selectRecipientFromAddressBook, @@ -488,7 +507,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ? const ClipboardIcon() : const XIcon(), ), - if (_refundController.text.isEmpty) + if (_refundController.text.isEmpty && + isStackCoin(model.sendTicker)) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: selectRefundFromAddressBook, diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart new file mode 100644 index 000000000..a3fb91f61 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -0,0 +1,329 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class DesktopChooseFromStack extends ConsumerStatefulWidget { + const DesktopChooseFromStack({ + Key? key, + required this.coin, + }) : super(key: key); + + final Coin coin; + + @override + ConsumerState<DesktopChooseFromStack> createState() => + _DesktopChooseFromStackState(); +} + +class _DesktopChooseFromStackState + extends ConsumerState<DesktopChooseFromStack> { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchTerm = ""; + + List<String> filter(List<String> walletIds, String searchTerm) { + if (searchTerm.isEmpty) { + return walletIds; + } + + final List<String> result = []; + for (final walletId in walletIds) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (manager.walletName.toLowerCase().contains(searchTerm.toLowerCase())) { + result.add(walletId); + } + } + + return result; + } + + @override + void initState() { + searchFieldFocusNode = FocusNode(); + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose from Stack", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 28, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Flexible( + child: Builder( + builder: (context) { + List<String> walletIds = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getWalletIdsFor(coin: widget.coin), + ), + ); + + if (walletIds.isEmpty) { + return Column( + children: [ + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Center( + child: Text( + "No ${widget.coin.ticker.toUpperCase()} wallets", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), + ], + ); + } + + walletIds = filter(walletIds, _searchTerm); + + return ListView.separated( + primary: false, + itemCount: walletIds.length, + separatorBuilder: (_, __) => const SizedBox( + height: 5, + ), + itemBuilder: (context, index) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletIds[index]))); + + return RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: widget.coin), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + const Spacer(), + BalanceDisplay( + walletId: walletIds[index], + ), + const SizedBox( + width: 80, + ), + BlueTextButton( + text: "Select wallet", + onTap: () { + Navigator.of(context).pop(manager.walletId); + }, + ), + ], + ), + ); + }, + ); + }, + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ); + } +} + +class BalanceDisplay extends ConsumerStatefulWidget { + const BalanceDisplay({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<BalanceDisplay> createState() => _BalanceDisplayState(); +} + +class _BalanceDisplayState extends ConsumerState<BalanceDisplay> { + late final String walletId; + + Decimal? _cachedBalance; + + static const loopedText = [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ]; + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + return FutureBuilder( + future: manager.availableBalance, + builder: (context, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + _cachedBalance = snapshot.data; + } + + if (_cachedBalance == null) { + return AnimatedText( + stringsToLoopThrough: loopedText, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + ); + } else { + return Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance!, + locale: locale, + decimalPlaces: 8, + )} ${manager.coin.ticker}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + textAlign: TextAlign.right, + ); + } + }, + ); + } +} From f75e4ea2faf8e71c385aa05732ace062ee90a324 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 14:52:41 -0600 Subject: [PATCH 075/100] desktop delete routing fixes --- .../sub_widgets/delete_wallet_button.dart | 113 ++++++++-- .../sub_widgets/delete_wallet_keys_popup.dart | 204 +++++++++--------- .../desktop_attention_delete_wallet.dart | 27 ++- 3 files changed, 207 insertions(+), 137 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index 54f991c37..fd401d613 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'desktop_delete_wallet_dialog.dart'; - class DeleteWalletButton extends ConsumerStatefulWidget { const DeleteWalletButton({ Key? key, @@ -26,8 +26,6 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { @override void initState() { walletId = widget.walletId; - final managerProvider = - ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); super.initState(); } @@ -38,25 +36,45 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(1000), ), - onPressed: () { - showDialog<void>( + onPressed: () async { + final shouldOpenDeleteDialog = await showDialog<bool?>( context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: DesktopDeleteWalletDialog.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: DesktopDeleteWalletDialog.routeName, - arguments: walletId, - ), - ) - ]; - }, - ), + barrierColor: Colors.transparent, + builder: (context) { + return DeletePopupButton( + onTap: () async { + Navigator.of(context).pop(true); + }, + ); + }, ); + + if (shouldOpenDeleteDialog == true) { + final result = await showDialog<bool?>( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: walletId, + ), + ), + ]; + }, + ), + ); + + if (result == true) { + if (mounted) { + Navigator.of(context).pop(); + } + } + } }, child: Padding( padding: const EdgeInsets.symmetric( @@ -79,3 +97,54 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { ); } } + +class DeletePopupButton extends StatefulWidget { + const DeletePopupButton({ + Key? key, + this.onTap, + }) : super(key: key); + + final VoidCallback? onTap; + + @override + State<DeletePopupButton> createState() => _DeletePopupButtonState(); +} + +class _DeletePopupButtonState extends State<DeletePopupButton> { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 24, + left: MediaQuery.of(context).size.width - 234, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: Container( + width: 210, + height: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + color: Colors.red, + boxShadow: [ + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + ], + ), + child: Text( + "Delete", + style: STextStyles.desktopButtonSecondaryEnabled(context), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index f70c2eadf..6b638bb75 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -1,11 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; -import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -61,8 +59,10 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ), DesktopDialogCloseButton( onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), ], @@ -117,106 +117,17 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { child: PrimaryButton( label: "Continue", onPressed: () async { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - - unawaited( - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 350, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - DesktopDialogCloseButton( - onPressedOverride: () { - int count = 0; - Navigator.of(context) - .popUntil((_) => count++ >= 2); - }, - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - "Thanks! " - "\n\nYour wallet will be deleted.", - style: STextStyles.desktopH2(context), - textAlign: TextAlign.center, - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - int count = 0; - Navigator.of(context) - .popUntil( - (_) => count++ >= 2); - }), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Continue", - onPressed: () async { - // int count = 0; - // Navigator.of(context) - // .popUntil( - // (_) => count++ >= 2); - - final walletsInstance = ref.read( - walletsChangeNotifierProvider); - final manager = ref - .read( - walletsChangeNotifierProvider) - .getManager(_walletId); - - final _managerWalletId = - manager.walletId; - - await ref - .read( - walletsServiceChangeNotifierProvider) - .deleteWallet( - manager.walletName, - true); - - if (mounted) { - Navigator.of(context) - .popUntil( - ModalRoute.withName( - MyStackView - .routeName)); - } - - // wait for widget tree to dispose of any widgets watching the manager - await Future<void>.delayed( - const Duration( - seconds: 1)); - walletsInstance.removeWallet( - walletId: - _managerWalletId); - }), - ], - ) - ], - ), - ], - ), - ); - }), + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ConfirmDelete( + walletId: _walletId, + ); + }, + settings: const RouteSettings( + name: "/desktopConfirmDelete", + ), + ), ); }, ), @@ -232,3 +143,86 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ); } } + +class ConfirmDelete extends ConsumerStatefulWidget { + const ConfirmDelete({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<ConfirmDelete> createState() => _ConfirmDeleteState(); +} + +class _ConfirmDeleteState extends ConsumerState<ConfirmDelete> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 350, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Thanks! " + "\n\nYour wallet will be deleted.", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: () async { + final walletsInstance = + ref.read(walletsChangeNotifierProvider); + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId); + + final _managerWalletId = manager.walletId; + // + await ref + .read(walletsServiceChangeNotifierProvider) + .deleteWallet(manager.walletName, true); + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(true); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed(const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: _managerWalletId); + }, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 30546f60b..6eb04502e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -41,8 +41,10 @@ class _DesktopAttentionDeleteWallet children: [ DesktopDialogCloseButton( onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), ], @@ -87,8 +89,10 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "Cancel", onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), const SizedBox(width: 16), @@ -102,12 +106,15 @@ class _DesktopAttentionDeleteWallet .getManager(widget.walletId) .mnemonic; - await Navigator.of(context) - .pushNamed(DeleteWalletKeysPopup.routeName, - arguments: Tuple2( - widget.walletId, - words, - )); + if (mounted) { + await Navigator.of(context).pushNamed( + DeleteWalletKeysPopup.routeName, + arguments: Tuple2( + widget.walletId, + words, + ), + ); + } }, ), ], From 8a6025db4b308756eaf5cf91d50cdbd1e545a801 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 15:29:09 -0600 Subject: [PATCH 076/100] place node url and port on their own line --- .../add_edit_node_view.dart | 195 +++++++++--------- 1 file changed, 93 insertions(+), 102 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 606c4481f..9062314f0 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -494,6 +494,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { condition: isDesktop, builder: (child) => DesktopDialog( maxWidth: 580, + maxHeight: 500, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -830,110 +831,100 @@ class _NodeFormState extends ConsumerState<NodeForm> { const SizedBox( height: 8, ), - Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("addCustomNodeNodeAddressFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_hostController), - controller: _hostController, - focusNode: _hostFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - (widget.coin != Coin.monero && - widget.coin != Coin.wownero && - widget.coin != Coin.epicCash) - ? "IP address" - : "Url", - _hostFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _hostController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("addCustomNodeNodeAddressFieldKey"), + readOnly: widget.readOnly, + enabled: enableField(_hostController), + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) + ? "IP address" + : "Url", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _hostController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - const SizedBox( - width: 12, + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("addCustomNodeNodePortFieldKey"), + readOnly: widget.readOnly, + enabled: enableField(_portController), + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _portController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("addCustomNodeNodePortFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_portController), - controller: _portController, - focusNode: _portFocusNode, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - keyboardType: TextInputType.number, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Port", - _portFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _portController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), - ), - ], + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), ), const SizedBox( height: 8, From 66950ccc5059bbf04187180fb7c60665cd892c41 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 14:40:11 -0700 Subject: [PATCH 077/100] delete popup styled --- .../sub_widgets/delete_wallet_button.dart | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index fd401d613..f2553c2da 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -129,16 +129,30 @@ class _DeletePopupButtonState extends State<DeletePopupButton> { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - color: Colors.red, + color: Theme.of(context).extension<StackColors>()!.popupBG, boxShadow: [ Theme.of(context) .extension<StackColors>()! .standardBoxShadow, ], ), - child: Text( - "Delete", - style: STextStyles.desktopButtonSecondaryEnabled(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 24), + SvgPicture.asset( + Assets.svg.trash, + ), + const SizedBox(width: 14), + Text( + "Delete wallet", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + ), + ], ), ), ), From e099089d04a3154fbb1cd61fc331d0435e6aeba4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 14:58:03 -0700 Subject: [PATCH 078/100] loading indicator and delay --- .../desktop_delete_wallet_dialog.dart | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index 087629673..6729d33b9 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; @@ -177,11 +178,35 @@ class _DesktopDeleteWalletDialog label: "Continue", onPressed: _continueEnabled ? () async { + // add loading indicator + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + final verified = await ref .read(storageCryptoHandlerProvider) .verifyPassphrase(passwordController.text); if (verified) { + Navigator.of(context, rootNavigator: true) + .pop(); + final words = await ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) @@ -190,12 +215,20 @@ class _DesktopDeleteWalletDialog if (mounted) { Navigator.of(context).pop(); - await Navigator.of(context).pushNamed( - DesktopAttentionDeleteWallet.routeName, - arguments: widget.walletId, + unawaited( + Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ), ); } } else { + Navigator.of(context, rootNavigator: true) + .pop(); + + await Future<void>.delayed( + const Duration(milliseconds: 300)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From c935c590c7c5635487503e4587f34d94e8f0ae77 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 16:00:00 -0600 Subject: [PATCH 079/100] desktop exchange flow tweaks and show QR code --- lib/pages/exchange_view/exchange_form.dart | 2 + .../subwidgets/desktop_step_1.dart | 1 + .../subwidgets/desktop_step_2.dart | 30 +++++++-- .../subwidgets/desktop_step_3.dart | 67 ++++++++++++------- .../subwidgets/desktop_step_4.dart | 49 +++++++++++++- .../subwidgets/desktop_step_item.dart | 1 + .../subwidgets/desktop_trade_history.dart | 2 + 7 files changed, 121 insertions(+), 31 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7faa83a35..7f0d9b5d2 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -1024,6 +1024,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (isDesktop) { await showDialog<void>( context: context, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, @@ -1052,6 +1053,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (isDesktop) { await showDialog<void>( context: context, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index a97b722ef..031dc0649 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -104,6 +104,7 @@ class DesktopStep1 extends ConsumerWidget { await showDialog<void>( context: context, barrierColor: Colors.transparent, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 270b98fc7..525d561fc 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; @@ -13,6 +16,8 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; @@ -22,10 +27,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../../models/contact_address_entry.dart'; -import '../../../../widgets/desktop/desktop_dialog.dart'; -import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; - class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, @@ -552,8 +553,25 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { child: PrimaryButton( label: "Next", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 3, + model: model, + body: DesktopStep3( + model: model, + ), + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 65b6ed2b3..284416545 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart'; import 'package:stackwallet/providers/exchange/exchange_provider.dart'; @@ -16,10 +17,11 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; class DesktopStep3 extends ConsumerStatefulWidget { const DesktopStep3({ @@ -76,14 +78,15 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> { Navigator.of(context).pop(); } - unawaited(showDialog<void>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: response.exception?.toString(), + unawaited( + showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: response.exception?.toString() ?? ""), ), - )); + ); return; } @@ -106,22 +109,40 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> { Navigator.of(context).pop(); } - unawaited(NotificationApi.showNotification( - changeNowId: model.trade!.tradeId, - title: status, - body: "Trade ID ${model.trade!.tradeId}", - walletId: "", - iconAssetName: Assets.svg.arrowRotate, - date: model.trade!.timestamp, - shouldWatchForUpdates: true, - coinName: "coinName", - )); + unawaited( + NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", + ), + ); if (mounted) { - unawaited(Navigator.of(context).pushNamed( - Step4View.routeName, - arguments: model, - )); + unawaited( + showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 4, + model: model, + body: DesktopStep4( + model: model, + ), + ), + ); + }, + ), + ); } } diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index ba9838086..7747f570f 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -2,12 +2,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -148,7 +150,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { DesktopStepItem( label: "Amount", value: - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, @@ -208,7 +210,50 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { label: "Show QR code", buttonHeight: ButtonHeight.l, onPressed: () { - // todo + showDialog<dynamic>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: true, + builder: (_) { + return DesktopDialog( + maxHeight: 720, + maxWidth: 720, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Send ${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker} to this address", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 48, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: 290, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + const SizedBox( + height: 48, + ), + SecondaryButton( + label: "Cancel", + width: 310, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart index 7c777c2dd..323517e13 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -24,6 +24,7 @@ class DesktopStepItem extends StatelessWidget { child: ConditionalParent( condition: vertical, builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ child, const SizedBox( diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index 40eeb8c1b..41bf4246a 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -31,6 +31,8 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { if (hasHistory) { return ListView.separated( + shrinkWrap: true, + primary: false, itemBuilder: (context, index) { return TradeCard( key: Key("tradeCard_${trades[index].uuid}"), From a10958b12d02f0073354cb4402504f46ec75efa9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 15:08:31 -0700 Subject: [PATCH 080/100] delete popup rounded corner fix --- .../wallet_view/sub_widgets/delete_wallet_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index f2553c2da..a2071e7d6 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -127,7 +127,7 @@ class _DeletePopupButtonState extends State<DeletePopupButton> { height: 70, decoration: BoxDecoration( borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + Constants.size.circularBorderRadius * 2, ), color: Theme.of(context).extension<StackColors>()!.popupBG, boxShadow: [ From 7a650c78d39006620e6ba4f96069feb42f6e1153 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 15:17:26 -0700 Subject: [PATCH 081/100] loading indicator and delay for wallet keys --- .../unlock_wallet_keys_desktop.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 23360c98c..739a3ebc4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { @@ -201,11 +202,32 @@ class _UnlockWalletKeysDesktopState enabled: continueEnabled, onPressed: continueEnabled ? () async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + final verified = await ref .read(storageCryptoHandlerProvider) .verifyPassphrase(passwordController.text); if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + final words = await ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) @@ -219,6 +241,11 @@ class _UnlockWalletKeysDesktopState ); } } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed( + const Duration(milliseconds: 300)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From 675977c787d9d987819852199fefe32882e7bfe6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 20:33:38 -0700 Subject: [PATCH 082/100] copy to clipboard added to wallet keys dialog --- .../sub_widgets/delete_wallet_keys_popup.dart | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index 6b638bb75..70f4a3e13 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -1,9 +1,15 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -16,10 +22,12 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.words, + this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); final String walletId; final List<String> words; + final ClipboardInterface clipboardInterface; static const String routeName = "/desktopDeleteWalletKeysPopup"; @@ -30,10 +38,14 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { late final String _walletId; + late final List<String> _words; + late final ClipboardInterface _clipboardInterface; @override void initState() { _walletId = widget.walletId; + _words = widget.words; + _clipboardInterface = widget.clipboardInterface; super.initState(); } @@ -96,12 +108,28 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { padding: const EdgeInsets.symmetric( horizontal: 32, ), - child: MnemonicTable( - words: widget.words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, + child: RawMaterialButton( + hoverColor: Colors.transparent, + onPressed: () async { + await _clipboardInterface.setData( + ClipboardData(text: _words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + child: MnemonicTable( + words: widget.words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), ), ), const SizedBox( From 6384d66308066645500f516eca9784163a8285d0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 20:34:36 -0700 Subject: [PATCH 083/100] added delays for floatingFlushBar in settings change password --- .../home/settings_menu/security_settings.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index f6762afa1..ff7537126 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -61,6 +61,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { if (verified) { if (pwNew != pwNewRepeat) { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -77,6 +79,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ); if (success) { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.success, @@ -86,6 +90,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ); return true; } else { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -97,6 +103,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { } } } else { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From b32e15a3ea1176eb4f77920fa97b46f1d95c3828 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 07:13:03 -0600 Subject: [PATCH 084/100] desktop login on enter pressed --- lib/pages_desktop_specific/desktop_login_view.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index f865fad47..eb5dec18a 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -165,6 +165,12 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + login(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, From b512b2cefb4663a0075b749c2cc2d4126060717b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 07:15:08 -0600 Subject: [PATCH 085/100] consistent decimal places on firo balance selection sheet --- .../firo_balance_selection_sheet.dart | 4 ++-- .../wallet_balance_toggle_sheet.dart | 11 ++++----- lib/utilities/constants.dart | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index e639a8cf8..d6de3c6ee 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -161,7 +161,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); @@ -251,7 +251,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index c9ff64393..74308f2e8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; - class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ Key? key, @@ -153,7 +152,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -195,7 +194,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -287,7 +286,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -329,7 +328,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index e27fbaa3d..0a062de67 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -40,6 +40,30 @@ abstract class Constants { static const int currentHiveDbVersion = 3; + static int decimalPlacesForCoin(Coin coin) { + switch (coin) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + case Coin.epicCash: + case Coin.namecoin: + return decimalPlaces; + + case Coin.wownero: + return decimalPlacesWownero; + + case Coin.monero: + return decimalPlacesMonero; + } + } + static List<int> possibleLengthsForCoin(Coin coin) { final List<int> values = []; switch (coin) { From 7afe6940f9dc5f20f08eb59e606d8e4cbb452cd4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 08:07:22 -0600 Subject: [PATCH 086/100] desktop trade history details updated --- .../exchange_view/trade_details_view.dart | 186 ++++++++++++++---- .../subwidgets/desktop_trade_history.dart | 124 +++++++++++- lib/widgets/trade_card.dart | 134 +++++++------ 3 files changed, 335 insertions(+), 109 deletions(-) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 602d588da..f28d1d617 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -206,16 +206,57 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { padding: const EdgeInsets.only( right: 12, ), - child: RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension<StackColors>()!.background - : null, - padding: const EdgeInsets.all(0), - child: ListView( - primary: false, - shrinkWrap: true, - children: children, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + if (isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + const SizedBox( + height: 32, + ), + if (isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + SecondaryButton( + label: "Send from Stack", + buttonHeight: ButtonHeight.l, + onPressed: () { + final amount = sendAmount; + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4( + coin, + amount, + address, + trade, + ), + ); + }, + ), + const SizedBox( + height: 32, + ), + ], ), ), ), @@ -350,33 +391,94 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, - ), - children: [ - TextSpan( - text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.popupBG + : Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], ), - ), - ]), + IconCopyButton( + data: trade.payInAmount, + ), + ], + ), + const SizedBox( + height: 6, + ), + child, + ], + ), + child: RichText( + text: TextSpan( + text: + "You must send at least ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, + )} ${trade.payInCurrency.toUpperCase()}. ", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + children: [ + TextSpan( + text: + "If you send less than ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" + ? 12 + : 8, + )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ]), + ), ), ), if (sentFromStack) @@ -1035,12 +1137,12 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (isStackCoin(trade.payInCurrency) && + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index 41bf4246a..a8f825911 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -10,7 +10,10 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/trade_card.dart'; -import 'package:tuple/tuple.dart'; + +import '../../../route_generator.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; class DesktopTradeHistory extends ConsumerStatefulWidget { const DesktopTradeHistory({Key? key}) : super(key: key); @@ -64,19 +67,122 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { final tx = txData.getAllTransactions()[txid]; if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4( - tradeId, tx, walletIds.first, manager.walletName), + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: manager.walletName, + walletId: walletIds.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, ), ); } } else { unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4(tradeId, null, walletIds?.first, null), + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), ), ); } diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index 0ac8e9346..5a14a0777 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class TradeCard extends ConsumerWidget { @@ -49,68 +50,85 @@ class TradeCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: onTap, - child: RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - ), - child: Center( - child: SvgPicture.asset( - _fetchIconAssetForStatus( - trade.status, - context, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: onTap, + child: RoundedWhiteContainer( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + ), + child: Center( + child: SvgPicture.asset( + _fetchIconAssetForStatus( + trade.status, + context, + ), + width: 32, + height: 32, ), - width: 32, - height: 32, ), ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - Text( - "${Util.isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - const SizedBox( - height: 2, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - trade.exchangeName, - style: STextStyles.label(context), - ), - Text( - Format.extractDateFrom( - trade.timestamp.millisecondsSinceEpoch ~/ 1000), - style: STextStyles.label(context), - ), - ], - ), - ], + const SizedBox( + width: 12, ), - ) - ], + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + Text( + "${isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + Text( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.label(context), + ), + if (isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + ], + ), + ], + ), + ) + ], + ), ), ), ); From 6552fc913db3d66782eddc02c54f6eb155e7a730 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 09:11:18 -0600 Subject: [PATCH 087/100] WIP send auth for trade transactions --- lib/pages/exchange_view/send_from_view.dart | 281 ++++++++++++++++-- .../send_view/confirm_transaction_view.dart | 17 +- .../sub_widgets/desktop_auth_send.dart | 32 +- 3 files changed, 301 insertions(+), 29 deletions(-) diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index c87175955..59675e4e4 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; @@ -19,13 +20,18 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import '../../pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; + class SendFromView extends ConsumerStatefulWidget { const SendFromView({ Key? key, @@ -90,21 +96,68 @@ class _SendFromViewState extends ConsumerState<SendFromView> { final walletIds = ref.watch(walletsChangeNotifierProvider .select((value) => value.getWalletIdsFor(coin: coin))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Send from", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Send from Stack", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: false, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: child, + ), + ], + ), ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -112,15 +165,23 @@ class _SendFromViewState extends ConsumerState<SendFromView> { children: [ Text( "You need to send ${formatAmount(amount, coin)} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), const SizedBox( height: 16, ), - Expanded( + ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), child: ListView.builder( + primary: isDesktop ? false : null, + shrinkWrap: isDesktop, itemCount: walletIds.length, itemBuilder: (context, index) { return Padding( @@ -339,10 +400,67 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send( - manager, - shouldSendPublicFiroFunds: false, - ), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: false, + ), + ); + } + }, child: Container( color: Colors.transparent, child: Padding( @@ -418,10 +536,67 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send( - manager, - shouldSendPublicFiroFunds: true, - ), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: true, + ), + ); + } + }, child: Container( color: Colors.transparent, child: Padding( @@ -504,7 +679,63 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send(manager), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send(manager), + ); + } + }, child: child, ), child: Row( diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 8f7afb0bb..276203804 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -37,6 +37,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { required this.transactionInfo, required this.walletId, this.routeOnSuccessName = WalletView.routeName, + this.isTradeTransaction = false, }) : super(key: key); static const String routeName = "/confirmTransactionView"; @@ -44,6 +45,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { final Map<String, dynamic> transactionInfo; final String walletId; final String routeOnSuccessName; + final bool isTradeTransaction; @override ConsumerState<ConfirmTransactionView> createState() => @@ -833,8 +835,19 @@ class _ConfirmTransactionViewState ); } - if (unlocked is bool && unlocked && mounted) { - unawaited(_attemptSend(context)); + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context), + ); + } } }, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index 9f863c8a4..a8d1ea497 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -10,6 +12,9 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import '../../../../../notifications/show_flush_bar.dart'; +import '../../../../../widgets/loading_indicator.dart'; + class DesktopAuthSend extends ConsumerStatefulWidget { const DesktopAuthSend({Key? key}) : super(key: key); @@ -155,12 +160,35 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { label: "Confirm", buttonHeight: ButtonHeight.l, onPressed: () async { - // TODO show spinner while verifying passphrase + unawaited( + showDialog<void>( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); final passwordIsValid = await verifyPassphrase(); if (mounted) { - Navigator.of(context).pop(passwordIsValid); + Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(passwordIsValid); + await Future<void>.delayed(const Duration( + milliseconds: 100, + )); } }, ), From 0bdf337ffbeed8c7ebb8a3aa00271c6ccadbacf1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 11:21:43 -0600 Subject: [PATCH 088/100] WIP send from stack desktop trade transaction navigation --- .../confirm_change_now_send.dart | 842 ++++++++++++------ lib/pages/exchange_view/send_from_view.dart | 196 +--- .../exchange_view/trade_details_view.dart | 7 +- .../subwidgets/desktop_step_2.dart | 92 +- .../subwidgets/desktop_step_4.dart | 36 +- 5 files changed, 700 insertions(+), 473 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index e99cf2df4..9f62bd8ec 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -7,15 +7,23 @@ import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -52,14 +60,16 @@ class _ConfirmChangeNowSendViewState late final Trade trade; Future<void> _attemptSend(BuildContext context) async { - unawaited(showDialog<void>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return const SendingTransactionDialog(); - }, - )); + unawaited( + showDialog<void>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return const SendingTransactionDialog(); + }, + ), + ); final String note = transactionInfo["note"] as String? ?? ""; final manager = @@ -93,6 +103,9 @@ class _ConfirmChangeNowSendViewState // pop back to wallet if (mounted) { + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + } Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e) { @@ -129,6 +142,60 @@ class _ConfirmChangeNowSendViewState } } + Future<void> _confirmSend() async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + await _attemptSend(context); + } + } + @override void initState() { transactionInfo = widget.transactionInfo; @@ -142,280 +209,503 @@ class _ConfirmChangeNowSendViewState Widget build(BuildContext context) { final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Send from", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - ref - .watch(walletsChangeNotifierProvider) - .getManager(walletId) - .walletName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "${trade.exchangeName} address", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - "${transactionInfo["address"] ?? "ERROR"}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["recipientAmt"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["fee"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["note"] as String? ?? "", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade ID", - style: STextStyles.smallMed12(context), - ), - Text( - trade.tradeId, - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackSuccess, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Total amount", - style: - STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: - "Confirm Transaction", - ), - settings: const RouteSettings( - name: "/confirmsendlockscreen"), - ), - ); - if (unlocked is bool && unlocked && mounted) { - await _attemptSend(context); - } - }, - child: Text( - "Send", - style: STextStyles.button(context), - ), - ), - ], + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), + ); + }, + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 6, + ), + const AppBarBackButton( + isCompact: true, + iconSize: 23, + ), + const SizedBox( + width: 12, + ), + Text( + "Confirm ${ref.watch(managerProvider.select((value) => value.coin)).ticker} transaction", + style: STextStyles.desktopH3(context), + ) + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: child, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Text( + "Transaction fee", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + const SizedBox( + height: 10, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + ), + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int) + + (transactionInfo["recipientAmt"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + child, + ], + ), + ), + ), + child: Text( + "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + style: isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.pageTitleH1(context), ), ), - ); - }, + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Send from", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + ref + .watch(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "${trade.exchangeName} address", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + "${transactionInfo["address"] ?? "ERROR"}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => Row( + children: [ + child, + Builder(builder: (context) { + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).coin)); + final price = ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + final amount = Format.satoshisToAmount( + transactionInfo["recipientAmt"] as int, + coin: coin, + ); + final value = price.item1 * amount; + final currency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + return Text( + " | ${value.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} $currency", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ); + }) + ], + ), + child: Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["recipientAmt"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["fee"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["note"] as String? ?? "", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade ID", + style: STextStyles.smallMed12(context), + ), + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop) + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + ), + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int) + + (transactionInfo["recipientAmt"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + if (!isDesktop) + PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ], + ), ), ); } diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 59675e4e4..7cbf38384 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -7,8 +7,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; @@ -30,8 +30,6 @@ import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import '../../pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; - class SendFromView extends ConsumerStatefulWidget { const SendFromView({ Key? key, @@ -39,6 +37,7 @@ class SendFromView extends ConsumerStatefulWidget { required this.trade, required this.amount, required this.address, + this.shouldPopRoot = false, }) : super(key: key); static const String routeName = "/sendFrom"; @@ -47,6 +46,7 @@ class SendFromView extends ConsumerStatefulWidget { final Decimal amount; final String address; final Trade trade; + final bool shouldPopRoot; @override ConsumerState<SendFromView> createState() => _SendFromViewState(); @@ -142,7 +142,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { DesktopDialogCloseButton( onPressedOverride: Navigator.of( context, - rootNavigator: false, + rootNavigator: widget.shouldPopRoot, ).pop, ), ], @@ -239,12 +239,23 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { useSafeArea: false, barrierDismissible: false, builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), + child: BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; - Navigator.of(context).pop(); - }, + Navigator.of(context).pop(); + }, + ), ); }, ), @@ -290,7 +301,10 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { // pop building dialog if (mounted) { - Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(); } txData["note"] = @@ -304,7 +318,9 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { builder: (_) => ConfirmChangeNowSendView( transactionInfo: txData, walletId: walletId, - routeOnSuccessName: HomeView.routeName, + routeOnSuccessName: Util.isDesktop + ? DesktopExchangeView.routeName + : HomeView.routeName, trade: trade, shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, ), @@ -401,58 +417,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send( manager, @@ -537,58 +502,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send( manager, @@ -680,57 +594,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send(manager), ); diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index f28d1d617..0b7f4b502 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -219,7 +219,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { children: children, ), ), - if (isStackCoin(trade.payInCurrency) && + if (!hasTx && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || @@ -227,7 +228,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { const SizedBox( height: 32, ), - if (isStackCoin(trade.payInCurrency) && + if (!hasTx && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || @@ -1142,6 +1144,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { height: 12, ), if (!isDesktop && + !hasTx && isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 525d561fc..7b793210c 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -51,6 +51,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { late final FocusNode _toFocusNode; late final FocusNode _refundFocusNode; + bool enableNext = false; + bool isStackCoin(String ticker) { try { coinFromTickerCaseInsensitive(ticker); @@ -60,13 +62,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { } } - void selectRecipientAddressFromStack() { + void selectRecipientAddressFromStack() async { try { final coin = coinFromTickerCaseInsensitive( model.receiveTicker, ); - showDialog<String?>( + final address = await showDialog<String?>( context: context, barrierColor: Colors.transparent, builder: (context) => DesktopDialog( @@ -79,27 +81,31 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), ), - ).then((value) async { - if (value is String) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(value); + ); - _toController.text = manager.walletName; - model.recipientAddress = await manager.currentReceivingAddress; - } - }); + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); + + _toController.text = manager.walletName; + model.recipientAddress = await manager.currentReceivingAddress; + } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Info); } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } - void selectRefundAddressFromStack() { + void selectRefundAddressFromStack() async { try { final coin = coinFromTickerCaseInsensitive( model.sendTicker, ); - showDialog<String?>( + final address = await showDialog<String?>( context: context, barrierColor: Colors.transparent, builder: (context) => DesktopDialog( @@ -112,18 +118,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), ), - ).then((value) async { - if (value is String) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(value); + ); + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); - _refundController.text = manager.walletName; - model.refundAddress = await manager.currentReceivingAddress; - } - }); + _refundController.text = manager.walletName; + model.refundAddress = await manager.currentReceivingAddress; + } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Info); } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } void selectRecipientFromAddressBook() async { @@ -168,7 +177,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { if (entry != null) { _toController.text = entry.address; model.recipientAddress = entry.address; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } } @@ -214,7 +226,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { if (entry != null) { _refundController.text = entry.address; model.refundAddress = entry.address; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } } @@ -334,7 +349,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { focusNode: _toFocusNode, style: STextStyles.field(context), onChanged: (value) { - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, decoration: standardInputDecoration( "Enter the ${model.receiveTicker.toUpperCase()} payout address", @@ -363,7 +381,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { onTap: () { _toController.text = ""; model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -378,7 +399,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final content = data.text!.trim(); _toController.text = content; model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); } }, child: _toController.text.isEmpty @@ -454,7 +479,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { focusNode: _refundFocusNode, style: STextStyles.field(context), onChanged: (value) { - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, decoration: standardInputDecoration( "Enter ${model.sendTicker.toUpperCase()} refund address", @@ -484,7 +512,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { _refundController.text = ""; model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -501,7 +532,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { _refundController.text = content; model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); } }, child: _refundController.text.isEmpty @@ -552,6 +587,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { Expanded( child: PrimaryButton( label: "Next", + enabled: enableNext, buttonHeight: ButtonHeight.l, onPressed: () async { await showDialog<void>( diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 7747f570f..c86713a76 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -199,7 +202,38 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { child: SecondaryButton( label: "Send from Stack Wallet", buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, + onPressed: () { + final trade = model.trade!; + final amount = Decimal.parse(trade.payInAmount); + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: SendFromView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + SendFromView( + coin: coin, + trade: trade, + amount: amount, + address: address, + shouldPopRoot: true, + ), + const RouteSettings( + name: SendFromView.routeName, + ), + ), + ]; + }, + ), + ); + }, ), ), const SizedBox( From c5c0443d000d425b86056d27dda575a3b895fde6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 22 Nov 2022 08:57:05 -0700 Subject: [PATCH 089/100] button sizing fix --- .../home/settings_menu/currency_settings/currency_settings.dart | 2 +- .../home/settings_menu/language_settings/language_settings.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index d9c20d8fa..9f5f42b7c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -107,7 +107,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { 10, ), child: PrimaryButton( - width: 210, + width: 200, buttonHeight: ButtonHeight.m, enabled: true, label: "Change currency", diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index acddcb055..3c511236c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -84,7 +84,7 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { 10, ), child: PrimaryButton( - width: 210, + width: 200, buttonHeight: ButtonHeight.m, enabled: true, label: "Change language", From 9a47ce349e52a12d2be80ab7f48ede2b9ca7f379 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 22 Nov 2022 11:48:47 -0700 Subject: [PATCH 090/100] submit on enter passphrase --- .../desktop_attention_delete_wallet.dart | 3 +- .../desktop_delete_wallet_dialog.dart | 126 +++++++++--------- 2 files changed, 66 insertions(+), 63 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 6eb04502e..a614be3a6 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -10,8 +11,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:tuple/tuple.dart'; -import 'delete_wallet_keys_popup.dart'; - class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { const DesktopAttentionDeleteWallet({ Key? key, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index 6729d33b9..e8d9c2dc5 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -5,6 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -16,9 +18,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; - class DesktopDeleteWalletDialog extends ConsumerStatefulWidget { const DesktopDeleteWalletDialog({ Key? key, @@ -42,6 +41,62 @@ class _DesktopDeleteWalletDialog bool hidePassword = true; bool _continueEnabled = false; + Future<void> enterPassphrase() async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + Navigator.of(context).pop(); + + unawaited( + Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ), + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed(const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + @override void initState() { passwordController = TextEditingController(); @@ -106,6 +161,12 @@ class _DesktopDeleteWalletDialog obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + enterPassphrase(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, @@ -179,64 +240,7 @@ class _DesktopDeleteWalletDialog onPressed: _continueEnabled ? () async { // add loading indicator - unawaited( - showDialog( - context: context, - builder: (context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: const [ - LoadingIndicator( - width: 200, - height: 200, - ), - ], - ), - ), - ); - - await Future<void>.delayed( - const Duration(seconds: 1)); - - final verified = await ref - .read(storageCryptoHandlerProvider) - .verifyPassphrase(passwordController.text); - - if (verified) { - Navigator.of(context, rootNavigator: true) - .pop(); - - final words = await ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .mnemonic; - - if (mounted) { - Navigator.of(context).pop(); - - unawaited( - Navigator.of(context).pushNamed( - DesktopAttentionDeleteWallet.routeName, - arguments: widget.walletId, - ), - ); - } - } else { - Navigator.of(context, rootNavigator: true) - .pop(); - - await Future<void>.delayed( - const Duration(milliseconds: 300)); - - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Invalid passphrase!", - context: context, - ), - ); - } + enterPassphrase(); } : null, ), From 8e2ff3883d3516ee561d9acb36a89a5de94a0400 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 14:42:24 -0600 Subject: [PATCH 091/100] exchange amount field re style --- lib/pages/exchange_view/exchange_form.dart | 362 ++--------------- .../textfields/exchange_textfield.dart | 384 ++++++++++++++++++ 2 files changed, 424 insertions(+), 322 deletions(-) create mode 100644 lib/widgets/textfields/exchange_textfield.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7f0d9b5d2..921b35bf0 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; @@ -25,6 +24,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -37,10 +37,10 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/exchange_textfield.dart'; import 'package:tuple/tuple.dart'; class ExchangeForm extends ConsumerStatefulWidget { @@ -1226,146 +1226,33 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { SizedBox( height: isDesktop ? 10 : 4, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( + ExchangeTextField( + controller: _sendController, + focusNode: _sendFocusNode, + textStyle: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textDark, ), - focusNode: _sendFocusNode, - controller: _sendController, - textAlign: TextAlign.right, - enableSuggestions: false, - autocorrect: false, + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; } }, onChanged: sendFieldOnChanged, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: selectSendCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.fromTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - 18, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select((value) => - value.fromTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, true)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, true)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectSendCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker)), + ), + SizedBox( + height: isDesktop ? 10 : 4, ), - SizedBox( height: isDesktop ? 10 : 4, ), @@ -1422,79 +1309,20 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ], ), - // Stack( - // children: [ - // Positioned.fill( - // child: Align( - // alignment: Alignment.bottomLeft, - // child: Text( - // "You will receive", - // style: STextStyles.itemSubtitle(context).copyWith( - // color: - // Theme.of(context).extension<StackColors>()!.textDark3, - // ), - // ), - // ), - // ), - // Center( - // child: Column( - // children: [ - // const SizedBox( - // height: 6, - // ), - // GestureDetector( - // onTap: () async { - // await _swap(); - // }, - // child: Padding( - // padding: const EdgeInsets.all(4), - // child: SvgPicture.asset( - // Assets.svg.swap, - // width: 20, - // height: 20, - // color: Theme.of(context) - // .extension<StackColors>()! - // .accentColorDark, - // ), - // ), - // ), - // const SizedBox( - // height: 6, - // ), - // ], - // ), - // ), - // Positioned.fill( - // child: Align( - // alignment: ref.watch(exchangeFormStateProvider - // .select((value) => value.reversed)) - // ? Alignment.bottomRight - // : Alignment.topRight, - // child: Text( - // ref.watch(exchangeFormStateProvider - // .select((value) => value.warning)), - // style: STextStyles.errorSmall(context), - // ), - // ), - // ), - // ], - // ), SizedBox( height: isDesktop ? 10 : 4, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), + ExchangeTextField( focusNode: _receiveFocusNode, controller: _receiveController, - enableSuggestions: false, - autocorrect: false, - readOnly: ref.watch(prefsChangeNotifierProvider - .select((value) => value.exchangeRateType)) == - ExchangeRateType.estimated || - ref.watch(exchangeProvider).name == - SimpleSwapExchange.exchangeName, + textStyle: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (!(ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) && @@ -1503,127 +1331,17 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }, onChanged: receiveFieldOnChanged, - textAlign: TextAlign.right, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: GestureDetector( - onTap: selectReceiveCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.toTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(18), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select( - (value) => value.toTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, false)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, false)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectReceiveCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker)), + readOnly: ref.watch(prefsChangeNotifierProvider + .select((value) => value.exchangeRateType)) == + ExchangeRateType.estimated || + ref.watch(exchangeProvider).name == + SimpleSwapExchange.exchangeName, ), if (ref .watch( diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart new file mode 100644 index 000000000..8d3c5d699 --- /dev/null +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; + +class ExchangeTextField extends StatefulWidget { + const ExchangeTextField({ + Key? key, + this.borderRadius = 0, + this.background, + required this.controller, + this.buttonColor, + required this.focusNode, + this.buttonContent, + required this.textStyle, + this.onButtonTap, + this.onChanged, + this.onSubmitted, + this.onTap, + required this.isWalletCoin, + this.image, + this.ticker, + this.readOnly = false, + }) : super(key: key); + + final double borderRadius; + final Color? background; + final Color? buttonColor; + final Widget? buttonContent; + final TextEditingController controller; + final FocusNode focusNode; + final TextStyle textStyle; + final VoidCallback? onTap; + final VoidCallback? onButtonTap; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + + final bool isWalletCoin; + final bool readOnly; + final String? image; + final String? ticker; + + @override + State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +} + +class _ExchangeTextFieldState extends State<ExchangeTextField> { + late final TextEditingController controller; + late final FocusNode focusNode; + late final TextStyle textStyle; + + late final double borderRadius; + + late final Color? background; + late final Color? buttonColor; + late final Widget? buttonContent; + late final VoidCallback? onButtonTap; + late final VoidCallback? onTap; + late final void Function(String)? onChanged; + late final void Function(String)? onSubmitted; + + @override + void initState() { + borderRadius = widget.borderRadius; + background = widget.background; + buttonColor = widget.buttonColor; + controller = widget.controller; + focusNode = widget.focusNode; + buttonContent = widget.buttonContent; + textStyle = widget.textStyle; + onButtonTap = widget.onButtonTap; + onChanged = widget.onChanged; + onSubmitted = widget.onSubmitted; + onTap = widget.onTap; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextField( + style: textStyle, + controller: controller, + focusNode: focusNode, + onChanged: onChanged, + onTap: onTap, + enableSuggestions: false, + autocorrect: false, + readOnly: widget.readOnly, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + left: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + ), + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => onButtonTap?.call(), + child: Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.horizontal( + right: Radius.circular( + borderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + ), + child: Builder( + builder: (context) { + final image = widget.image; + + if (image != null && image.isNotEmpty) { + return Center( + child: SvgPicture.network( + image, + height: 18, + placeholderBuilder: (_) => Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + 18, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 18, + ), + child: const LoadingIndicator(), + ), + ), + ), + ); + } else { + return Container( + width: 18, + height: 18, + decoration: BoxDecoration( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + borderRadius: BorderRadius.circular(18), + ), + child: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ); + } + }, + ), + ), + const SizedBox( + width: 6, + ), + Text( + widget.ticker ?? "-", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (!widget.isWalletCoin) + const SizedBox( + width: 6, + ), + if (!widget.isWalletCoin) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 5, + height: 2.5, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +// experimental UNUSED +// class ExchangeTextField extends StatefulWidget { +// const ExchangeTextField({ +// Key? key, +// this.borderRadius = 0, +// this.background, +// required this.controller, +// this.buttonColor, +// required this.focusNode, +// this.buttonContent, +// required this.textStyle, +// this.onButtonTap, +// this.onChanged, +// this.onSubmitted, +// }) : super(key: key); +// +// final double borderRadius; +// final Color? background; +// final Color? buttonColor; +// final Widget? buttonContent; +// final TextEditingController controller; +// final FocusNode focusNode; +// final TextStyle textStyle; +// final VoidCallback? onButtonTap; +// final void Function(String)? onChanged; +// final void Function(String)? onSubmitted; +// +// @override +// State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +// } +// +// class _ExchangeTextFieldState extends State<ExchangeTextField> { +// late final TextEditingController controller; +// late final FocusNode focusNode; +// late final TextStyle textStyle; +// +// late final double borderRadius; +// +// late final Color? background; +// late final Color? buttonColor; +// late final Widget? buttonContent; +// late final VoidCallback? onButtonTap; +// late final void Function(String)? onChanged; +// late final void Function(String)? onSubmitted; +// +// @override +// void initState() { +// borderRadius = widget.borderRadius; +// background = widget.background; +// buttonColor = widget.buttonColor; +// controller = widget.controller; +// focusNode = widget.focusNode; +// buttonContent = widget.buttonContent; +// textStyle = widget.textStyle; +// onButtonTap = widget.onButtonTap; +// onChanged = widget.onChanged; +// onSubmitted = widget.onSubmitted; +// +// super.initState(); +// } +// +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: background, +// borderRadius: BorderRadius.circular(borderRadius), +// ), +// child: IntrinsicHeight( +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Expanded( +// child: MouseRegion( +// cursor: SystemMouseCursors.text, +// child: GestureDetector( +// onTap: () { +// // +// }, +// child: Padding( +// padding: const EdgeInsets.only( +// left: 16, +// top: 18, +// bottom: 17, +// ), +// child: IgnorePointer( +// ignoring: true, +// child: EditableText( +// controller: controller, +// focusNode: focusNode, +// style: textStyle, +// onChanged: onChanged, +// onSubmitted: onSubmitted, +// onEditingComplete: () => print("lol"), +// autocorrect: false, +// enableSuggestions: false, +// keyboardType: const TextInputType.numberWithOptions( +// signed: false, +// decimal: true, +// ), +// inputFormatters: [ +// // regex to validate a crypto amount with 8 decimal places +// TextInputFormatter.withFunction((oldValue, +// newValue) => +// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') +// .hasMatch(newValue.text) +// ? newValue +// : oldValue), +// ], +// cursorColor: textStyle.color ?? +// Theme.of(context).backgroundColor, +// backgroundCursorColor: background ?? Colors.transparent, +// ), +// ), +// ), +// ), +// ), +// ), +// MouseRegion( +// cursor: SystemMouseCursors.click, +// child: GestureDetector( +// onTap: () => onButtonTap?.call(), +// child: Container( +// decoration: BoxDecoration( +// color: buttonColor, +// borderRadius: BorderRadius.horizontal( +// right: Radius.circular( +// borderRadius, +// ), +// ), +// ), +// child: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 16, +// ), +// child: buttonContent, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } From 0b6545645bdd7bdf1939c2de2b42397e47614210 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 14:55:26 -0600 Subject: [PATCH 092/100] macos + windows app icon --- .../AppIcon.appiconset/Contents.json | 132 +++++++++--------- .../AppIcon.appiconset/app_icon_1024.png | Bin 46993 -> 69450 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 3276 -> 4664 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 1429 -> 926 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 5933 -> 10085 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1243 -> 1441 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 14800 -> 26089 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1874 -> 2443 bytes pubspec.lock | 9 +- pubspec.yaml | 9 +- windows/runner/resources/app_icon.ico | Bin 33772 -> 1968 bytes 11 files changed, 82 insertions(+), 68 deletions(-) diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..96d3fee1a 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..55efad011aa22c651bd4eeb225bebd9c6fea31bd 100644 GIT binary patch literal 69450 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 PKlPC5u>%Drr*HfpG~rAC literal 46993 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 QOu#(cH>|q6Qt;3J0APUL!~g&Q diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..c8ba3b1a21bf014a0ed55ac2169dc48b4abb240f 100644 GIT binary patch literal 4664 zcmb7|<zEwC*v7ZfASoR`av<F`ln|u58xe=Jlz`-rfpm?KkkK8J5JX}$f`E+f1`!6* zjStUX@O+*Z=bRVk&3&%V9pCH3=<BMH5;GD5002@Abrr*h6ZXH05dUHCk+&ZP0Dy33 zWo3O0Wo0%`@3)T5ZcqRqAU7oCnR<^RbDy1&mWd_xKHGY70mOoW#%q#~y5M;^jpaL$ zZ>J>L5?s7Oi630Mx{~@TBjOrr$!b5aT>{sCZx;XN0N)GqHb$SVwy+)^JUdthK5zdi zQRT|~%XdK=y#9*&J0HFsr3(EWVZNGUhA5AQhA*?-#owbdq5cL4^2P@~mD>kEsy`im zG{L>hmbctUI(pMfX_{uT?z1k65K_M#eAl-jJ&Ko!m&YJFQSpAA@Qb;VLrH?g_+)fh zbWvrgQAhHT`Bqz>1;3faN#1Q_L2gI=&v)59>Mtp^lG&as(SXzJIaRB4V<V}t19F!i ztvgkOBCWw9-U9g`()vAue-SH7mRCQBgn6k6K*EV9q99!VaMcm7jwF^cFVH12oWFgm zZZbSe592B*7>Il7jzc#is56Hn<PYoOQI>+59ZygnzhAwJ@8BhQv<PZ<wS7Ahk}D5z z{=@ekhnX2vgg0b+`1j!8eEI&_#Qq_{zPY#8i<bEW0D$7HhKiDL01*3`s8M%|aex)7 zo@D}O-e4brvXngW0n<EBEKuqnEaVhxY?bw0>Mry^fJ^q)y=62@4{T%&wpCupP;)3d zW_`T5Uv}GL9{o!ibbEeZcUIFYdlPgK`tML&VQe($_VVb23f(-ocM1Z@#K8daYKj0P zCmU>xk^;R$NVw{Qi~Cm=1iEAf0)rF(&uco*^<JW@jU<K5Uj@qE*Qd^R{${rCOQ`ux zwKX#Yc+KoWntJ#f@jH0X<3(!FiBg@wlatLKK69Oi%k5U9NCjcAR$8)VI-kau^*$E! zl6QXvf%ms3dhJIy=hz9R+^KD*)s=Hr8MYhK+B7DQ7Nttl8a_N?YLr*gbeBJ_jL&ww zfNh(3=nYny*QV9iypx2|4?7PIN;6huOiPMG@Yl9<7pBkStWdy=Iy{yjS)vY6)I!$g z9vxTP(RJ^1ZCs54TisSVY${Fagl6o*+GF^e=sMT{Wg(_6mA7s75zDu5IGp)K=#$k8 zy*vomb-FrbeMrBWkyZFb9Id2t-#L3Jk>}-K`>XXh;n#EZP`b0=CYPzkcW-H1ww1PI ztIfYWUm!54G(JiTp+MlG6RbqSI&KfE>Ncw}SNO*}V|CI$_-^lS4(s2H{a0aL@ZrsV z#3v3wS@2Y)iJ1Gm1a`(QcCYsP=&_D$(0O(7+hqYc8ir(ik7n{$QGlE&Wu$ya>-5E( zd;Mp~_3RgidPl4!i$b)ShXXq_sD(0*2M`*psTq5Fxshbay5rl8ogVp=b#rl;2_4SV zV%W!d0S~@9o@<R-T(tl(D}-8a|I8{1x%Sx^ec?W3MPxjY_*o9xTu&a&x)`vLP-{`Q zL+GiyJKI>-nSn<_>m<rV_G?V{1KTA2RxFi5%Wi2-G2D<`w?`azm3qx(#BBO*xjl#z zqEwc)av)(YjUjQMDUy(vUmjOM@DQTh7o;?Fbf&%p`34uSi(ALogIfrw^k+r3hBDHJ zh8zZSX6~_`l0Y(Mnf$Y-Jgh_%H6;F<gOmtU$NyUDutPpw?kbFFyhi<|H|TU+^hZ4{ zErf9A?eQy*<wuHueSkP;xMTRclq$t}%9_Suc~2Uk3@O}wXY_NGUgsPSkTZVIHVE5P zYC9v~*;!oCd2=AgqF7lu`CKUui(^IsOVHEPo5^Zrk@NdaU>`}pnN1zgk`7={QfFW) zc+kv{T}b!Xl3Kwt7qIw*Nso<S+^y+;q2kudT12>aH|Fi_^<EuhK)>SE3*Msm7Y)cH z2V6AG^?t+HX3pD=acO9#?RHQK$do(sXa5fb4|M7J%o6hINw!6XqZ}|kmKoD{x#g$U zdnv!DQ5n8Sl|X`5eTEWyWmanmF40WKsEBYjy7L1=FV_gNWPKdJ;(hX4TkVRlB!*j2 zZPTLsKG+w3-k9WJeCJ34{L1%CN?g(;^lFRl?w>C5X$Na&oO&*kO}9QQL~%ZA|4o}W zQwnhk+7QGn@2hS&8Dm$o$XF(QXQ)rs;kBXaP7z3>YQdPA-t*$K!<xv%Xa)tK2zuZ4 zzFtI7hd^fW>lI9LEpVNRgQ$&+u-$jpkVS(lG+9CA25k&l9Sgg`HjC-G%bx|<0+cdu z4!-LV)9`Jc8W!Rg_d^%8EG$bj4Qs8xS)1fK<DxtBAkbJhgTg@)`LDpIDO(RIik)xT zs;sK@4wNljjZbYYm)^(8HuvV!ipvH>2U)j1l%R7@uU5`m*kAtCy}Y1<n5_R4IkDPv zkoJV~$nF=h*5^KykI_%iT(KYTy^8Y436f!#DkIsFHU{PPw7ymgk@b!B(BVJPufiXt z3UPoaQnCws@1|?41mhOufW-`{`GIGDT78G(FakWNx4jfDQ<dO{#GeeKqXo)<qi?`k z87&=T!iMyMuhZO@yoUuv*VyUvr&w~(v%wTpz}fw|hBtfluBp;+A6XXb=r~$~R&a(c zNw3r*A4uSGX2#klVPZIzUZNLuWD(iv0|sz-Z4Rbf8^1P5?2G9N?BB|tpQ=h}+J6bR zF(8r%Y^kZ1<3X9x@EQL??w^CLK>Fz?^R9KyDP!Dk{DCY9hOLNvVo3k*s2K1W43{e$ zZi$xnG_vqFec7&xe5LAzq^aiuwBFCXK~9y)8~;^gBemC<1kLevr+S3yuc6tlFIK|p z672SI4kp|$J~CMqsgkyT)%RoozSMi@Gu`vI@vGq?Z$7RPLwGVuodHXv!GH7p7z~0o zsdCBK*bBx_()rD%Yi(tU+u{Mp(CdnhZ%zr2qFds-7_!1f-sycD4tKaMyR6g)qYFAZ zy0T7%)cRxXPA~14`!5ay*)HRp22P7y#*dW0R4#E#6#;+Lp6$)-x!AVheRp{zbH-I) zX;wvRvgX7MDjv-S^Q7g82L(O4fwd}d%47SS8Rx=pigw}mcv5&A)IT`(U1&WuWVL&r z6h70=B2513S+JXfq>Ktdd2bDM4&~8M=1wCWBV2d_8TErEFN$(TKpc5{9LFQ6g;|6s zgKrSOt|vj;j+edJA&Qd19->^)aekK3R2R_nu9!UOh5S(!fK;FEU3>@%I-C(VRr`fj zA-{^Kc@{-JI27p<_WNqRFV2-J(rWiHpBN=N=Hr-uUaS5z{Lp1<1Ol`f3A#ijmK)YY zB;{ER%|+K93snCniiiQ|Zl8$vlLzS2%R2tiE3-G=TV?clYf`_9zrCYAXR6OCcg`_E z>||7`HRZf2R+t!nsU%Q=N5(J(=}#u)^z5nFX?*PRRopWp2H>095)OdH7p2&t<q6Hg zS{b6O+5*p9+y$1s23U?)BgmteJ0u@x5$Y?8ieV^FCi&GdmvZ-#9z~fG^_kE0sH_@P zx%Kh{YVv{&uRBgtu~X_<MCjYaG-@k43St>iZOp1Nw-5$ZW(D)z*xX=YyVekW49}4c z>u8&;%52opZWP4k?(!~&{kwx~Y;A9nk|)G0(Hr{dO+?eWNx7;u9pwt!$hoW?a;gq? za_gjq;}Ezsy_6KAgfSnxH!qd)SbU>-Dsj7aM%M~8AnAMdmx|yibG*WRju8tuLqXMZ zrq-s!<1+-)85Z`;?iZgmEkVHVf{)fZ0G#Ud6k!QJipQOe(<6eb#y;y_{}juqNN7;6 zcMYAVD$^UC<_rV<4u$#z>D|1tINT>n6XF@f`%i`Isfu$GoEd4-|LsSi>O~jB*D3Fr zd3E`N?Eg$ww2D=^?u=rNvfgO7cMcOO(pHtcvW_K5=*oqKZrO9nH}Jz6U0HfXp0F#A z<>18{uP{n#N-vyiX{NT4KYclL2X)rG*q<|U<gGf@jxyM@1)g{5Dcqf!Ei{e@dntko zc53K7@mjbIOSGCj3_ER~y}#PZZ0e1}x1a)ip0^44L63-2-v2-u#)FOLLS2K&HpSu# z={9a^ROGin6R~Qc1v-1vn7uwa&Qav+J#B>=U((QBZYHB%Q$X|Z6D{%YZY)cQ#RbwU zM9vzLnBpG@=;-1%OoJM^^WCJ8J?pF+k)o6f{8$5NNcXNMe#r?Pu?}bSaA-`I`2GHa zAv0Ooz?e53bp<y$s}Q10JEcoG>i2!W{-N-ICnH2cR@UPBY;UImrsanQTYdMdA!~jP zSMM!7>QE}%VTMMISbbF=NMtDqy}O1as}%A|Z!+qIY^JEaMz+pv+V?!2qCv3CO2kWW zVwqi<$vN`YX;eZzgtL9A%4IY$wERp3HaF*(DziG~4Ni8vNx3Jc1yuLv*L$UclcZ=T z@#B-+jOtUKoRP7D=Rb3;+$2Z&ZeFkV#)$d!KiXAz343}z>tL~LH^`7~Iy5g(Xw9b* zmvvt=ftF`GcAh0DC)n`i<fXU>p!Rrn^lfc$yy$p{zizNt1L14VaKpygtyv-Ij8Q>m z+z<U*%)ebS4;L-YY7~D*phC@#3uM_?X@4{c`i6Lu_GSj9gu@`?^sCRX0p(Nq`{zIH zoYEcO-N}-F6W*UQf;39id0E;nHUYjwG43%A&B~tgUO#Mc7KZ<jkBZH<Fvgz%^<R?0 zeak6!!Az?qKDx3JBu(6&`sve?I({6l@%6sc8N8TJfH2_AcJ22AjGvXC@UT=#brj)3 zi`T|xCkgG~L-5drb8LnmJ>DQo?rCu`m-Bq1bK<y?(OxB^l!pNMWEIb2ZtdiwjRDsl zK3Mz924vX%`&~?lmb$xtDcv-s@!BvlV4djw!^}RD1LlU>Az5MU%P4#0kUz>X07g`G zJ(aY>?_UL&ukKb?1JRWAzH?%Fbl$Br%tDpl2uS~{J_~6<1*<tlUSA^gPMOfl^zznz z*k$Yw>V^eHcxt}M1M-(VjBH8;qlLuRMXCwK9!sra1}Wi};dF62i||Aii29gqd*J~B zWx{QgHf}LT&`TZ#<{1WoX<o5+y{4nMNIM{B5K~d(AAxadX7PRgGNfqT^eXaE&<3fE zDvj2-3O_nNyd&gV{SKd0JH#~60=lR&0>^#k1~Ca?>&+vyBW9^m9HZm6G=}gt*G5oX znW}3IjCf16y&ulNY_5IWinv(GOp?D=AC+r=LbsYbe>?r>+o@^U@=!nB0MScW0a3{H zdHVK9&T#p7(V6S|ofGRz=9QqscCnXez%#aPmzF31K7G8R12K(U&qq>(&hBo9CllpG z{4b{J+p7nbm@6f=Q3Iey#3$rm%;EpblhDX48!)6CDq;J6z34NhadEKZj*MN`1ktm3 z&mQfTmFyNJ4Vi;!N_CL`8DU`5fe6)v*JyM&W#@&kWlJ^yf6&b!iI72`1fJf$FUWRq z*mk1(8JWZt?8^OA34oJUlwfAt-5p5+{lo$4%eLiwP6QqAOiSZ>Kp>w;i4$tzBN3T! z|GNZMY2ALkPx%9bEi7(QlJA#V-}y}C{CKJXfW^($JhX}B!bmcX@t=QGSEnUC7R{w+ z^kp`r0x|&jn(0C{Q%ZZ&HPG_<$J76A8~7(dfq9|555CWYii*nmOKJ7$dk}rg-T6XO zrpV377<97SATct1F2EAwwL9PBx<mX7`S`)hz({!})VntkNeUU{^0&XHUAQ`sI)??u z2($jRfrmJgIS-GzNdmu)KJYWI&-TwRzHW~KJ9Ixo#GHPy37)sZKXJi;n9;W5M0R3j z1mX<E-4KHgibzdUSjjiwocVK=OEG~flB{2qQCwR3Cnonnsl>eI$AZ6Gu#**t`v6(z zEfNqwH}t`Fp3y&5S7dq4wWLk|4%e%^uOTq)JIi0v2d#WQ(Y^Yrr9J7qQ#XO((j&}( z^lvWtuhhsDc(k9HGMT!N2de>aOg1*Q+Ao$T?4hb_A|d?zH|PL4I>BjQwIGAB9o!Wv zG{AoA;pKbyX2#0>jpD8C4}J?`!sII4EVdd10(k#z<I5zQPkGx8HY(Lk|CnB**noF} za9{ue#bBy&_B|hm0pNq(k|fCopaX$khnojFn|S=cjl2B7Kj87NiTiISOu-L7LI8~y Lx+=BLUw-%>(J$+( literal 3276 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be61389733f2e5331c08db8ca245268610ccb..e85f014180ccfa3df01d669e08620973393bc259 100644 GIT binary patch delta 915 zcmV;E18n@23!Vp%8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xJ z#a~lPRazA6AmWgrI$01Eanvdlp+cw?T6HkF^b49aBq=VAf@{ISkHxBki?gl{u7V)= z0pjT7r060g{x2!Ci1pyOAMfrx?%n}Hz05SLYaGyY+e{_mVt*#PDh6K>L>T=Dphsqw zF(*k$bR1vz@bUF7#<Mz~`*VcVoW%g2NIc67(<a^^p5C+#&ilj>R+LrZbK)_RE=c^y zb;aX1&IOkRo*6OIsd?fEu~=whrHxt9)QG2uqpGG;zL4=)<-EmND_2?bp8SQOoW8Qm zb(+ISU=d4@Ab&zZ6(y8mBSx!EiiH&I$2<6kT)#vvg<K^tax9<%4YKP8|AXJ%TKUNd zFDVoU0xyp9F#-g4fkw@7zK<QJaRT_Cfh)b`uhfB=Ptt2GEqVm>Z37qAElt@2E_Z;z zCqp)6SMt*o@_FF>jJ_!g4BP^}YhG{7eVjf3Y3eF@1AiPG0;2`WUhnYk?)KjPJ=5&( z2kcmKaYeY=jQ{`v)k#D_RCocUQ@gHdQ4}56B;py7NIc`&_yCQ{2PhPXLL*VAG!z=O zs5KgeLR1>1L?aS9BGD+sBOwxm#9Q1uSF-nc+<Wf1$r;UFd#y3YoO7(Xxbyk^!Vkdx zB=}o_&3|Tt)oKN$QVFZo3W-GWe*(MR4$Wp0r_%{erxWY-8q4JpE|&`ig8?Fu=<fp8 z>lLk53+;9r_xl}oyInZDTrL=mMp!Hs@OV5>tJQA;uh$E`UXNux9*;uMY&L_<W`jzl z6884{J^KAVj7B4?j+@Wt$mjE{Y_r)6u~;mSpMTFMI-L&OZZ{kb$A>d&?{>R^t5hmX zkwE1US+Ca%eLfx!jK^bEjo0gCW)g?@f};hG-!p;Xa0s1F$4KNpkHg`BTCE0?$%Je+ zD<t3|kq8=%2FuQ4yWO&?h)hP(|70>@CQqkRghC-iqfw+%DR5e?7WH}^rBaD~sc+s` z5`U%ZqxE|bXD}GBb9M_FjRv_~4&iVZg+c+{ZdWMrb1?anPNxwJ2AQcJ=70vJTrRUY zC>D#L8{#3AN)eC8kxV9`P$-1`KPHhf2Lb{3{eD!dRVL{3`H;zEV6j*})4zTTGMNmC pL;|r`405^rE86$3@n6A&Ut0#p@f^_w=nMb=002ovPDHLkV1iY&s4f5i literal 1429 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a><YADU|^aU;1l8sRFH7;2N1=B zQQV0iv0xN^;z#sxC`vr}6NoU?!~oTRQPlAta5Zq#;6?$JK~=((;W7=P3}^zxG`N{) zZUedys1hg$u?tff&}tw`Joy9Yut*q!D29`8Ga>GbI`Jdw*pG<g2rUo=$RxyAi0zQ@ zK{g*#CB!bc_u+~|kN*O?3St_>cA%L+*Q#&*YQOJ$_%U#(BDn<O1)%wG2LRc++rJs_ z{ARTCo88{;VIVC(fy&@kKq3(+!cg`Os0N4(foRuvi_Pzx*E}+qe75q_|L~(AaRZ1H zSXO7vE8*g;>``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44<vP<D2@r*P^96AbR1lK<$hXvw`IQ|Nj*t=9vH!<*kw+ zKTrZ^aK8WT>yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE z<A9vK72<4{``^ETd}9%0zzx*KnB?v5a_b$lS`(1NS>O>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;<dAc};NL)@%U|@7pOA`w-E93Li%QKkYKD|HQA;2TTCB#SO z$n*(QCQX|-b#iz>NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9Qzk<mEnd&1#RGiQN30 zVeL%o{C`cY-D3KYyK;VZO-*PwUK6!9?d`6w(q?(L_M~b@B;5WcYhC{K-sbZA|CrhN z8(sP2Evw%A=$yRVZ@y(U-=dd)TDzy~#qX?o`>S_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..bfb090fc604d253944fcabee1e019ee460c70201 100644 GIT binary patch literal 10085 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 literal 5933 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a652864769b250ed58097f2f6270b393f751f..877b87dc389f3f83852a0eaf258bbeea9122221c 100644 GIT binary patch delta 1424 zcmV;B1#kM>384#+BYy#eX+uL$Nkc;*aB^>EX>4Tx0C=2zkv&MmKpe$iQ%hA^6zm}4 zkfAzR5EXIMDionYs1;guFuC*#nlvOSE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JW zDYS_7;J6>}?mh0_0YbgZG^=YI&~)2OCE{WxyDA1>5kwgM2!EhQW|lE0NlA1ZU-$6w z^)AM<I-mP<gw>qI0G~)a%M8;d-XNadv<=St#1U4MRpN8vF_SJx{K$31<2TL)mj#{~ zG1IAe;s~)=Xk(>~S<%#pr--Afrc=I<@mS@&#aSy?S@WL!g`u3jvdndw!$@EeOOPN! zK@}yGVIxMXPJfDp6z#`5_=jA-L@tF~B`|U<paKoD>j(dX-`!gI$q6qh6bAw?j`J}B z1b2Z(&2heu9j9>u_@99*z2&deftgRzYb`B$1oUkK7uPLK*#j<jfWaq2Hf2}x(-iV~ z;QfrgDGLnT0=;WqZ_Rz2J^*RzDtQAO90H>S%3kmA?tkv~-u^w)?C%HcSaNYixY~^X z00Y=bL_t(|0qs{=C}v?8esJuRec#taaxcwDjZ%u#Fg2q@8C(#GJC`UUxp5<z6uFQT zV&=vL5xF2)BeJjA_hruWO~3j19sjY+xS8k1a=!Du&-cF1`@G+gPEJm4@P?4yGWdT2 zuR>sNZ+{Q_`}?rAwuX(3&A%hCxw(n)@o`*ST!_DKZf@Y{=!l@8AXr#f{5=B4$H(aI z?#95t0AgZdU}$J4{%>GlfUT`9Y;SMF%gYPCzP^wo{oAW1aCv!&zP>(mb#)06mX?;c z!L`9mEG#Twb!7$q0ReDxa}(sAO@JGYjg6tLt$$6VU4zrpQ(RwPKS+W&mrFA-F@eR! zMFa;2!`9aJ=>(RRmeAVTikX=iol{(0T?s<Y&d!e>;PCJe!^6XHa&kgoU?9xR&Gi%5 z+ucQbM+b(6hC~LGQaCj=C4{Gx1#N6>YzU1|A3i=lxB9q4fOYQa=@BA3KR?&IDO+%K zbbl1#;o+)Cuw?V|^H^P7MS!di7Z;a51p52?(a_L<v$Hc;Sy}1bOas>V;NSpZVPT4O zz7NJ=Htg-~e-o&vs6c&v{p|u*S6BG^`@_`KRMm#Na>;sod*R{Xfsl|8n3<U=uBBno zD>P&!3QapW!4$D(-rnA#W=2Lvs)%qezJJRA^a1VnjE#*&+V8iew6?bP;AT|$+}s>i z<bLLhRPkf|to<I_k6sWO8Y;5s>FEhSKfec!Ng|`il+J#q576g%GNw=sj*gDd+1V)! ztEi|*(G?{!WZfnwCm-FAbsiiXguA=DIE{&kiQ-!J{`B-Tc6N5e!IF}ako@r@vVU`O zbOx5h1-ZGo$jZu6b+YB<Wvs8SiyBjO+U>`|#IkXXc2-%I#-=88$QjAb&PH-_@?9T@ zii$#BULLBds#KAnU(g*Hi0)=*XD5DVa6VH4eWSFr6wS@eqAZb-k$-$3BO^nUqP)CZ zY@`MpN+Tm9LgAXRtOm2Qv-mD|9)IKAQ3e^cw6vf}PFGD$jW8HBcuH`#bzxy4qNAfl ziFBmN)1;=RB0fGI)z#I45H0kj;3eg2W+py=`Ep0ViqnuzRZ>!dPbn!VEh`fppvDL8 zgYP6HBnY89I5_A$*Lxobl4p5cT^)Y@_<?Vgl|pQ45fKrA;^Ja4CzPP-Mt{Upe29(3 zyZ7&fZP(V;LjIspT+cf~ettgU;^OpX?MZh+4yR1Hj){qhsFbtO-rlbBJS{B^>FMb@ z7mrsVpaJJXnXHQxSs(mKtB#d3R8UYLhRsv(qVEFMxUsQOTrz0S&%uj7OlZZ=4gM+w eUWzv-@C)B~n#-8vW6l5o00{s|MNUMnLSTXy*2F#l delta 1224 zcmV;(1ULJk3)=~hBYyw{XF*Lt006O%3;baP0000WV@Og>004R>004l5008;`004mK z004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU z3ljkVnw%H_000McNliru;|T=_G7A!Wjg9~S1IkH6K~z}7t$&wYXdG1($A4#b8}k9i z6oeKZ#P&&qlI)X$Amqiq`Dz7CgPVW|f)IT2t%#_hFMZ4g#MY*dRbMoOAcdADh?Jrw zCEB!!RQn+{A3NPmcIWtT=T7d-?(EKPdtsTGd*`0t|D1F0xjRCaZEUFusC^s)&Zu%+ zA+Vk5UX>hhLw{icSULY%F97*&;3t===sx^e;B(*v;H6Fy?x36!SaG}4C$xZrBKz>K zfXOzt)_@|yd~%>80pszoTlfT6J*n_XV8KrPHVl790t_6)usVmE$uT%&Tmu%~spsOt zfpf3+HoVj}0$Jgzv1yU#w!0a;t$=Pl$MIZRz*`DPgMTY3;4DQq3dx)UXIsEi1#}7z z!{0I7M(C-+?RXYAK+b(Q>FjesGKKv~;IRmdEme^gMW$)awK449bO4V8jKw^U!&AJD z!3A>y#SWkFrlBuvbMeC{8Xi>mk~Ui{>3Q9}{1R}K)J#S8t^@aNr&|g8fiFVCX<$gK zb~ToV6@O-cW6^QkD8mCT0w1}Z$x__ZYp2S^;K4q87AQ2#i(kJ7J_hb3RoUhPp8zFQ z1GT>-yvKtLU&%=vu1Rtn{sC||DKZuTRW^aC@UiF^SOX>n7HSGN{|itU-r8e!ATLef zj)n&KoO2>N+prROt1t*mhazrc7Ue=+;pSEVFn<ip_IMNy^$GoXX$sdi@P?efceUag zcrHxI&%k^V4ipqP;KAWOVIU_R!y5zuyc>#e#<i}nvK1g9T%84{K#vE9dxe3#DP>vV zO$8)HlxuLi9DsqAfRRLwA&P{@Kwd7Y>W5k2sRG<q_{q1op8}^+D<eYn@4J=q>gxEx zLx0a-xOVz$dS5S4IXm9cekK87u^9@hWU(21XZ<h!S^kAR-~UrYCV|BuFmiX#zn^M9 z)1Oi?A%BF~qNNC|2423i!Tl@C)ON}Ua#U4kRCQEB_YS->+r5Ba-|Tnuy$MXInqq|r z{@OYZFI}a+y%P#mfFr8Rs4{9N=1-KG+JA4G0^3HywO~H8WDs<^ytZC^aCw>C-3me8 zLqnIHdU6X>Ll!mctj)zUm{|b8@Q+5s-u4a}1cMv%i_^Q0{UQl)Bot89Tfl@Wi*~Gj zVP8Z?Uq;uC_uCIm9<Bbd>2GaLAyRVB42{D9j$ls8QTsjVy*(4z%w>h!cJJJ0Y=1xV zGLIiTM7&%TxC9&oM%0O@4S9GO0^R^_VA6vA_Ct390BYC2BY*5I`&d=D0=x*kpsHoe z$?B_`E8r{I2fU)nW#C_aHv;|#93kSZ<%JtK0000bbVXQnWMOn=I%9HWVRU5xGB7bY zEigGPFf~*$H99pgIx#UVFgH3dFhQU!b8P?s03~!qSaf7zbY(hiZ)9m^c>ppnF*z+T mIV~_XR53L=H846cF)c7RIxsNNam&#F0000<MNUMnLSTZ9Sr(fB diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a726136a47ed24125c7efc79d68a4a01961b4..f24fbe65de8627a88836014c48b7f8156a627594 100644 GIT binary patch literal 26089 zcmeEt_dlEO_x~L;F=Hz&iLGkSS|L)kN?RTFrnOgV7EwX0nk_{sT1u%sYVXlfRPDWa z*?aT3z5k5w<NH%QB-gmF`#R@5&+|M_*hB66lw>Sq002;`t6?4k00H<X0RSZdKlZ); zodN&^XRD&}P+dg@;o|0OW9wiI0A8tnu?lJ}N;lg~bv2(F(61xrVlrg);S8?-ZqsL= zzcCoR7WuhPo-Dz|FZAxMeN$6(TV7B^=?|(OZxP3kxs`=4D;(UH!u)0UgPAJ!txbi^ zDG0i*TjIO@&AwOTTHJFMJT<pTOldI87tqi5Z9a(ds;j@cX?nE1b0E}SDw}@hjvGvE z1Jc#xe^U(bOd-;y$|(PwwbDFGcsl1kCmJuLcHa5AZC?5haUyZr4bg#I?>T6(-b>4` zQTqM=@HzObylmb2m>s>v+BSWG=lXkT=fN4N^(Eb}lUvlxXf$IGXk`ZO1anT+@7m$P z^cx*=$Du|I7@=SzZV|UTp9v^S)<~~{rY8+fx?sZm^rQsB@AgCqh&%&TdtB?IZ{@fW z9H$2g)GZeN3;d|_*IrQ2hiKoCkZD-(!6>1SC$@=KMaug5?g0I+_sm6PJwG|c1VNp> ziF1!%sv=<f-)(Qgn>Puvh<}-EZEtQKPF*SttZ$L7>$$n!s~U>}04sz#M)`>sWc?Fq z($mGkip0OZTYe#8`?m==%%T`1Rp|^R(l5gvS%!%gbJ^v86a9cc9j$4iWw>K@PdQI* ztIjiF>R|dPE|EGa_~YFd+M~UG#&Y7RO)fU+7xio2&jssWuMHJGfFg15;!Ll+X)GKK zMTWD%0Pr6iA!@+&7or#fCGZoEqy@k)F%+@D^^Yv@MAtv2QQ-n`a5SEo1A+iQTjBqI z=>J2<M7)G0q}ncQ3uhQVQ+RW6FpN6(G(Xy$^u79XbYYCd>|Wo6b!p}uLTdTG!cTIZ zV@o|Lj>|ucrCy9l7?wMIdM{u)b)~)f_iMw!^hWq4RY1V+4}zDaOFhZ@Kgk&K9X^U( zzPL;&tUvuFjKuUKz&mvO=xu+fpkd`$>gs^wNjSr^YNMZZA-;zDvJR7bug5DKr%$hD z64-_OrF;(^<cjn}k8ZAy6u10(FKGWDS)%wqNjtqTTJ8xIgeU^zcD=nf!pwdY@~VDI z-5)Ewb}YW@6=ym|Q^;iBW%iXpoYrt_d^m5qD)dt!#tzS{eUiKI#Wej`Im;c$-eGba zKRp<#u)P0$x@dMPT|f(gnfu?25oY;JLPjqw+arrv`g3m{`R&ZL9i^>PPgr)+?QG!+ z&smvI2{qrBd@Ox2j#H1%fBOB|vl<Ua0mF)XzhGz#9L`~WZCa^j_(Wk-Cw9O6SXLA5 z^Qsjj4ib0CmKgyDGO$10m&eXe_8bS2B|TbqI(*+X4U<tqW8v54_UFIB?~dCuP0uQw zj8O-cxTBs4VY}nX9q6{=uP3<{ocG_A_1F)O_4}P&K99?Qb6kO|(YSYZ2Q0pSKC$io zpU&`tT&vP(-c!fU801i$ujhCDQk%T#IQWey(5OX;EUW#_JPfN}<vvJPoCMcO6b<uO z{Pn&RM#tl&h>m7c$T#33qvCtJjf0PdT#rB|tUrp^xiNLTGe0>PEvKLx8GwC5L`#Tb zl^dcO`8jZUzW(A_2?hp(Aa?`70s7kbuk-(zRyuuBxY!>Us(x+ndGsQA_DZOYGt0SO z(qTlz{@=G)UokkztS2M-Tm&>JEB><&!v)4QLzf4I0YA@b?T3Hp+ah-C9#)Uk`}ux< zk{kE_j=q>cfDV#2_<9AXklU{#DW{2-bi7(cl~X>ylcnr!e*XNaZ%?neX^WAtXoY@% zR*+n7H~RNiqOkK<F_Bgz<`H-UF^qw;ynh;OdRS024Lj|u6%M0fYv373zU)KTM_tv= zEPmFU?O^w_qRg7UTlj#Ln%u>?<ieAQ0|lOPEHREp!Su~eu%KR1<5a+v-`Vkebb;+a z_E3$d^LNt*|9lDrI>+Mu?F!jy_HkbAiScnD8faZqgzi{g4{yRLE3isA6y0@S8hMfK zXVZ(is+Dw}nutPm`E#|mbsdu)``Mayx1w*eRZ+kG@88ZPWXj<9d!dD=4sL`A^u1#A zBx?`<-WR;uo^9EvKie#oavakGt!8pt(tfBriY-9+PspD-2!p^_qN%^F=7&43u1|`M z?ta&8sDFR^>1{_=c37+fc*i<LWP04qyR#pp*wFE6?`J4j9fisU#9c5KYNGsZR5{l| zCdBgy;aa6bg_X4hhQ^4e1qM&202<Bfv2YQy)b-p&b_JiZo@B}2ipMjA(T>A;Pk%=- zGwnFp%~_vcu|~?X#cB-Zd3Vi#YZ%eTAi@%X9WIPf3;^dK{M<^(Bo*fO#_wS8d4bQt ze`yn;kH}TrZ$J5FcK7bxv~`~Os}`3AxZ1>T27zZ@X+QuE?lDL8aE-^N)`1~C$LjS_ zw!0W`hbVaUAq?wnq7o0D{xCW*3}Jt(Jiibv0zV@)VfaPDpYr8ZjIUqg596V1%Z|E& z_~6cG;QrNA(5dXwl9n!^^=Cge@FpE~4TZDR+b@~{C?O_@V)&A7mvB2dhP_Bm_QE-O zlU+SwffUa8&>!2WdOey`=5^f1dbdRlR(Y=?-qW7cy8{}D{Fc`1XKTZvhp84S>~C&d zy`|YDta?^pusA^SGhOiL75zkR5w=t8dOV%Td26Pr|A{r~q&G#n*Hl<HyIbe7l8fN+ z{Ev7$3&dy~k+P7_o$VX1wx%YlYHHr|@s$vPul}(Xe(!1-byeII&pW=m`1_(vUF#~R zamHPiTcYubIt<GpirWnLA^=^elM3p*&+f^08^Q7N(!!@wd?mp31VpOrSy4>^TXjdZ zWzk;ZIlUL6myo%Ctyk9P@hWfvLr$bfKtIQLiDhWH-Qcelq1&wVA0t6y;|B<Jrv2y6 zvs9zp=5HAcZwjCLMZ&`ZI8JWDVSyI0FSDpy`Z825>X#Q6bEn^S>Vc2Rhn9KZW@n|> z8zEf3qHc3O`nxw%py5gG<2JkD9|Z#eG&I@#yLW%+{rtPsn{0hc*0X{nishu2ff4=@ z2QGOJ2q9F_QJ=0mnpzX}jk^??AQnAu?1~ri$Xszn!atpAKL2JmVAEYRXg}n2wn)Yx zz<9hzpZN{+-<eRol9#cQZU=+U^6j3YikXJ2yVc{nUrqG6p>h~&A#2Pn=d+<hpjUBj z$2f(;SKoo_sS~u+y3VVn|2tZD@Ab}1<L<oI%^$xWz@7P(6e>0*qFa4(vYc+%o2F1F zp^H}fA`ZGNu9!Z>4}V3o_Yc;FdnYf-Ip@<mk3}{EC?d75R!O~Y#PjG>PzpRBd&HZz z(u{aQ3ysyjzPJF|jXBElC9iK2F_RbR&-N(R0Oz%-X87slz@Dl4i`7tGm};-ed)e0x zt$3QL$4NX7a1Pth6>+-Nx8O#D5nOLv=W_D5YWM!^_0;R;Xv3t>&n{D#>1ct)OC8g< z4^!kke}JK~tJ-7Zzs!PHtABlov*5WP$iovf+rERjtqm7e(zJw*3tXK<4+r!9-X?Y7 z6|XTN;S!&@tDOJfqgb2A`pE1F<SQ8A+!lYI^&Z>}e}V)Zr=q#a@<N@@fnD?-3Uuqw z<Yia;V+;AD+=&3rKx-MX(7uH*`vpABv-R(V`Mz6IX`Q*MXpX1gGZ&}hOwK@L;68r^ z`pdJM-f{KeC5NmKC5w+lN;Z%4UfHtWmhf2Dv;X@gjMshF+b*({&knpq$1ssi=E*_u z4CB6PZcG!BjO?!t)RCm?2Hc*aUJT%Hhq<}7N8T`zRm(*5(pu1(^CA)91mIQo$C@3h zZyVNHg;BD}^eLV!(0zZF^G@1VeGryP^_DA+A0o0hTZ#h%o*sDB^VR3u(}W+|>frC2 zu*GU!FU*D?v@pB-=|PHAkK)C?u3@#q$3eTk#*~vUc9yCMA}FO0WDSA{+jmpiBY!Yo ze;|2jQU5><zJ1f~PmmfTcsbSydHsKI_lKqH-dHr8Z;#rOar`RLRomO!suX);+sNCq zV`cwpu{}ykf%a1Z#pB1}F93Q*c$lhO82qrOE14_59E|Wq<}D8WnfDGiYqortP*St6 z+&Vabzl(MFR$3oYhw1vsGt!3^8cEpQzu~bmBX#zPioUTTTmU8?7LtCtu1?20mQ-x% z*!Nl4X+OTdt*~g@Ulh(!1Iu8O)f6(VKTi>BUOD&x29Q-T>jg?UOqoEbmC@+JA>V30 zn&s|a9V4rgoUGYtCK%QW-b}znJoj;|*fGWN-48}@Z$&*a8UzwS7DR-_-(vQUT*-U- zeSI;}^f38ywDiSUSj2AiE6H;R<%cjPMMK1C_is$^QsUaM@9IqIUH3ObL?;kA&;NPz z8{yy#l>bZ;`JVn7x|fp+hQ%Ri?YMKkTuyR1U4)@gD>Y83D~{U{ygw!xX!}{h%okOL zg(i87aAGvL;kSKlTch{cioeDJGxWhshyj-WzNtq-y%8zW&J~3mND-TE=I^G3X@gNe z;6t<)0bl^qr)1?|<q^6JBqSX>|LN}NS*9%ZKnJhYPN&C-1kao#rQABA=aY1=tmo#s zJBLTEfzmoDEx;jQ?%rx#?J|pw-iud%pn3p_Meu$2Pns7)Fst8DQ*IwEja{yHPZWAq zZ*=!{1U(9TvQAli=p3819<}RZsg9UOpD1GpV+0!_)NU$6MKXOx`ZAuCKjvPWsH<77 z?_@>PQ^SB*NusRdzna3MGfhDagJRD~bX&s+!x>6nvr3O8^=Dm!4K_vuwJKU^Jp!Dc zWvS<ry>snONKdNdZ_D%1s?i;}Tr#C~YP`uVq3XMvt9!&_6^<$8YiN23aPs~bng%d* z`JQc5Zp=rc4&$jr`y5nXPjjI9G?G3vd$qmfpUAh>70C9cZ3Zt=$XgIAq23(3_#V%O z7MQn$C~7KD@fB;*<G=uPrni<m-(d)*0|m<QJkPWqe6;}aWhbA=xz(Ods!acn+Fgyc zzh5IH!=y@NeoFo%l?ciRe3a_%{K%^lmupm-{UBZP_t5<{K56M;ck6$~VrgmszARLa zCqzg~P409hYoluZU8xGy-PS1ciAY3)@W&}AfZWwgl2H>_*N=t29RBfX@cZ-L6pP&0 zhqd5)wWVaGZGY!~@lWR3KY5JV=Ct|1$r_}OzSs@sjpg49RMy+0=ohdA6AF)o*!w<P zSkS*|LR-P;8v6l9Qzx<AG<j7RaPeD*zv^t>?wGKN5P>m=1%)sND3~`BjaS)39faeQ z-m5Xsn1`Pk{PqA($e_svBTlEyZFOYoyHWnE!Qan*C|}<odPPv9igW?^RvD;9FRv@n zmuFD<BZ^JD-JK9l41yXk!KY;TrF}gC+~+W0r;Vw?tcjpzHiT8){$p&8*!eWi6D9CC zFnh1}9b)OLth#7>sXu!H@nRq_-f%2yh=X_D21jc&;myx&^_sKAo!0eyw}Pl^^Qo<l zuTuIb)N;gP??9VePha2}ElTenZ9dENa{c}^=N*wZBk0W^lYH!KEg7|9?$s1$mtvG+ z+nMj9cnLZxf7t<>J8{b27(X|(gLuC{Z+}_pmRz+o_8co>sbVEW3uJbh>t=lHDYG4z z=s8w?HD_AmQSdbHiMrFq1=hFqBCCegLOrEOjYr<}s~R-2Ud(9iw{XM$e-IpCub=j< z`yPQ3{f(+qS3!$?(Mu3glz?DZ@8G||y5Pc*fKI|jLPQf+csL<!m_gVnf>E#pJgt#x zN&Ea~J;~2<{Qa0<v;-eRiLh$2%={FnVhqKVp>v&eJkNt&2sh}<|H3MOqS3!JkHA$V z&4=2X)u1tDHEx5q3z@&^*Ld7vb+;A+O*!|W0j4(D;L>%z{Uj_sTM?v5o{Bv}LhFE) zcby7De)RV!%>y0fJgVS&3<ax<{YMGw9EXdPofP1iKQ^OlT+~@J$&L{;tmrllEn^*2 z``e1$;ibmH!#8u(h`l~*++Xv)&$X++7QrNZXk+}EBamGo!Hz3o34EnE+Fz$5P!sP( z2I1$NE$l5i?bT`&6EEZixU2@hJ>nd6zTcYEhk+_<BspJa+P-5FYkglMp5{MMPpg+u zB~ap`(7urKTrZmCR2>Ob3H4E7*FF<F??OHyz-@-1ztCd%pF*axIL1iZb&_VLfZaEp ztT9kE6u9w>CSdlztY=<eK&Y^tR~?5NzuKki3Zi_EQObkRKciVCu+44b42p&$?kLSU z=V*U2g7T@0)Pa^o%){r;aw5OeO1Bx8oyGnZNc^m(h@{3I7L4iLlci~XBJ%pbCprY1 zlQoq(tP^$i&%>ub0ULiamCTO2mKyf{$Q`9&RDP!Ou~bcKX<=SHdWyuzHU|R8#^*i( z>-e<cmT1(JA-QH*HX~??!p#=-tJx_N+h$nwTsogm)s+{J;3)X_Q(x0=qnNeBagq6l z#KdLJ`u7cSvRpu`f74uBC%o79@?=@zOkQi7E;pzbdlRS?m3S5hhuikDAw|4*t+uHD z78pvWOMiY*xdNg@6ihXZ<?=e%FbWy11S!3m(_sfw!MV4QTmne3V7S=LyKY~2ix1aF zb&;(c;IW@0GN#WPt}dLx{4I$ZXgZGAH=mXVem=g(Lt7nOzc*y~tia7$ZY4$~zY3=m z3uc=2#3aNjZ<W0*XOw(x8-c>b0x%)Sn@&E4Y`4dd7J(UPFa*>7xY|MAu)#Va>Udw* zelYJnyUc>E7HDP7Fj4<szuF^j+EP!vPJHS((w8i0pRF}15!(BX$^j36Snwm+6d-Sq zuF8RZ@zeVEhbc9!J)yWDBBgGa`kU6+L?L545bCG-9rxdV<{UjcoT(Xr_Ba6J^cz(< z%B20DVOk+Wv-q*fY5(!DGF!`^cki<K-!aL=rEL&#K;XI}EJm-^EGmvx`m;7-@3ZaD z1=K0TpJJBac@XHNi^bn%-Vz9{;q!bWTk#yN;)+|cdh?*Ii9mhY$)kysC}8F16zk^l zTkoHn*pjlpDJplGG|0K6;Ko7FY*?dg4%V3)iXuK0--muU4!rnN)?-*{5JW<m|M;th z5K+R`V;L^Mf+!@N_@Ir+bfo*GsIf~E@z7UICb{$NmccAJ@|nGRz(ZO*^x;!%Cu9A) z!<=w1=N&7zcf3458Oppz{{2xw@?azmVjftypU(t^VVN6OW9X1Q7T=cBC2Zp;Z_4z& zlbM4`pFi#&g`|rMeuNQzvs1BR-K4~0gdPNa5Hxiv&DJXZGg+<Qr3U=VeeS#kxxbF0 z1@yH<&J8P_UT5_ljM*2S5M+ARQUrL=#6c1T4Q;@1<SCu2Xc_Bhwh(czz22=SS;}%r zYrNXOu6*NP7mI|$N>`9qJr~g-H4J-)yL-`wfXPWb`ylp~_LgHGL6<iC4GvOuTsxSj zZzXVjdJLS!a<9iE#w+~JrWD?tV~vw#j^ot;s~hI@Tdkej!bT;nmkMUsh99Tf$Bnt{ z!b(vrlJZWsg|VGBZ~|ukUrDRTBJTkn0mJIDMG0r8PE&;|R3wqo2kdN4v=naxzi!T$ zVPT!k@8^Xc(nWYC<TPO~wa_DP2&Q7*Jn-e!{6d3_?D3mhpDxwO^L2CHMFeeowpPXh zoh~C2Ai1zAIg{>p`0+thB#N%WYeeAJaR<vUd3BNmCZ$#wk;^{HM6k$dQg$msIe?A@ z#(!tqkqZ!Go;&BN?UUNu>qS+9P|+90d|NN4-mAqHDh#lijh0vs>KeCF9V%(0*~a!j z!dgeNW8vH0ytD-Gh?GcZCoZ}N{I4#K>f)zC2r@bO+eVnEsQ;Rdkyf1eyi0(uqG>1R z#PP=(-`aTpL`m{b&(kfsc%YN-jlajv+~K`_kpS5@Rg~<a%^!pfmsC-$KXFi8D%=g? z67Y*0F-K+&p}1sXmGY8qDv<LyP8g_qP7WS7)M3f0it3i^KVc(-gp;oA2=<_5o74T( zG;(Yx-zF~`jWe8}2E>lnVLDcsXklTYO6FZws1^aDXSQi!0cAgQhkb~!s$^s&$X653 z{&qR^285hRXdlC;6GO|<j_Y4G$|pa3W{@fF#ph%W*&wkcqvlVM{+NGpdQkf7y}+63 zyZ&Jwp<TjW3RtFuS_X#_bSFxNCoK6u)cO}atAaz)f&9A2ccZTw4jAkmb(Xg~(^cj{ zlYRpaD!S@N8-sS3I2v$t)CHewz@*SR3ypV%3YKZU7k1`UeOCr@x`s|pKN`|D&`Jb| zsF^^{KPdlmFW=oR-Xo`f8dJ2v!7pA8<~{X~B`$cOf!~5}G$cIJy{i(;!1>pfE1^dX z_yS`Nn0R<`c61b9uWifG?-j8UM(4*&4+})&uXxHBvvXOjhZ>ICglofjSbe{P1DK6U z6YM6v=A$nCavy(veDMMEp_!z@eObGZY-OZ43$1}ZC-V9oLdj|~UVr}g-RAgku#yxK z62l>Kn6NYHvsPg8t~pl=Zxz(fyZ6&$V=Rq6DJq8rNlQEhG+SGLe^!oE2^mo)y|pk% zkRdBD5DW03Yl#;(C!LOYBiWq`Yj?vPqqVKXE;IR%wGtq<E4=fF9fmxlZnok2TM`<t zWyo!SYST>UT(MGyVL1@;2#KwaU|wB!zWckvr9ab|4e`_b9$6!yq|=1VT7On3uObZT z&$9cnsy&j$+L{CD=hB<@jN{Zj_wQHn$3$uXSL_T&Ce9gTSr3;|oX767`z`z!&XYZe zpr<AI+MOiY0(#(hhD|MGc=%)MU(9}l(*Z}u-&t-g$liKW^j^SFYQ7di3#em5vG!{p z1Qow6w{)(c4*uz0bF20vMdqps2o7Hu<y&S(j5Zd7g`Z@-X$!A#U3<nVXA@cevcI{- z&YZ=bXMK+rfbqOav;cmu#By$QU0xK}WvQmU?jekYQ3vCxtFCTvKhP2{Lo=Y)+1HmB z{yj7n_t}35^2gbdVTWgVpkG>5gW?+Ee}IpK@6!vuvEF)eKZ?b=C4{=t$J<*TVtWC{ zdj*+&-GQlVv#{R5T+X(dW}u0wkKWxBZFX~W7tFEMHP!JN`}_JT5#Zx`v&|L!=43Gu zq%kFWY5nG-q~-sZ7CKFqUCabSw<*NU$Y}aNriNWukOqOdG%I`|_%zR8@I#@Itzo5e zWnTZ$KJ7P<R5I|vasy<L9DgHJy8mfzrJmn*6Y0^rM6C{LL(tr!A}~+m2n;J6Rw;G% z&&gr~^rUe^@WFikyoOH7mwlfD-wQtfQ$JQG=Q#9%NC_firqjCu2Gr4!V#|#*-_4zc zdN6W>Rv#I>>;vZGQ&D2V!o9>l(yWnWs!;NL!^%G&?&!~5m};on->$qzUdg^XV-5No zUamKvS5|0x`k=o~t&Z<WHMvKT0y@vKt;ES=-{G|1LIb~QCXQ8rC6)VD#~>}UH5>S3 zoDzn;z+tF<L{Iq*e*l9k7!-HE<bnx|Z4kY_N)nTZ`;u0`6sx2i7kDB_^NXUiU(fRv zz-v#4cTlB{XKzf_B&G#WkQ8>I{>9&k<JUxSP*aej=DxZ4ad4+<v!2~9M2SNQ-`dE+ zVf&^fgwo{pyV;KK6B#O`0*rO_-|@j%qQ>Y((t8t21(j3R==_*o(g&5u|6`n|gq1?E z*B=g9s8S<>#xg_6U5dW2^8N&BJU2=#`p5noDgS;|`)!xds;iz<@#ci&-f~ZAH0p9h zHHyu#CX*Wal?P;ip>U3$X7*J>`NsC9TT_mfsI%^5Y5S!U$VjSHp(G5ug}vP(xi{sz zHQE|RXPi;I+I1WuLyG~XgzIw>)US6w7qyUSZ*1kOI9=5rPx7%}S%(UzR1yP>fTYWu zsB^<b#fxKSkRF-;T>FD<mE+I$c_~}A_NV_s*l9k~DkEuiIvX-xxvcbMOc_+UFaNm| zm-XOD9w7&uWKu@M7k^*ZUN+w%FKJ6IgW*?|qWyv{YeNMeQc~8+yJ;b4t`9CV3j$Ls zP0n?TluOB#4j;Emn)Jv43i-Me)cLlG&OW)5{|$fSC94!ytLR<Geskl!t_ATh09+Va ze(PavRZpsTht$31lr-@KYM$HbA|D8_tPyg-{4YUwzgK@@b?&aaxXCov(PXJAD1Lp? zZ_rL-F-VpC>q<kueksmt#)2PETW38#96xNj&#wPT_VwRP?kxC6A`>y$6j7oG7MoKG zHF@$_d67fBqt9OvpW|;??{<|{g7rO%X@Cj&xO16<X}_b%>$+8M5j{u_$U{K;D30a4 zZ&2wpX+(r89zo+58Ux>BCTs_C#yc}q3VQqR8<zh!NX|uMfdu%nX3BQgOs~c)`Krzr zOXxGVaSuZMZwMNcZ6ncDJi+j;7I(tT!l#)*>mBKD=H3%{5$gG#&=BYg0O-{84-xEs z+kqF$2g3!H9q;(r>k$|!bYz^<M=nw^?WD`4T@q-lQIT~dOPZwp2c7JVsk)lb@+0Y3 zg*76j&L=jU7G<kZ(o-uxx<tIyp|LmBz8Kf}ln;wkI!{l5ma@o8dq4r_@WzD&;WtHE z$!UsER#8CXw)>;3T9EuW+e^P%&!L^*K;tt5qo5E)kRdYl)snj|9VU)wfXD5n=L~iH zMF?x^mwy>RH(rVcK6`<z#!>Vwg)tL$nLZtMa42utA60yzLHREhX{U8-&D!>_d8|k> z>bJ>KlOPPN9E<IDPK}?h5J9a8WDx@mcU=9YlKFMBN3amW^N_y#`p{Sd^i8*^TJJPm z1oWF}Lw%x{Z5kF%9Q0b{xi)&->nE(zd{5;3gkkswKj0!>5}2=7-24FqnP53JuP04m z-0t7Ej0^?zmm3Qt55rpSK-`GjGULg1G_I>~q+iz)3geGMKQHa~BC9^T6NqU5Ueo?# zM^0l4yy3$p0h5ZS+bW)-bGyT;92lSGu{KVS;`1V4k+fUr`iVcZqL8KGV1+U`=CjY8 zaUI8+wxpBG@rzgz0Xfirz1M}y-7o4=ATf02cHvW~Kox@GB5*5hwnsgo%5YNxB?d#H zYww;lzoNlF5mP%*oW}%+1T1x5&2Yy`N%MBFj=bkL92!>Vz_^c*0}~fUsmHupNoMmM z(Ut1)yoM)ki{~VL>^IDdoX-iL(ntEf)S%72Ebellk(+&ykzPU+2*0PYO(KG~2g4>? zdrL$|SGG~vk8;Lf5%#92nsg18xeH{mLhMvY83SM*|2aEpKrsxXgSsfIRI+l{EA=U_ zL~`GFO9n(Lq8sJ7HpI0>@dS~mAF33n=O??17Xs2rg%0uwk8nv}q!7qBP56<-8X6kI zu9T=PB|G$`7Q*q4=q!kjr;xHTK+Q`>!j>Os7S{aR2U2z$|9vk!4CG85Qa|JzKsm5A zZS)50=}tyh<$k+;>8n99YIf}0`FR0NF=IDtK{)mtlwQQC@EJG%?x;2@wTbGG89hdu zLXai5)+tRy(~j5_eDuJ=-67em)nezD_W)GL#~_@FMWQoXC*2A3tW`Bi+eAf=T;i+v ziOo%W8|xi4!irtUlo3RtN?LJ5+^tv~7Ty&s8PeF1RS7be)Elw0-D4ew9jEcnwXFo? zkG#rm<OTDx?EKzKbEt`DBtS5~Zodf<=JEctUSJHJgfA~GRiCOaVPj_=fH^6YKpSZ# zP<k>GGE3lyMsr2a^jlK?{gV>W%8L$2pwz%eI1r4ht|Xt_xGBTG_1jrIy*b7W(ip~m zpwjC<5pXme(9kiTBITG&Yx#SLPV9Zx<0XYzKc!TJDxByS4Hy`Rl;6QS<T_X|Pzl?= zU(~c!(Oh(C&glNTzG7#0*WEe=)vl)$gTUvG;(2u{t`qpppUZ3wu%_*tpuovt0VRhJ z@?navnpL)CFj%Y+x<LGb5Dt06eK-7pt~n&u)@;u!jX!8(ys|JfNrl_s_PV+T5zrvQ zyH>MV{d@kp#<e>j`DOksm3l}OcS}(Qh(Uca<9I$@&$UUFhOQ1M@kBItQ6pY`<3V%K zH8##a-5f2=x#hExk@kuzw~YPXZ2-MHxzag<x@@I8ygb`FTl@A|3&BM=2SYrfK`Kd? ziukFcmGBERIN-g!1QF+*P&n_M&qx&x<EGmVVGbm&{%OMx%|B4le*~Zh2c8qIp>_Lz zHPd#Ix?Ah5XeVS+F=~&|jUpkj6U5iAfPLtOFVwne0X9Rag#XQo_)EOr`lnO-Nx_pY zs?D*%y^g~IMNQIh46v<f$)~=3)`7a5-Klx?NGX^YspQ}WlA&q2-3%78pu2zDbRQ@p zp#cgEE2j*l_%M0UmjAvjcW9<Mg`i?yL6p{SKEGw{mwKs^j~f4+V7WQ*Gi{<NA-k^? z1BNJ(<8^VbMZEypISvLK?X!ssztjCu!$%)}a6JXg&PBti<+rg1Thl1}r~oW=I_4|r z3%#~%G~~anX)Ti~;T-}G?)x`_5nti{XX6@=sRorb9eQG~Anz$E#bdK0v~o4dSDg+> z^<SO7>QkZqxuf&cIN-J!XJ))Uv)kM442Or;VY2NHo`$pbap*Z#OI?ETV{mU;n8W0z zn%9zqt%cxtMS0=jN7a-~u6qD()sJEexyjBu&#pvdwhx0LNZ^ofDid*|aaRaTHSG}d z8Z~k%`vI%)t$(L<VSq2=(vv5A&;L}jc_g6>LA;}JVECH}#so|xfaSmO6z6(dkVvWN zn4e3Hp_9Gk50X)AR4@B8gQ>kc|B*qFVqqePNYObjuCN}CR(@9cOoZ9Fx4+9?{3{JY zIk!f!Vgl|A0>Ju#Efw9lP~HAx&pPY52kH_NOv#FUr>+xKu4$h!HhhH_|A<tOm|63d z>;TfWLhVno_0q+cT&-=FwAs6{(`|byTmG#;J4#nQ34lIuQ+>y5-T(?J>Vy>IowdXo z<(gG7UREH2@STjKxclh7GU(gB{X*VpWZ^=EZgXA8m0h+{(?rULq+?KV=Oxx?hh`!` z+P3K}@!>Cl!jZuK+-L1qq2TYU%&TEPOzOt=2-O~g92>_;(Iu9Q+n|(#K&Hp`BFu00 zZsQDHN!yI)fB(AbR7WZtyj5vAfU3J|^~cR}=L$Ou@x{?+Tf${HE-Nv&1+^_!tRc)? zY1gcp0z=DT*u;9jo^~zT4kAwgM$+Mt*(5sIx({K8=DYX!mZ@^C9OOnA4FH!?Q1)QQ z4%YCa_qn?qpNCkZvuU+&re4wzaFLb3kR&dec5|OW=XcN{+d_9=TG^+Z?!)}Q&>W>T zVMVjw_?>}r#=?g^oyWrk&$m)O)a%<E6T)bKs{TKUM@GNHv@gXjiG!l>5hOe~5-;HH zdjwSp_Y@bV8Z9QoCViWjQP^aC5JWZGoxyIxv31*{@m%4+0x*>KRDda@M*LfL1A<-y zI9?4Hmtx&tO1WGn{D@DCsbnzi)L2485&1Gv4{e%NiAK*w$YM1{dYg08o!&x$A@)*D z_}r)y#T+!9can4G*k#6|#OgSI{$bNIK1d^4zse=!VwO4}W)0PZAi(R0tq|S&rP>-o z-%K!u$Ei_4Aw%p)h|mz?YfWol7d1Y}ywd}n+F-SRceuCbb?p6g###l}5=&x6AY)6a zz+a!<wc7R`v6H%xJfuvj;OX}dF9b?yh>sHB9CvYSw_eeHN#Rv}dwC1$`cMzC2en+K z@tESN)))WIvXKL=mto5%y8~1`x<f{UExC**&!e|&c-havF50DvyAQOHRUd%?!ov4g zvVH*Gl?CP+e841~725@0UUpv_EC0^^1zLzUI_e`J#~Sy)^*ZdopZhgzqenpFtzn$+ z1I~C#R;goku2x=9zkt~_yKpVu?4;$r^RUpQt}d!Jfjy?4$P;yPOTsj(jJl<8;SU;$ zjY^Rkjk-A1*ZvTF{6N|(>25483i_Nehh{?mpQOMY1ZEdHNu=+Y-|$}Gc{P0I@u=A; z4Te?WIn~{Tn7WN&8vE0HGCrWnyR}Wpz5KBXUMG&P3akv&vC^<XUW?Ol{KSoIgJQ2y zCet^wZxOHd6QqR{KBJ@J1He5^XsS3f_(@G?D-i&Tui6-%d-Z%#6J++4;*dgWb3w)J zl0H898fpx>{dTg_OXzQ8vpVe;3j{Okqx~7;Hr3bbBmFI3K-apPek=thaLmbIwe9B| zLoRPCulF_$!8Ol@I)0;=QBZXKZd8p^Wo+ll-<-Ia`p#m%96Xo6>7fJ%VG$O`LDEms zn~ro55GAR6HB@t$RmZzqCW1=uNpfh$$TcD4u|{v<Ggt|b5@#ZutbXn8jGgQ03O<V< zUHYf1^V&0DguHzDLgs`KnCJ_b^vW7^{>OJ?>pmCOY@LRnOzNvXU4fu2-xp#~9SGX# z);nrYYI08|=P^b6km($<t57Fh(U<iEf03iahFzZWzQ1`zfs8D>iSn<ltXc2N2uh?Q zL&oW>d|+uNfI_?pX$g^uwYBo;L%c1fBD;b>)hfpx^lMP6n%WZ!anQA7zHDTb2FU?- z30!}s!}BTZ+a|y7`gPTd<#vm%{*PS1A^1{!cd)@?OmX))$uANx^9b#`c>>{DrDart z;wWD4%$x0x)-aE2DWJG}Gq)EEfjh?4|HOaaTcWi~f|2*pL~QD5vHnx#!nncgBa473 zpxs|QP}}#vqoCOi->!)p27lN+M$G@4NmK$Cv*}GTXE=(?X81_XO7n@(mO}#MN2c*F z?KSk2b|gT5%BPb8<}B%UJME9Ef3MNbO~WJQO*GISxE}2$M+VGwRszQt+=*4i56^GQ znYGL&)4*y#Enu}_TyBtE8WP;Yh1|;I&T3A2b%y0dn?lMbGO~$_ubB}<4XUv_R|X*{ ze((t%>I!AW_~Am7{|SQ`e>3NuMl?NWYlvoj&$7d%GR?S|dcU=KfFx}?u?`=oa2?O( zU{AS7H#RMVQ&sie>`SJj0_ug8sy`OfAc^KmXkf8qdX@`IT433Bj}Y%|301L7aWDyB z4%m(5d)nU8z13Y8v!f#f-U@SQRHw-Z9)Dj5<Xq**N)RXFw|YwaIB7ZF6VYhXWq=hm zZ%xD6ANdUrD^eb;MYC};d($HMb!?JoEr5C9xM+D`&euF1W*#VN+3xk$3`+j_Ps=n7 znkx;?_&E@6UE?+1*#k5Gsb%WR>=!A&7;~=ZI9_out<HOy36>7e`BK%u62=m5+@mbj z6I)yqzM?rfKnUYz9_9Mrlk8v%0p6t+0vc&gCsMlBlmZ2d#xbr&sx=pDpXY)ChjXqw z2HKpF%r+AuD%!HI|69nRvGBkJa}nXx*qGHdJXU$s-UCAJLu~dj<%yeU#6DXt^9E8f z>qwiEec}QKSe@&F#m7#jeX5ycG3#(=MJ|{di@EqOT$CDKi#vfN?u+IsBIo7YlGnmE z?U$@G`N)+;1<%ciQM)IFPJ@{OUIZ9Bu>3?!aCGaUCr2lJQ%GHk6HRQjn>`QSzzR5R zaToPxhnP5JwA&MkFY(gvj+9qo)W1}n-n|pknIyEOL$3rB{rpa=q_o1*X=}AHRxU)) zX|QCU6eR?@|MeD%7byz96-RKyqr?a}Bxm!9_dJ!%w$ga5cj;i*Aw>3O3GcTEQ=d3z z2!g5bSjYim<bc+?iGj~B%wPz7Y_3Cy<HQxS5e`h#V}KmTJg@M-tR23iw{9FC2t3p4 z2%;*fKKSovPM>Mwa}h!CNnl`%ECPbGNANB>aqB9(2v%&`Eik9qt`dC?K(xC*odamX zF19qf%MI|Y=z}Vb0{4!0&wgsWuUiZ^I9k=LCvFTWFf2%3>WpAVq^fo5sX~irW?_wz z>QzcYO5jXMg4hur6j&@A%Zg<RtL`Er7nx(04(RCIjvv)&Bk0`DL4o(OE;H12_t$%m z&pgk<*paU&K&DpsD1=xhHii#6b`yLq^JK&cM1sbGGLBD;n%ZbL%%vl=MDg<K8eTFn zvmicrgNAj!l=pqTcKW>CvKD<#Z@$i-2A~P5xM&D)#AT15vGSnn#A$C)`FPD_E|*ae z8wZ?Um82CzCTl!BqfL)1%d*8I-jHK8-V4-}&|3t+QV50AY{=m`Ux;7wk^jvGeI``& zRKyezzG>~9I=1I)!CTgmtgu-%FW>c$!TjTcdSZaO=Wt^@e#Zp!fli7Q2Ly%4yubOX zPuX`o8T6Z)YV@tFq0kp|h#zO^zj}}kjq2%i4YduUD&R=TY&1P1g<=jV31xA{!s6md znKHOwhQSLEr&oY+S%>yZ`8M^ZCGSr*iu*^<N$5R~m@Bx_SB)+&c8+Y9JcPO}$RwwI zX$`|T-9v_x(4M?pdoU_=0(rG7Z<bEvJharso1XvB_yc%(;2QEcH7*Act2=ZoF9))t zUI?z0fs4T&)P4{${x1zXF=1pH9eT?nDe=Iz{r+L5KEUBPv(VHo?mCAlw4^eJfFwnn z=E^ka11PSwB^zH8O?J!G)=^J(@#|Si1T)Rze~JQILiBHV6Jlr)HA!6D%>=J#7cl5L zQC<c|44Ug*pA7yXz=j2-%_pa8U<P)Tii*!t5e^tUe!&jj2YI3w#;;-(&gUZPMag3+ zy#7F?`%Myf&|J6s=jFR^^?}yb1p6A8VL#LTQQVj)VPs(OecdNX)t@uoyy*_oQ@(|X z)Ojf{JRTvafzi)sdg3Z}e%4fZ?~+8yI4<U;^fe3zrm1mcAR|7N^%nunAb0)1+^2(J zxYuk8^~SWfx0G2UaE)?!$?Wo-Jsu(;pJg9DO28GjKm&7ib9`*XN{KtEm7q|tY%oM) zkGE00mI~<Kg080<s8HJdb7^w<tIq4qjvaSym{cGffe?XJ<)z1;C&lGOQtKpWl;{~8 z%u=6>)Z(8%mQpj+#bKKX&~H#plMU9t84W=uA^r5^vWEAPJ*YvG*$HHshH`Y+Yqp!o zH!>rHN?2l{-x!Va=sUH@xB#!uPA6-o8%w@h<)`I#+@}h#oh)3kl@zCxgaMg53({NN z9D=Mz<`WxK&P&d>_WGD#EOdZM<tvL2(yxI}$Xq7fS||_qOfUZqBksz1F=Lj%r^Swc z+gi;TH+BBmrbVFi1^ZB}%6rcyMa2B=bMn5iC-Z8-$0$$Kpacb^*^(q|Wxm;VI-V#i zV7@TmswOH$qU(?1nS=dweO>5M5p`IS3}2j1paM`1vgj<0HY#DXQCcfDA#k%f#2zt} zoKKN|4#T%b&RGRZ6eZ%jK{-K%Cz4YCxz%u0Uw1fW?*Ynd#%Dv>ofLfbw<YmF|EZ=k zS*kJv8=s;{xh+ku&2$l<-ve4X#ES?N;f+%D$}q8swu<EVPh93lSa~?NHCnEBwXIG! zG?;o$dFPZnj{dFo*m7bJc=}im5yf8w%q$FE)t{};{T9+i0kMolkT}xDvR|tMIlhx_ zU`2L9pm7Zkz#y@XMNHxop=mc-k6n-AzZoYuj_=5tksf<qoiC%#>^<+HXTD`cR}2*x zo>lAWAujm9nS&JPk}PRYOmJ#Y2Ydph8-az&!V?@b;*m>$YC2gg45uf@>!0SXeUVz> zeW*)(rHIBdcs%7Ix_r@2!G7ZRii;>o9Q!a9s>4pU2>Dj>LPDhYS*~2H6YfVK#SM!f zBT%I8OXTuA&tKo_wpaB?K$rLIKb_8qB-G~RtJMf)y`*z22zUF_yyjpvbKwH{VA%Dz zR<0U4!vgm|H|mhHv*qw7qmA5qB*_TzWx-Lg@vt9{Y6+}VLov#O!Vy{o$`SV?4(pf$ z(iUfSG^F(mwGJ>v6TVw@^?$q?qBtS`W1}x>^!l4b%jq?QLSkdlU*s~v;KqF~ac`vo z9pD6FXMg+er<u)b#Nu1pxxM^x@uRk<$T_GKuP1S?nkBIxEXwFHE@}OPx_YBC)W-Vx zAd!WP8}K@Q=r!w`dpC9y=dc>ZDqYQNCQhl}O-sNrIinv8pI;Vm^rj*DD8<8y#O8Qu zmbJ5a%|MvuaIimeTk_wZH`F^$R|pO|i+a$@nqgjiJKL=371EW&b|TPq5@|pykO9%= z1Z7W;^pw63-^}xpx_hvf>h?H9OZ<HLEAxqEzpU@kq^BcTWRP<FMK5=`lBIA59ug3i znemVS`yg&NPlZ}R;?dT9JO}qHEzY!XC_)%OKFod0?Ole^XoNogRSmlAUc(G$l&|A9 zi4}SG_RP`gt5#@=yltEx8!S-#kNm)IlEK#RFZs$yvD9Rer1Dj#eHfl*)z)lHxA(la zC7sqMg(R)i=o!pkQiH6`>D`O1vr>mvL!ZPuwuYJI(kngE7aY>qBEY%^Cx_YPD+A2j z7s~ec2Xw}NE~kAL!FGDdxQcot?JDB{1tKmYOi4z9FxN@jhCcaR==2#F49dP2){n9I ztRUq;y<Gu1m~6M5I}8T}{*wj8ovHXEPK-fg2#(k6?-%tngc==P+Fq4oH^+&~ueM2@ z$3{}sr+X^UF4vgzvx3U8phQ!rqlSYIuQ&Qzs3Ld#)EUE8S!n!32D5bk7D!K84I2m4 z)q)yKD68n`FvU|_P%0A<_{z3|LyT3sd?5{V;vE8yx5e5<j{_3jzG{qqI6c@X;*2i4 zruDStcQC@^gQuD|!+yqhhy8fi6pDg^#>ii~ss-wuTL=<_Vvpirk|P`!je`%8Y(dpe znmbxWQb7|%u@)8L{@nQWdxfJ3caMLM?tAMgS>$eWVSt&6Oge_WkwCZiuhzA=G3GNL zv&qLhV>vJMok7~idFrFd2kvl^L-mOZW`Cp0dkRIoy2>KRDs?jXv-Q%8^4XTqv(v0N zCG_VRuI|L<(AI1<N8TXzWGN7G<#x)BICK&>zOJ!oBct}*nJA65=cJt`Jwv!U#+~tK zrzTYt_zryX<D~+3I>2(TkTO-IiABn8VR41!>{9?O5Ac==%gq025O2Ga0ga8qDXV~G zuJVuKFRqI_`4(;AQx9S&O;}?A3e@5oT=a1sUBK*Ynp707@!d+_XuS%l?|%RN0?(}} zlZuF*NBpOk@qG?t^epNuSH~Tw_2Guga`08GHO;uwFn%+xsW_77RlFhPqj0qo6wmj= zpGtWlGG^g<w$_asS47Quw`pLGyM&k1S7!m2V{3&CP9SLAJKQ(-czf(hdm9*8e@Qt} zVy$_FVAc(*d4hKcT~37vg3VVuF@SZDS8Ej)!JP6-;RjUox6bhgL5GbCO}6Ppz|rMG zyw2e;D4&1vRN#4q?7`YN#P{;uu|KVe4h;{=_{~n+Va@c#ABJQ_fQA&UhwVu}2QWYm zjdnn|j*aka>0+x+uRrbi3Q{@Wz#Dl_g;@eukipy28!glPqi&wcG)N+k)8X^M(KMG4 zunmEeZ-7$PPcF@{Lohe<33!d1BVa|PQ1U8K5R`&3$b~im-pTgXH78hN$BzQlOilC6 z;NTy2jUBmt`t8wSdMAm(<?;Mbx&8C&)~7;LIpd|2y0?aF(4$P(|8jH2Kmu`fkrmXh z$jmDI(M;Gb0!F8~I^x(>?$Q;TJ-FHJAR@s`M}cBMww|7pmYfX)@s!)2EjJkjzPlyo zLmdceQ}p(hdXAo1<GEm15$^5JK~%fcGap~1?dZn9T{&^YBDDKHyj+bAAgRVo4x8`h z@NyA3Uyw0B2D9r0UjI+01LQ<9VJi}bvU}a)8~LSwN=pC8wA@q5;(739qs{Mvdn4?g zpKBKN-37R3%ge`~@#1*Es0~N_Qcx=sVXY}41}5`=St(G%6^`RZ-J-?L;9K@pP~cE7 zH!g1Pyytrv%(M=+P*naFs_xXj7%ZINZ0s$1>S`RI{yzT>6^qpQ<x#^?m-cOPdRHC^ ztTMD4?2V~E-_`v7C@UniQ;nXeAbOuO;lO+wwhUkH2+pSqSeh9mxO-Qza;ct_y=>Ke z$*ZMW(eG3btS+3FMq272a&<(c6Yg}!pB|R4f;y-CpY8>b$%J7^bOxmEaa{MY7-b(Q zf@GV%Q}+WKjteDr>Uv;#-qOc*<BEctcH|~zwNq_)OdLD}P(ZzgBxwEzt&=He_=0Mi z(5n7&%WWk?YCm3Qff#N<%}g^y%KHnh%VYTQ9O#o(zlE1rQiT7QU5I`dKif-%ZeYn9 zB%x+=5UxMYEC5?xlJDDZF~uUjJ*)F^sD<3)0Dj$*X8qcxdIT1ae87t|m!iuP5**Ln zWo6_=@QoR92c9#aCX1bYvA5i+v&$To@`gn0e3Ud@fcrn(%x1%tzrYPxkUChVy-E$J zPb}WsbJ*h1<I<5NzYXdX;?wZHZY{5?Y38n=oS~vJk>T9(4?&%>4&%ZLT7s@%-^iVg zm4VK%-asSD){of=HquBG5V7Bx6+QN!q<yWyax=Mb8K{4-ejR5P+hvgOFa2_#*E8p0 z>*}%)4MD%5&M*jx0rM0nIf4k@OAG0Oa7H&>2kV+s(ev5YH0+(njY9o@Qu=!1)Sw<= zR6i0;+@nG%N*+PM0*Be&9FK`>8$+6wxWh<MU9mQ@wG#H@B`uLk;@!CS5*v41aW;zL zHr?AnFmC_PhjlyL`$ZX_E|%xpNB6_%93OMFjxR2aEe;zezOIuCmndRbx|{C%=Wd11 z;BCV)ui_^aa1=x}yzd4toh(^gK|WH2m>2g%W)Yd+S<_cXKOHQoN&>7o{l&qgyr<-A zPL@(8HDd&)upGS(%0xF`@An^WC7P7zm%i9?#Btz`U0ogs8&&nC^hr2PjO-DXD5&%I zyZqPDbFJ?>%C~Ca5sHgAg5rrdbxFWt<EyoQx$zelTLGS;X{&<-_k;jF7g9{bHmGbk zxcldyC8#%Ry`;(hbKQlhQ{?sUTV5AWrIpyszg|BOW&6du&=NzO`}?9%6_&n771I}& zM~5jOcBl0X39#ni&vl%&0h``NFApmlMy(S~{*z2X<Ik1#%$jj9HrkoTnqym}mO3!8 zNv5zpf$ygDA%wQDTgJ~#0_yza?Gi4KCFW%eJf4UFoYJpwL;VCTTiNrOppBDZQ_n&y z+~NvW?2z|Ajyp`Vikq9LKF~L)hP{QI<)rgC0@Gh)qHyLgA>+(@p4f2S=6_T(?R~#j zOM~Nqt!Kn7Igb(3vxCx~VA~GZ6?Sx}!h~K&F-uv*T9xWbDxEo2r&|A2O;AH8S%HOx z&0T7`8^LN43#2jhosBhen^i3;<2mUb$sn}qqUy%J5L(<Q(8~u|#v*D87BMUQv<wn+ z3B5(U%BmEZ5(9+2<r;0j2Q5l;`S$rEwFwO-wid}Tx;<fDxpLbJ6c(LHZkG?|-!C+I zCMElC?zP5npOOI-_$3Io)F^sp)E-UwJ}b59DXTL_e-U?(`l|7M_gRG(p&IR%Js$fz zDZxNUdj2ISfvJLWuzf$Q1Lb1*lM)8bruWwJxA=2LUJo0;$Vv0b>pfB%E}ph6MmM{J z7&`rITN9Xmv-cqSMfP|PEr;&RLd2slulV<=5Ic;nF@lJfGK&MU-eBn7JefJt<$Lsn z?9LD*cG7OD_UE;<&0pboCMb<p>Ap^y&bmSMNJSYOXrydf$x0rnWoG%}(S)YINx{WS zO*^wv9|kp}Lpw0_&iwrvbKY5|sC}lJt>sD`O3|RkgdUI^8okG4yiiC97L1ecgM5D( zRSQ?&lXxitpbD9w8}q1((r0<nvp{t)FW27?Z6$$(z0-h*&5f7aFV_+N`XIPg_#zzM zxE)G_(m>=qNLjM>6FUdXlqw9k#xSs}t+4Lu@=VbRpPN2X`-Cj8j239@F$=0*FZu1z zh=qV04}(B#Td^iGoJ`){2rSng;<zp1pJpBe-$#6!h?^#E=?(s#E`$2vFU!GUR>J=U zS;B+^0slDX+5h@8+LX4Y>L!0bc=@i+^R}8C1nl^zC{VZ<vGmv^W;#y4=2;nq=#XN! zw7aW%LE7w-NCpH=%>3__p(U4O-0#LS?qZsj=wY;A>%Hv2TA>>Xy(40^6Wc3U(bdwQ zH4}v{jhM>eYMPKn^)HyNIFJ7d{jC{pKP#&Ql~-)ht=BTTFi1-{4R=Vr-QaB|g2<cg z;*F{a|Gj3MxTreqGSve+sC|J~b4(wp>?4YRJ}Pz+s+%ug+YOZ+ev&;&kH~xa+JFq0 z;hFs*zxgFTAMBB-5SJyE2gB>Nh$tq8P{|)aV|mb3T%d}Q3<;mONImAaT=>UceVm)G zW5o)EMEXtF`)#}ky=lQ@>Q{d2-`5CFdaL8-B`t2Yb(te?Mkqiy=NNF@!hSA^0k|1i zmXH_=v#68SQsbI8op2LC3PDS3>(Xo>5Pl9s%B%Z<y(6zhn-`~)B=>0aDS8#qW|*?W zkWc>A#(q7LVAd&pO<0h-f?6sydkEkFR0+^PUg>2{EEjjM#e=e~CIpd41t?TIvXY~4 zg%d1La@p4v*PmRq5BB!4wcnyUT7G@7$>f@012#Z^M-$gvo~{-6ogFIrBw?CBCBw0~ z%#9O>tuTPVV?oaUe>$8+y6dtjs8HmXUj}&^C*06iu-i5$NBjlYX;^E4_&3A*c4qTM z>upj0O9i8=jDlhi&2%#=y&0fe#?k4NWl_s@``kl+e!xWr{}o2@+ZqR85d@ujJU5lv z#pZE~FgV8!5oh;UX5}SV$(NT<dfp`R@|%b`<kI*0yo36n_C|P_7Fon715(89AD^## zAL8spop<!(a;oM1o36Of#qAhwjys8>1fa$vQZw!+WCHhp-JJJ7)&Kwhk4?fsWmJwM z(WJ5}Gb3cSjO++ylaM&b3?WKFMBComv9fyW5ZNo)d#|%T_vh>TUwr$;FFh~koX6ww zyg%;acDq8dHZYR#;SFebE;o=+x^pSNF808nXe-jy)c=PUnw{70^A_i$y2!A<glS_2 z<w+jQr9%-;xq&Wtr?VMS;1zHgfYj`vyzlmKa@lQO5a-al2D%=oSZ_bO1VXJsfAapo zP#4>+1xx6EUQV*kpA%_qOD%0kU|-ggska<n6Smyt<nl&&n|<-e`lmEU^ZUF-?4src z-FO_%MI^XGh5nG_x!5>bRKM%nT{SueXU4w)Gw*$!UHm@5iUiZh2_Kj^ENIZy|M$S* zGrI)59`owhcur;nhZ#x3_EnWdI5}*qAp%2e9zWaQE}P+*a62=UJoPy_+g7-VRfzVI z0SYA((h-8LtJxW`i_T1T`zgP(8~RaL2>VK1gYn}X4ljj@E5QuV$`=xZ(zC#lblUE~ zryBPi)|(g5w35s3u0D976Vp4WZr$8}B1U08%&Ij8-D*#hFFutKJySX4T*QsrI$H(8 z2Pe(PzfYMJ+I$~-gErQAiwy{zxubjndxr7Cs^n@C!~ov|wPz0mqeD(eHV;-mh#ZpZ z<@0ktg&iK#_wHp5K;cQUE7jIArfwl@{8xKkeDi&a)(m3{<EBLMNys(pdB406=<Xh@ zA?97x_Kn7Uf&v%zkX6|6QVXh6ud%xHv3GBV$+MZ;$)|5G;wUpXIa^JPTdG2^hJx^p z8w<2a&kcR<NL~uzF<))IM%3zM2|!J0Y8&%Q*hcl;s*DuA8mBLLQ@c$am@1ce*{g(m zdcWUpx!0HLM18idDION{fW2;~o=utg*t6Ao#OOH~jU#1tm{j#PU(X20esJ=@0c>!q z@P*aQ$#ib9_oXcYem(a<$eK4(#wD1=pd`4-R!;r)Meb$<XH>QH<5~R`$Iqv&;lqCO zDfdtQ(I>{pSIvLVs>bTS{d5z3-+C{oD!6tUF${(=zCEqv5u&Fal^@E+|8f63d=Pp} zsJO#cavN!u@tAvmD5JklZ@J6beLzu~RuF9}41Ml0C^!b0F#h&`vhEfcn)`t;JkR;= z8AF5VLbP&t0vEqMXaHMZ%5NJjfW{^jN7*x<3+>Wt60Girr5Qz;Qd=-yDJAxUJ?t-< z>iQ$NmtH4YQ1U`W;5)0{3=I5|QVL;e8#H>u(Nt{Mu?ZiCh-0kBOlVNpbFo9&NoGD< zf3~5JFj+9jft1k-`L0jPPJX_m3BA-{4QcEzoTZJ2SjFyr*@@N@NDXUH;r?KUQTX?+ zN;a%l`7OlNIgk*{^z{>hJaKC8`SvC`K7PXM6fkj;>&yQwfBCeMmRtQ>j~dM{^Mt4x zpJO9Plh1ML-1U^-Y7v)NmB%BX;4mC}`cpLRFB<~O;bIW56>+@vj)JEW@gT2|K!6hX zmpV5@xKLwQVHN`zN4-?ox6d-kdXu^vY0*Cgdu(U*s52IpO!b1-vMCkr{W7FDueM{` zvunG$h6C3vMlivkc%i?`Jz1vAwoA%y|8doL5No#WNTtCC?W99^71*#O<`QDm7P<Sd zVSiVPz=D~Brov2<#hqr1Fv>p&eMBiW4ig(6Z;iXMp?&n|X&L@Yr>^NUW1A=eJ8xN% zE+9SX-xWnt_Whky(}Yk(_??(wBkc<JDY+}FZCDk@4jv1mGzCA>Oh>Ye_#FvG_{4#q zwV`QoH9kH*qwR_Hp5Ka(bD{PGCz*=v|FpY<4p=i@WB`nVOT!+7t-h1=Kn<dQ`&&JF zV7PU6%>C#2(Ke38v~V&12qoHdRB++7JEjom9p`(#ZEp{`__|Ct>m=V>%fm}EFv51! zcG^1w*o8ult$&_lM%*?h=;h#qDtAxPvIHmxJUwwvNepEkkjr%3re-IY0nHTQJSYf{ z)eQ8FP@#`yLQr8Z00xLLAY<fGQgnT+xlkb>$}WF=)oX5|G32v$(tSnZ0D&}ffE7;P zwu-G{w=LAeHZsGm&&lhF_{SV>vzT~YWMN_jcU^D#Z*rvDZGimIZ+;xi&dbgi5sqmu zgCf|*P0^HnaS-l6u-SmQBa44IvAb)PNf^bYjiv=N_x5`lZMUbSwU#C5FBT7f*4bnI z<{yb19;Mk4Jh<u?puL>x*=uYr{c9oi7qWq`n3dB(1+WeMmOo$E-al#PGpKm#nUx#Y zsLTvb-d04=2kote#aCKl1vg%wS&||p1<@;UDy_Ffd`X!&v(}}KDRIcF>3t{paN^<# z<h0O*l`+KqRa}4)9}y}FuWGZc+&~@Tl#>Ll_|C0*q})aHbiRK>cub`rB52?=W`8+! zH3-a)jXm9(`zK1#mZAvK=r@f^XUs@6Vsx*rsrpWKsV{A~{d4pZMqldi6NWRQ%iIS_ zn4ejdm#f|0svNX=ddiSikRhG-A(xx|%4qLWtjgkts+H0mg}tWFgm+V^nY&4_hCRwS z|ArFap13vYxyz8v3O^N_yb%dmMIYx)-KTnwc=qlj;)@ZZ4Um4Ii$=(P7IHuY2cW7$ z=e_TjIS$-^08Z)m^1g%M8gn$Oe7x3|#1gLqeiNFn|GOT|KhVgb=m@-%VkQCaIt5<I z)?UXUTPcU0R&SYjH1wT$cj1`(N~Bq&x`mcJ>hGjHCT|&3t43tI?%*(7sE4%R35~0_ z6Wdgz?&<jobJ4RJ=9S_`aCjM>?_gXrPzZpHxzP}T0~gb1O&*X<o&b_(|HHZDVe(iQ zz0$k-#_K0o8fbz3@#(d=;jhDUxAo+wM(Drs12jlEyg}tO6a_^~!Q8s8|8Uy_b|%E_ zC9={Rr)A2D3aubpeI54^%g)XYnK8%i&pJggV?*XCHT&~fZ@P=W9T((;v(G@=NQl5B zQeALF6Y&m_Ulx^*=Zx6g;!zsb`X5Zeu)4|jTbOBjda^&&Yh+5XFCB{w?#FFAjS!Pz z>It0=2hr5Wud0ZryL1yT)~JPm=c-Pn?KxBgh7QN%zorhBmAL^|z$^~^RsTa!;%6Io z3FMTBET#>Wx|E%a<Qclya_nCp1m)$hv*F#}a28<sq6e)<AitEu;2b19X1<&Gkp2DF z9Y?AJiKnS?P3;`;`>7|I_L3#-Ez8E9ByTNux9Cy8gb}TlLT!mfwO(-D^N<P`u$?df zvAg?8B58v#B%1atBjUL-v(;G2KJwb0HOS77*HakCEho8id@{(p*~oF*O$Lr@?1xoU zrsGiY!+%9zIB5xT`(Mcmaq8Lkw&o4gxS2xG7f))>FM#hzo@*%r<_gZF0q%eE%TxvU z?r+gY>RzkOwZAsBaB4|&e_@7eWRnKP_zsu;Ex0k<0_5Fbx<(;e$H?2l^|)hDyZHPP zi5g9>bOg<XT2N!eKkaz84Z&~#@{L6ZN@ZqU&&R%AkNz)yYbfwi{mVvdTLf^`h?o70 zVeVp>FdubAomMb%{F+@X`bA<ps-2Pg6xx~lEto1+?QW2`3|!Y(fBr!w?B366IdFZw zpc;o#TfmlgN!bXMXb6X6DRAa}k%^3BJ~B%p_~=y7D2Hp?Su|21_;^R9J^IRZ^2XP5 zPZi;rTS0MO9AiJ$ffwbeXgxpyMlD==vy+ZQpoRX`#Z*WPjvcw)oJRBL&b7a5=k9** z8J&K3p9&k|-f-VC9Z_RF%dG?9a}BG(zhTp}Zzgs`gV97*>@TF-oHI*YOT#(CKyPik zMzApr2d++90h%Kmw;A;0x9+T*@qV_@E8W*~%ZNE{bbQ$&-!iae(`~1_EY0|IyTh4d zT&Rn5Sf!{a)%pk!Sq@it)PyO4KsyJ7kGHy%!;5L?@IY-bsc`=U%F;32D^rIEi-~{r z4#IOG45}G|HTiwHW)3Q$2MZ(!a^1d6Xsz2B>1d(X`RW}oFL16}lE(beO2Oy_XuwA` z9A)7=BB(eHsa*cqyz1XA9IRN?GtAA@l0Nf|dU=v@*sG=!G=j%Wsgq2m&|dDXP+_W~ ztp9XkAQzT7m)!r6aOM=`kyts+6$;YFuuDxieY<%h-i8zpl#G5YBO7^k`JT`yKI=R@ zrE&!BLJGf%l!nkD$N?H^JuDwL<mne=Y%LA-A(JeiN!4;J+;n$$?|LaliOiq1KdBvk zJ}1P&Z|bmK(*XI8;W{Kd2iQV!;7u`a_F8mn85pojb4tDH^~B|)$kt&tF4P2*73V8A zEu(a=Jo9BvZ(o2V6+WwYou_{-sa!dnwg5)OJEsJECgVmwZNeS0Hfde?_C<yYqO$pL zF5fSVy*Jm-*I_6l#Ba4tbURIEG(R;?>8p9o(-&ia9lKrkyxg^(c1NrfXhCc^NCcfA zUR|P`R-#o}<gs#l@Biz{lh(gsCO<VwKovG(=Opv!chie3Id$eWy5MXIMBYJgaoEFK zT*!)I=LL+F*49wud9deO<QqYo+$zz+ze0xdetGAKvf=w4(vF=Ft(waEy(|fT`b@3Q zXErwQ&7{r*DTz{gU}M&G^p`+0Sgb`^WpcRl<>h&^nn4Q8EA|Oe_8MSH`)g6{U9iG! z2$g`Bz>g(DvBEG}NFH&whd!cd_tKSJIHxy{wL+KRfQ~^VK<W~Xv9jc#Yk`E(5lIuy ziHf%9kuFi@fIxQYP&vuC=iI&rLB8+E`FoSbHHh&{ezQ~MzgWymJ`M>?tMe%i#KD3i zV8{EdW45kYFS4Ck(9#1hzOoW?ek-JjGk#UFPZsT+gs`q_ClhxGCdKw2-ln0ATWjO@ zbE}rGS`fnoo_@94`FHY;MA{|@poA0h``xG#)tNe32GUmRHdWAVA$dj%91)UT)${`a zWc%T>izyzz3gUIIwy|HG70faz%MGIl?{;IrTYu=ndK9n1M?-OOz9za=$hd~cJ7alW zjmkEc>ql_<j|cHZ7K*avCNg>lJ}2T2;-&>@1Ya>SzR$Wf)s83#cxaHCSRU@)ZBPUo zId^|<V!uU}4nY%Hmi<Gw9EZ!h5cM)~#0BTzx$vjReX4z~CoK@L2Ok@o1FFz{f~j;6 zhOJWpOy!V^AAON1=A^?f87rd{E*ZL|r8)NMq}C!pt}4iT1l)F@e8QIB;Rx@LtxMc2 zsGGvE!o1CXg>EGL7<KgJ)or7yjohk5L^%RVC1xouE}<Hw6hfNfSl{eXm+HT6{@26) zvBmx+0PsvTjw=|IsR+zMCq#$tb(r+if^r|sZ*Yqa-@&V4n*?0vklwBUZjzH{@%7oe z>vMj*{ys~WkX7SSHcG!${NeFzXT3RT2fy@kA-4!8zm*{6*wcFBP+#V0)iWRd$Y=v} ziC@j()kDAZ+qms?GlP!_g7n|-<~))a^N2C?TgXv(>aAP62b>qxF0Rn+IxyDPq$F}H zF8hbf6e8po>4X>3{JCq~BK?zX@j2=-0+h<ogDi2+o~S!GUK6fHm|+WtBUYhcUz8%< z!wp0L1w+>U_zfdVi1^#}Oc?LyaUvBJp932~P3JnVct)HM`YX*v>QV3GMh+Ml0G&lc zt3SbB+8w5QzH?nGdRKlli&;2NZ8u><&>>88df7PeqAE0=;PvO^fJGa;opVph*Qfsi zSi_R^4m%9N*paE&m?aK(VPAL?>02`%<Q-S>z47MUw{Cxg9Lh(N%pD!fE2~|BgwffH z?a&qRaiPIB#c+#wT`FDx%X|s9?&bQ0J<X=qYcZnmu3KBi3rV_y251O)OpfDqflqeK zyawLg&B``<oN+q$_cbhq^&kmM$~E6ZD=DBG=u6%UHW(4AwQo7Z$CS(~_CE{CykT#x zlZo$Y8nrk{>rKBX2ImMp&(&cX>;!v|4{UR-2o%i^@x+l8xi5?#vfq<w;pbg%m@bta zY<erXo7aLVgMsXjwdZ_)wa*3|V>g35bNMOe@cy9WG~0V&+lvod(p-a(;5_4srj1?W zp@9wS3hs!ioPx_}0BJF}R8uNtE*j(AGVgxZ$NpH+56^fm=Er*ETz;?9U_Uj*>-}*s zbM#iLv08}o;;`HBGTShuc+VO&e=K}?{AxeM!TWjDyBTbIvnO1vzmrgL9pjvh#6RV3 z*2JMP$~3pkT-vqEQ6<=>oeyR=ZP>6MPK(O)xhG4POn3FY_uKC$lZiv~ux?O+9bgU5 zwjTPGTKX9D*x&zDc-YB;rzhvx-n#ovWE#N0Y>r!X%WkeV=G?7%(koI4Gi7_3QBeDS zvlwgto)p_ktIULFXX7`*+of&w4tQ5F(>V?yatJ*e6zrM9fPhmFg0bQ5f@8Os)JcYV zxtWlfwMQ#tT^}h0>`rPej!Nm36K!Faoj<h`urypD@nc2Th6|p~vbsGfsmds+oUR+q zU+Uh$FT1qaO`g$~y?ws8m3~`_gm_+92O*E&*Kqo=r*;iubb;XiF06D_l`Ta{{{$D& z7}z-<C7L}+!i!-AfAdwE{Q?g)1uC}kEIf2}5nCgN7x+elglGisGc>35KZGh)?)JNH z&$W+L16(BA%{f<`nSoKYQfaFlv;$<~Od4*q(rab;e1T8sgIzT%ILrml`xOgr?WcRZ z%eb2E@kRH^?>12)VPk#$kk;YTA?r1W^lYbT4~gG7_eu&2AN>3tlBdB7zpyko8X1`> zSXj;{oR;=$K1lo_?*CWOrQl283$z-88>M8Scc8MwVM!0SAD`;pe!92X$0KHABRf?8 z4H_zAr6vLCUapf`_loQ@bH>sGqcMu3vIj0*jAl#1p1JB4VZ%L47Cq*hZRS4r3Av+p zZmk_`UyX!5g<$Yf(s{H~;Xyv3s&~e;hy<_23<Js*mvc72^V3ApDdH&4cBDsObO8mW z-EIWlM)(G}P21p5g!?hq(9B{w6|M%l?JIt@R+rr&ZIC_hvWBZ(6B>h5(g{$nPe9w= zkBk+jzCUPBSW8>D!rgWqo?W5A)-gt-clYMLn52R5LS4Y8$+>3BKpda(=Q}@3L=eQh zW1c-Xw*hWt40nA^PmG-|;q@uN7zOWg_D4l@K<A`8w_=)K7Os~ZfN5?+wp`_<6izC9 z-F)Zn_x=SS)@>;)Uf_`BWE?k+U*l1O2naXNCu41ss+C$|_Jhr>c|3JwzBkY1fuspQ zyR7by0lMU2jQsXn!?Z=B1oj0#3m~LeL`#3>)((o1{$ZAukGd4km_Kb}4>xc0JKMB# zt@}M#NCyzm+)D7lS`Db+gHH*5y78tMKjQO+yiVGv`Lqh?2|xNQG>x#;8rR9i4`Bl9 zV*X8g4@hd2umh@C1jVLz<QPn=*f0vi|4ry#JoxF@XK3MLYS_eaQ0}Ck=X`I;%dUsi zY2wO`&8@<Hc=jq_nN+bXIUx_v%DIj!RqcJxa-3eDk_IrI_?%FLzKtTUcl$m%_!+f{ zThEAH=i`$V1I;uItzrol0Ff;<jF0WDHkJZ!1L+c(@;-wg|9<r(Ha0bu2m&|@?>v9q z{hmz>7}Y36Bacp=|INFk0dt6PwmNh*)R3bF<xnCcI8+|mJDlN*s76FeNOg?4GR^BU z*_~x5e%3HI#a1Ic3eIP^FnXd++buJ&6GnVFw!-x0-fL=EcPa}1d^I;)8$Za<h<6&2 zHYv1;;8>`yQsFw4i>p?;L=n0Tud+~t<-rTQNFFgI<>Gw&URh~dk?lYcLGE8@CTNJb zX!&UQ$g-IPkG~BeNX{X<RNVbv;q)t2mS}gbQT!4cY~m~JRmM)q*%(L!yFcreq=@J& zB1DmE&l!!u)!_H5Dp+YhDA1z7{nMYRV%*%PCz}L+TWGj%zUl1QWsQpfEa6Snae-+e z<0$EdO!b&;bUkqe4?v8l&Zc@vo3TLoFpPgWdASS^z(Yjx{1pz|e>hCFgPK9bho|WW zfW1K7ACA`>ln}4Am&)X}RSRMG9dsd|iHFK2GdymcQ6=y!|2S_1EuViJ8-jkMy|Bvr z_6(e4=r7xPI`oxhzP!@BTtW3GJ{!L502~6sbDnQj9s66Q{vG@nQV$f}klZJi1vIZ_ zWmwp-0$1=OS`{@}^5{XdhkLe<`FjLVh$LJ?ZsUOl&_z0gZ}cvJ{P-HvK#4fs(dYa< zZ$8nqcwMs45VG=YKz7PZ7c;9gifo<Lzx8C{uHnG=&CF-sQG*9WdG3=vrvJCLLfwFd z!qJh9-laig4kd+rDocvOl?T6d&}AEn7~lWp5+@>S%%;!`?Q9z7n6rT5LfnCRE}`UM zck??L7v~4<Z#-{`s7gapuKJF1zgY$vNbVWjvrrE?u1~i87k3u8Xd?pKp22KC9|!ND zSYdhIW7o~w_$YihkS>%zp03~0)<}n1LmDK*zMgfcs=3mk?3=nTUtb<a#UE@{48CXk zD`jr1`+d?P1wo{wK@f8TB4D^AQ>LmB3hUbSDRFSkk2qLAQn^R)*17ILP2Y5f7(*dL zb$1^epk<){FBgneS8L*g19d6DGX3&51N))gBj-)+RTL@x{^SJ<;zCo7idx{VbkklR z0xZZjvGW@7OiX?JTNAlt#ET|t+jk?&;hIrp3@m6!@47IH=#s8W(J`@a7S^XgDURYR za^ch^^N^-xwuzDAr72J*n^hLuk$<j6mLp5V7_=r75gn)u^O@Wdhmpa3cr~cnqX90u zWTc}3+QmUu@oLK!0pDzy_4?K4QF!PqDBH-D7gm7^^(Y{EITT2p>OB(^w)#552S;dp z2RFr_3BO!RbVg#S-^Be3BxtXwz~Y=k(HRPH(WX4+GlfAUp<k-clsijAIXo2sQX@?x zq@JkG7Eicy000KCEQQQbmQdRLznkjm{I_i|Uk(5d+!9JHpz&&*flyxLY*sZGAEO9= zJUy-n{}XSDL4MZ}<%gEwr(G<CY0k@>djs5YR;HECsWRnlM`DyT!aw>DSVU7S`^>yj z4K+cDUe+0gW&n#u__>oQ0ffsda<b$UkLrIT06uVphMKc*TP)!Y`m(n6|9<`~0v*}t z(UV=3GRti-%9BjU7ftCrHH+kzMZd28M8^Bc3VxMkyrVJ_3f%pbo>@ott>JAGtC-|k z02>lMSJO{<5|VrD{RR-+=Qxj4KAB1Ns$9bX_GW1$kj|;&vyQLH-_IwQ6QFBWJ~II| z)mrv{h5e66T@uXTk!MwzS&N;9diS>$>^k@*^B-=^3^j7d+kkkX0B(^_I;3Nb_|sgX zJsW9H2B|J9%VtG#nJD(OYfMrhkIZ$7GcFp?tGxcma_{3+RT3R-htOj;*Zk*)immk* z3|z~8cl`KpS&}(O%gj{B(-4bh^$!r6xHGsjyy;nhdBk-hWr3FEk;do}>HSP?8gcLY z4=rcs{|%9Sez@#5PB5cRol0bI=ctnz3EYQLgSWr<o4kTIbWeDPtFP?CW!{CJt@-Tr z=+SriUo!IY$1^iCmpo=t4_dC#qYk~MQwV#gAbnXvqI)z}=JEwJex!mMZ;Bs%+2J=< zMd!Y%=)Ib5HZrr;TvMMGcHDR-?9)k9db#P5R&@Ws@!c<kln7Z5kF~uf)f9QJTrxG+ ztm+LpX@9%n{TID|5HH*y3~U>_@6Oz8;iDV%X4cX}p>)^>hmCfoKB!#0bcrjCo$W_n znM+GdqSz0>dJPQ{Z-prhjqj{KAW>GWWXorPYvQ&!+hzD=myeYHj2^XmLyUFO`Uxh_ z0@K~{@<%^~y!IoI&J9Fy<-mMcTx6d%%BZ2K@uU>^5hsRjf0@wt+O_kx37)h=y-7r& z%)5uI@V-0CyaEC*y9x^WuSg!60R}v^_N@&2^f+#PQd15`sh02cBJHk8?{1t9uAYz~ z{Uw7k1K)0yQa0zI<R$GG-gZKLVg|siR?1iMWmA706=$6!v`G+ojvN{6su6`8yKLX1 zhDyNWcvdOQsBy)q!aB^z*^R*Z`u^$%T8<e07j12A+pj(xm2obvp#hTQ-dGFc%19Lt zW^1d2E?h)JWN?+B&Y82(*bbk3w9N!;#_>5OnMH2a&^Q*Io}M0b2=gePc}-Gmqft~; z#8~L*{P3C~v{=druS+T>lPn*wpvH41JIzZT-Sr(BZ+2#WR_f(Kx*)-=Xja2-Qsow# zy!&){3_n&oTIqSV)Mcobov*!q<&dJ04=u{u!KX@5MKFxK`ttZ1`A4MN{^9^H1{1?0 zZeG1#J1{U%iW5|^0+b`8;xi@KkzY?2DCAUK$JrGZEgBfaMYqIYTI!!O4UH2tT>o%j z1ZYs_?yYDB$QfK_!cpX7RytCtKmA?^t~%4B@@?GvXD9*hIi@;->K5oxs6)aiRJtGy z3dO*JLY>k;p_H!CqEH98P$>Oa6e=L-|Ih!j$&>I#j_CP%=`ITXX{za}<|<nT{U7(O BO?v<U literal 14800 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 OIc;`vc)7{z(*FShbtRtw diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2dd3f91d79cab6e0390356bb3c1a355f94..b81789038bb1b5e0704d296cdcab37af92284870 100644 GIT binary patch delta 2433 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 delta 1860 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 diff --git a/pubspec.lock b/pubspec.lock index 0b8f49c2c..f12b25487 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,6 +205,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -494,7 +501,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.3" + version: "0.11.0" flutter_libepiccash: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index af4370d99..b1312427d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -141,7 +141,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.1.7 - flutter_launcher_icons: ^0.9.3 + flutter_launcher_icons: ^0.11.0 hive_generator: ^1.1.2 dependency_validator: ^3.1.2 hive_test: ^1.0.1 @@ -160,6 +160,13 @@ flutter_icons: image_path_android: assets/icon/app_icon_alpha.png image_path_ios: assets/icon/icon.png remove_alpha_ios: true + windows: + generate: true + image_path: assets/icon/icon.png + icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: assets/icon/icon.png flutter_native_splash: image: assets/images/splash.png diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..eee73d91b8f7daeadfb6dcb917cdb15466910278 100644 GIT binary patch literal 1968 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 CG^h~( literal 33772 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 From a8b0901ec664e9f1bee21222cd5d4e319e92aeff Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:32:06 -0600 Subject: [PATCH 093/100] desktop exchange navigation flow fix --- lib/pages/exchange_view/confirm_change_now_send.dart | 12 ++++++++++++ lib/pages/exchange_view/send_from_view.dart | 6 ++++++ .../exchange_steps/subwidgets/desktop_step_4.dart | 1 + 3 files changed, 19 insertions(+) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 9f62bd8ec..540067915 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -37,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { this.routeOnSuccessName = WalletView.routeName, required this.trade, this.shouldSendPublicFiroFunds, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -46,6 +47,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String routeOnSuccessName; final Trade trade; final bool? shouldSendPublicFiroFunds; + final bool fromDesktopStep4; @override ConsumerState<ConfirmChangeNowSendView> createState() => @@ -105,7 +107,17 @@ class _ConfirmChangeNowSendViewState if (mounted) { if (Util.isDesktop) { Navigator.of(context, rootNavigator: true).pop(); + + // stupid hack + if (widget.fromDesktopStep4) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + } } + Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e) { diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7cbf38384..7c5f5541c 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -38,6 +38,7 @@ class SendFromView extends ConsumerStatefulWidget { required this.amount, required this.address, this.shouldPopRoot = false, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/sendFrom"; @@ -47,6 +48,7 @@ class SendFromView extends ConsumerStatefulWidget { final String address; final Trade trade; final bool shouldPopRoot; + final bool fromDesktopStep4; @override ConsumerState<SendFromView> createState() => _SendFromViewState(); @@ -191,6 +193,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { amount: amount, address: address, trade: trade, + fromDesktopStep4: widget.fromDesktopStep4, ), ); }, @@ -210,12 +213,14 @@ class SendFromCard extends ConsumerStatefulWidget { required this.amount, required this.address, required this.trade, + this.fromDesktopStep4 = false, }) : super(key: key); final String walletId; final Decimal amount; final String address; final Trade trade; + final bool fromDesktopStep4; @override ConsumerState<SendFromCard> createState() => _SendFromCardState(); @@ -323,6 +328,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { : HomeView.routeName, trade: trade, shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + fromDesktopStep4: widget.fromDesktopStep4, ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index c86713a76..5b69f064d 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -224,6 +224,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { amount: amount, address: address, shouldPopRoot: true, + fromDesktopStep4: true, ), const RouteSettings( name: SendFromView.routeName, From c3a3dd3180358f0f1e2faae0cf16c6d7a8b4ecb4 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 22 Nov 2022 11:48:52 -0600 Subject: [PATCH 094/100] remove Wownero if isDesktop or isLinux or isWindows or isMacOS, respectively --- .../add_wallet_view/sub_widgets/searchable_coin_list.dart | 5 +++++ lib/utilities/enums/coin_enum.dart | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index d89d42bbf..38181b9e1 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -32,6 +32,11 @@ class SearchableCoinList extends ConsumerWidget { // remove firo testnet regardless _coins.remove(Coin.firoTestNet); + // Kidgloves for Wownero on desktop + if(isDesktop) { + _coins.remove(Coin.wownero); + } + return _coins; } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 48212bde8..f80c40f52 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; +import 'dart:io' show Platform; enum Coin { bitcoin, @@ -36,8 +37,7 @@ enum Coin { firoTestNet, } -// remove firotestnet for now -const int kTestNetCoinCount = 4; +int kTestNetCoinCount = (Platform.isLinux || Platform.isWindows || Platform.isMacOS) ? 5 : 4; extension CoinExt on Coin { String get prettyName { From 3306cf8b99f4afd54030bdef8746eb5de89adf6c Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 22 Nov 2022 11:56:13 -0600 Subject: [PATCH 095/100] expand the ternary for readability --- lib/utilities/enums/coin_enum.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index f80c40f52..648407809 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -37,7 +37,12 @@ enum Coin { firoTestNet, } -int kTestNetCoinCount = (Platform.isLinux || Platform.isWindows || Platform.isMacOS) ? 5 : 4; +if(Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + int kTestNetCoinCount = 5; // Because we are removing Wownero from Desktop +} else { + // remove firotestnet for now + int kTestNetCoinCount = 4; +} extension CoinExt on Coin { String get prettyName { From 172b3d157bacc84dff2dece8b4fd0c11ca9e1487 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:37:47 -0600 Subject: [PATCH 096/100] wownero disable on desktop fix --- lib/utilities/enums/coin_enum.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 648407809..543a193ee 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; -import 'dart:io' show Platform; +import 'package:stackwallet/utilities/util.dart'; enum Coin { bitcoin, @@ -37,12 +37,7 @@ enum Coin { firoTestNet, } -if(Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - int kTestNetCoinCount = 5; // Because we are removing Wownero from Desktop -} else { - // remove firotestnet for now - int kTestNetCoinCount = 4; -} +final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; extension CoinExt on Coin { String get prettyName { From 157829a933da78229e6279aeb3ae1fcc8409307b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:53:16 -0600 Subject: [PATCH 097/100] fixed failing test --- test/widget_tests/address_book_card_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index 7c53d8d50..07b1387df 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -70,7 +70,7 @@ void main() { await widgetTester.tap(find.byType(RawMaterialButton)); expect(find.byType(ContactPopUp), findsOneWidget); } else if (Util.isDesktop) { - expect(find.byType(RawMaterialButton), findsOneWidget); + expect(find.byType(RawMaterialButton), findsNothing); } }); } From 467d43d9f3c3c80b4dfc03c8232b56bcefeebc1d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:18:38 -0600 Subject: [PATCH 098/100] desktop trade history scroll fix --- .../desktop_exchange_view.dart | 16 +- .../subwidgets/desktop_trade_history.dart | 368 ++++++++++-------- 2 files changed, 217 insertions(+), 167 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart index 0f44eb59b..105c485f0 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -63,19 +63,9 @@ class _DesktopExchangeViewState extends State<DesktopExchangeView> { width: 16, ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Exchange details", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 16, - ), - const RoundedWhiteContainer( - padding: EdgeInsets.all(0), + child: Row( + children: const [ + Expanded( child: DesktopTradeHistory(), ), ], diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index a8f825911..e31a87dd4 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -24,6 +25,28 @@ class DesktopTradeHistory extends ConsumerStatefulWidget { } class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + @override Widget build(BuildContext context) { final trades = @@ -33,169 +56,206 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { final hasHistory = tradeCount > 0; if (hasHistory) { - return ListView.separated( - shrinkWrap: true, - primary: false, - itemBuilder: (context, index) { - return TradeCard( - key: Key("tradeCard_${trades[index].uuid}"), - trade: trades[index], - onTap: () async { - final String tradeId = trades[index].tradeId; - - final lookup = ref.read(tradeSentFromStackLookupProvider).all; - - debugPrint("ALL: $lookup"); - - final String? txid = ref - .read(tradeSentFromStackLookupProvider) - .getTxidForTradeId(tradeId); - final List<String>? walletIds = ref - .read(tradeSentFromStackLookupProvider) - .getWalletIdsForTradeId(tradeId); - - if (txid != null && walletIds != null && walletIds.isNotEmpty) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletIds.first); - - debugPrint("name: ${manager.walletName}"); - - // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData - final txData = await manager.transactionData; - - final tx = txData.getAllTransactions()[txid]; - - if (mounted) { - await showDialog<void>( - context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - // maxHeight: - // MediaQuery.of(context).size.height - 64, - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ], - ), - ), - Flexible( - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: manager.walletName, - walletId: walletIds.first, - ), - ), - ], - ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), - ); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + primary: false, + itemBuilder: (context, index) { + BorderRadius? radius; + if (index == tradeCount - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; } - } else { - unawaited( - showDialog<void>( - context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - // maxHeight: - // MediaQuery.of(context).size.height - 64, - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: radius, + ), + child: TradeCard( + key: Key("tradeCard_${trades[index].uuid}"), + trade: trades[index], + onTap: () async { + final String tradeId = trades[index].tradeId; + + final lookup = + ref.read(tradeSentFromStackLookupProvider).all; + + debugPrint("ALL: $lookup"); + + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); + final List<String>? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + if (txid != null && + walletIds != null && + walletIds.isNotEmpty) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds.first); + + debugPrint("name: ${manager.walletName}"); + + // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData + final txData = await manager.transactionData; + + final tx = txData.getAllTransactions()[txid]; + + if (mounted) { + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: manager.walletName, + walletId: walletIds.first, + ), + ), + ], + ), ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ], + const RouteSettings( + name: TradeDetailsView.routeName, ), ), - Flexible( - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: null, - walletName: null, - walletId: walletIds?.first, - ), - ), - ], - ), + ]; + }, ), - const RouteSettings( - name: TradeDetailsView.routeName, + ); + } + } else { + unawaited( + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, ), ), - ]; - }, - ), + ); + } + }, ), ); - } - }, - ); - }, - separatorBuilder: (context, index) { - return Container( - height: 1, - color: Theme.of(context).extension<StackColors>()!.background, - ); - }, - itemCount: tradeCount, + }, + separatorBuilder: (context, index) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + }, + itemCount: tradeCount, + ), + ), + ], ); } else { return RoundedWhiteContainer( From 4debb0fff9d67087f20743c4c3899996085c6b41 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:34:31 -0600 Subject: [PATCH 099/100] desktop block explorer warning dialog navigation fix --- .../transaction_views/transaction_details_view.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index dc4e41152..4d45428ff 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -266,7 +266,10 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of(context).pop(false); + Navigator.of( + context, + rootNavigator: true, + ).pop(false); }, ), const SizedBox(width: 20), @@ -275,7 +278,10 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of(context).pop(true); + Navigator.of( + context, + rootNavigator: true, + ).pop(true); }, ), ], From 67d375dbd5ba0b994fa9d370dd6787e197a6dbf5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:37:47 -0600 Subject: [PATCH 100/100] desktop new notifications bell icon indicator --- .../home/desktop_menu.dart | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 60a424a06..d82d62883 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu_item.dart'; import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -149,20 +150,27 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), DesktopMenuItem( icon: SvgPicture.asset( - Assets.svg.bell, + ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, width: 20, height: 20, - color: DesktopMenuItemId.notifications == - ref - .watch(currentDesktopMenuItemProvider.state) - .state - ? Theme.of(context) - .extension<StackColors>()! - .accentColorDark - : Theme.of(context) - .extension<StackColors>()! - .accentColorDark - .withOpacity(0.8), + color: ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? null + : DesktopMenuItemId.notifications == + ref + .watch(currentDesktopMenuItemProvider + .state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), ), label: "Notifications", value: DesktopMenuItemId.notifications,