From 4c45487e6ea02aa88325699d80d30ce5f053e5e0 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 29 Nov 2022 13:29:03 -0600 Subject: [PATCH] desktop all trades view --- .../all_transactions_view.dart | 1 - .../desktop_all_trades_view.dart | 454 ++++++++++++++++++ .../subwidgets/desktop_trade_history.dart | 11 +- lib/route_generator.dart | 12 +- lib/services/trade_service.dart | 10 + 5 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart 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 b9ea02e79..15061a841 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -16,7 +16,6 @@ import 'package:stackwallet/providers/ui/transaction_filter_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/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/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart new file mode 100644 index 000000000..d6ba1f9cb --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -0,0 +1,454 @@ +import 'dart:async'; + +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/models/exchange/change_now/exchange_transaction_status.dart'; +import 'package:stackwallet/models/exchange/response_objects/trade.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/assets.dart'; +import 'package:stackwallet/utilities/constants.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/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.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:tuple/tuple.dart'; + +class DesktopAllTradesView extends ConsumerStatefulWidget { + const DesktopAllTradesView({Key? key}) : super(key: key); + + static const String routeName = "/desktopAllTrades"; + + @override + ConsumerState createState() => + _DesktopAllTradesViewState(); +} + +class _DesktopAllTradesViewState extends ConsumerState { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchString = ""; + + List>> groupTransactionsByMonth( + List trades) { + Map> map = {}; + + for (var trade in trades) { + final date = trade.timestamp; + final monthYear = "${Constants.monthMap[date.month]} ${date.year}"; + if (map[monthYear] == null) { + map[monthYear] = []; + } + map[monthYear]!.add(trade); + } + + List>> result = []; + map.forEach((key, value) { + result.add(Tuple2(key, value)); + }); + + return result; + } + + @override + void initState() { + _searchController = TextEditingController(); + searchFieldFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 12, + ), + Text( + "Trades", + style: STextStyles.desktopH3(context), + ), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 20, + top: 20, + right: 20, + ), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 570, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .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 = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + Expanded( + child: Consumer( + builder: (_, ref, __) { + List trades = ref.watch( + tradesServiceProvider.select((value) => value.trades)); + + if (_searchString.isNotEmpty) { + final term = _searchString.toLowerCase(); + trades = trades + .where((e) => e.toString().toLowerCase().contains(term)) + .toList(growable: false); + } + final monthlyList = groupTransactionsByMonth(trades); + + return ListView.builder( + primary: false, + itemCount: monthlyList.length, + itemBuilder: (_, index) { + final month = monthlyList[index]; + return Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (index != 0) + const SizedBox( + height: 12, + ), + Text( + month.item1, + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.separated( + shrinkWrap: true, + primary: false, + separatorBuilder: (context, _) => Container( + height: 1, + color: Theme.of(context) + .extension()! + .background, + ), + itemCount: month.item2.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTradeRowCard( + key: Key( + "transactionCard_key_${month.item2[index].tradeId}"), + tradeId: month.item2[index].tradeId, + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class DesktopTradeRowCard extends ConsumerStatefulWidget { + const DesktopTradeRowCard({ + Key? key, + required this.tradeId, + }) : super(key: key); + + final String tradeId; + + @override + ConsumerState createState() => + _DesktopTradeRowCardState(); +} + +class _DesktopTradeRowCardState extends ConsumerState { + late final String tradeId; + + String _fetchIconAssetForStatus(String statusString, BuildContext context) { + ChangeNowTransactionStatus? status; + try { + if (statusString.toLowerCase().startsWith("waiting")) { + statusString = "waiting"; + } + status = changeNowTransactionStatusFromStringIgnoreCase(statusString); + } on ArgumentError catch (_) { + status = ChangeNowTransactionStatus.Failed; + } + + switch (status) { + case ChangeNowTransactionStatus.New: + case ChangeNowTransactionStatus.Waiting: + case ChangeNowTransactionStatus.Confirming: + case ChangeNowTransactionStatus.Exchanging: + case ChangeNowTransactionStatus.Sending: + case ChangeNowTransactionStatus.Refunded: + case ChangeNowTransactionStatus.Verifying: + return Assets.svg.txExchangePending(context); + case ChangeNowTransactionStatus.Finished: + return Assets.svg.txExchange(context); + case ChangeNowTransactionStatus.Failed: + return Assets.svg.txExchangeFailed(context); + } + } + + @override + void initState() { + tradeId = widget.tradeId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final String? txid = + ref.read(tradeSentFromStackLookupProvider).getTxidForTradeId(tradeId); + final List? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + final trade = + ref.watch(tradesServiceProvider.select((value) => value.get(tradeId)))!; + + return Material( + color: Theme.of(context).extension()!.popupBG, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + 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, + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + 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, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + flex: 3, + child: Text( + "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ), + Expanded( + flex: 4, + child: Text( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + Expanded( + flex: 6, + child: Text( + "-${Decimal.tryParse(trade.payInAmount)?.toStringAsFixed(8) ?? "..."} ${trade.payInCurrency.toUpperCase()}", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ), + Expanded( + flex: 4, + child: Text( + trade.exchangeName, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + SvgPicture.asset( + Assets.svg.circleInfo, + width: 20, + height: 20, + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ], + ), + ), + ), + ); + } +} 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 f9e51ef0a..01aa71ad2 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 @@ -3,20 +3,20 @@ 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/pages_desktop_specific/desktop_exchange/desktop_all_trades_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/route_generator.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/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/rounded_white_container.dart'; import 'package:stackwallet/widgets/trade_card.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); @@ -71,7 +71,8 @@ class _DesktopTradeHistoryState extends ConsumerState { BlueTextButton( text: "See all", onTap: () { - // todo display all trades + Navigator.of(context) + .pushNamed(DesktopAllTradesView.routeName); }, ), ], diff --git a/lib/route_generator.dart b/lib/route_generator.dart index cbc4cb343..bb040173b 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_all_trades_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'; @@ -96,12 +97,14 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_vie 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/unlock_wallet_keys_desktop.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'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; @@ -115,9 +118,6 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; -import 'pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; -import 'pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; - class RouteGenerator { static const bool useMaterialPageRoute = true; @@ -1029,6 +1029,12 @@ class RouteGenerator { builder: (_) => const DesktopExchangeView(), settings: RouteSettings(name: settings.name)); + case DesktopAllTradesView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAllTradesView(), + settings: RouteSettings(name: settings.name)); + case DesktopSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/trade_service.dart b/lib/services/trade_service.dart index 0ec6d17a6..abdcebb4b 100644 --- a/lib/services/trade_service.dart +++ b/lib/services/trade_service.dart @@ -11,6 +11,16 @@ class TradesService extends ChangeNotifier { return list; } + Trade? get(String tradeId) { + try { + return DB.instance + .values(boxName: DB.boxNameTradesV2) + .firstWhere((e) => e.tradeId == tradeId); + } catch (_) { + return null; + } + } + Future add({ required Trade trade, required bool shouldNotifyListeners,