From 8192a6538a2c41efaa0f6a2093bca3986c6e78e8 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Wed, 6 Mar 2024 22:15:10 +1000 Subject: [PATCH] feat(webapp): add currency toggle to navbar This enables the user to display balance and position values either in Sats/Btc or USD Signed-off-by: Philipp Hoenisch --- webapp/frontend/lib/common/amount_text.dart | 14 +++- .../lib/common/currency_change_notifier.dart | 31 +++++++++ .../lib/common/currency_selection_widget.dart | 40 ++++++++++++ webapp/frontend/lib/common/model.dart | 23 +++++-- .../lib/common/scaffold_with_nav.dart | 64 ++++++++++++++----- webapp/frontend/lib/main.dart | 2 + webapp/frontend/lib/trade/position_table.dart | 34 ++++++++-- 7 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 webapp/frontend/lib/common/currency_change_notifier.dart create mode 100644 webapp/frontend/lib/common/currency_selection_widget.dart diff --git a/webapp/frontend/lib/common/amount_text.dart b/webapp/frontend/lib/common/amount_text.dart index 45c42c15f..8dc6a966d 100644 --- a/webapp/frontend/lib/common/amount_text.dart +++ b/webapp/frontend/lib/common/amount_text.dart @@ -33,9 +33,17 @@ String formatSats(Amount amount) { return "${formatter.format(amount.sats)} sats"; } -String formatUsd(Usd usd) { - final formatter = NumberFormat("\$ #,###,###,###,###", "en"); - return formatter.format(usd.usd); +String formatUsd(Usd usd, {int decimalPlaces = 0}) { + String formatString; + if (decimalPlaces > 0) { + formatString = '\$ #,###,###,###,##0.${'0' * decimalPlaces}'; + } else { + formatString = '\$ #,###,###,###,##0'; + } + + final formatter = NumberFormat(formatString, "en"); + + return formatter.format(usd.asDouble); } String formatPrice(Price price) { diff --git a/webapp/frontend/lib/common/currency_change_notifier.dart b/webapp/frontend/lib/common/currency_change_notifier.dart new file mode 100644 index 000000000..3896dbc75 --- /dev/null +++ b/webapp/frontend/lib/common/currency_change_notifier.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +enum Currency { usd, btc, sats } + +extension CurrencyExtension on Currency { + String get name { + switch (this) { + case Currency.sats: + return 'Sats'; + case Currency.btc: + return 'BTC'; + case Currency.usd: + return 'USD'; + default: + throw Exception('Unknown currency'); + } + } +} + +class CurrencyChangeNotifier extends ChangeNotifier { + Currency _currency; + + CurrencyChangeNotifier(this._currency); + + Currency get currency => _currency; + + set currency(Currency value) { + _currency = value; + notifyListeners(); + } +} diff --git a/webapp/frontend/lib/common/currency_selection_widget.dart b/webapp/frontend/lib/common/currency_selection_widget.dart new file mode 100644 index 000000000..f9b0027c9 --- /dev/null +++ b/webapp/frontend/lib/common/currency_selection_widget.dart @@ -0,0 +1,40 @@ +import 'package:bitcoin_icons/bitcoin_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get_10101/common/currency_change_notifier.dart'; +import 'package:provider/provider.dart'; + +class CurrencySelectionScreen extends StatelessWidget { + const CurrencySelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + CurrencyChangeNotifier changeNotifier = context.watch(); + final Currency currency = changeNotifier.currency; + + return SegmentedButton( + style: SegmentedButton.styleFrom( + backgroundColor: Colors.grey[100], + ), + segments: >[ + ButtonSegment( + value: Currency.sats, + label: Text(Currency.sats.name), + icon: const Icon(BitcoinIcons.satoshi_v2)), + ButtonSegment( + value: Currency.btc, + label: Text(Currency.btc.name), + icon: const Icon(BitcoinIcons.bitcoin)), + ButtonSegment( + value: Currency.usd, + label: Text(Currency.usd.name), + icon: const Icon(FontAwesomeIcons.dollarSign), + ), + ], + selected: {currency}, + onSelectionChanged: (Set newSelection) { + changeNotifier.currency = newSelection.first; + }, + ); + } +} diff --git a/webapp/frontend/lib/common/model.dart b/webapp/frontend/lib/common/model.dart index 3e416717e..847fdd839 100644 --- a/webapp/frontend/lib/common/model.dart +++ b/webapp/frontend/lib/common/model.dart @@ -59,6 +59,12 @@ class Amount implements Formattable { } } + Usd operator *(Price multiplier) { + Usd result = Usd.zero(); + result._usd = Decimal.parse((btc * multiplier.asDouble).toString()); + return result; + } + @override String formatted() { final formatter = NumberFormat("#,###,###,###,###", "en"); @@ -69,11 +75,6 @@ class Amount implements Formattable { String toString() { return formatSats(this); } - - String formatSats(Amount amount) { - final formatter = NumberFormat("#,###,###,###,###", "en"); - return "${formatter.format(amount.sats)} sats"; - } } class Usd implements Formattable { @@ -157,6 +158,18 @@ class Price implements Formattable { } } + Price operator +(Price other) { + Price result = Price.zero(); + result._usd = _usd + other._usd; + return result; + } + + Price operator /(Decimal divisor) { + Price result = Price.zero(); + result._usd = (_usd / divisor).toDecimal(); + return result; + } + @override String formatted() { final formatter = NumberFormat("#,##0.00", "en_US"); diff --git a/webapp/frontend/lib/common/scaffold_with_nav.dart b/webapp/frontend/lib/common/scaffold_with_nav.dart index a53e71002..9494fad93 100644 --- a/webapp/frontend/lib/common/scaffold_with_nav.dart +++ b/webapp/frontend/lib/common/scaffold_with_nav.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:get_10101/auth/auth_service.dart'; import 'package:get_10101/auth/login_screen.dart'; +import 'package:get_10101/common/amount_text.dart'; import 'package:get_10101/common/balance.dart'; +import 'package:get_10101/common/currency_change_notifier.dart'; +import 'package:get_10101/common/currency_selection_widget.dart'; import 'package:get_10101/common/model.dart'; import 'package:get_10101/common/snack_bar.dart'; import 'package:get_10101/common/version_service.dart'; @@ -175,6 +179,14 @@ class ScaffoldWithNavigationRail extends StatelessWidget { @override Widget build(BuildContext context) { + final quoteChangeNotifier = context.watch(); + final quote = quoteChangeNotifier.getBestQuote(); + final Price midMarket = + ((quote?.ask ?? Price.zero()) + (quote?.bid ?? Price.zero())) / Decimal.fromInt(2); + + final currencyChangeNotifier = context.watch(); + final currency = currencyChangeNotifier.currency; + return Scaffold( body: Row( children: [ @@ -185,7 +197,18 @@ class ScaffoldWithNavigationRail extends StatelessWidget { trailing: Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.end, - children: [Text("v$version"), const SizedBox(height: 50)], + children: [ + Row( + children: [ + Text("v$version"), + ], + ), + const SizedBox(height: 10), + const Row( + children: [CurrencySelectionScreen()], + ), + const SizedBox(height: 20), + ], ), ), leading: showAsDrawer @@ -241,10 +264,8 @@ class ScaffoldWithNavigationRail extends StatelessWidget { value: balance == null ? [] : [ - TextSpan( - text: balance?.offChain.formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), + formatAmountAsCurrency( + balance?.offChain, currency, midMarket), ]), const SizedBox(width: 30), TopBarItem( @@ -252,10 +273,7 @@ class ScaffoldWithNavigationRail extends StatelessWidget { value: balance == null ? [] : [ - TextSpan( - text: balance?.onChain.formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), + formatAmountAsCurrency(balance?.onChain, currency, midMarket), ]), const SizedBox(width: 30), TopBarItem( @@ -263,12 +281,10 @@ class ScaffoldWithNavigationRail extends StatelessWidget { value: balance == null ? [] : [ - TextSpan( - text: balance?.onChain - .add(balance?.offChain ?? Amount.zero()) - .formatted(), - style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: " sats"), + formatAmountAsCurrency( + balance?.onChain.add(balance?.offChain ?? Amount.zero()), + currency, + midMarket), ]), ], ), @@ -333,3 +349,21 @@ class TopBarItem extends StatelessWidget { ); } } + +TextSpan formatAmountAsCurrency(Amount? amount, Currency currency, Price midMarket) { + if (amount == null) { + return const TextSpan(); + } + + String formatted = ""; + switch (currency) { + case Currency.usd: + formatted = formatUsd(amount * midMarket, decimalPlaces: 2); + case Currency.btc: + formatted = formatBtc(amount); + case Currency.sats: + formatted = formatSats(amount); + } + + return TextSpan(text: formatted, style: const TextStyle(fontWeight: FontWeight.bold)); +} diff --git a/webapp/frontend/lib/main.dart b/webapp/frontend/lib/main.dart index 8e69f93b9..6ca7c8f2c 100644 --- a/webapp/frontend/lib/main.dart +++ b/webapp/frontend/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get_10101/auth/auth_service.dart'; +import 'package:get_10101/common/currency_change_notifier.dart'; import 'package:get_10101/common/version_service.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/routes.dart'; @@ -33,6 +34,7 @@ void main() { ChangeNotifierProvider(create: (context) => PositionChangeNotifier(const PositionService())), ChangeNotifierProvider(create: (context) => OrderChangeNotifier(const OrderService())), ChangeNotifierProvider(create: (context) => ChannelChangeNotifier(channelService)), + ChangeNotifierProvider(create: (context) => CurrencyChangeNotifier(Currency.sats)), Provider(create: (context) => const SettingsService()), Provider(create: (context) => channelService), Provider(create: (context) => AuthService()), diff --git a/webapp/frontend/lib/trade/position_table.dart b/webapp/frontend/lib/trade/position_table.dart index ce5acfba6..c79552f4c 100644 --- a/webapp/frontend/lib/trade/position_table.dart +++ b/webapp/frontend/lib/trade/position_table.dart @@ -1,5 +1,8 @@ +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; +import 'package:get_10101/common/amount_text.dart'; import 'package:get_10101/common/color.dart'; +import 'package:get_10101/common/currency_change_notifier.dart'; import 'package:get_10101/common/direction.dart'; import 'package:get_10101/common/model.dart'; import 'package:get_10101/settings/channel_change_notifier.dart'; @@ -25,8 +28,14 @@ class OpenPositionTable extends StatelessWidget { final positionChangeNotifier = context.watch(); final positions = positionChangeNotifier.getPositions(); + + final currencyChangeNotifier = context.watch(); + final currency = currencyChangeNotifier.currency; + final quoteChangeNotifier = context.watch(); final quote = quoteChangeNotifier.getBestQuote(); + final Price midMarket = + ((quote?.ask ?? Price.zero()) + (quote?.bid ?? Price.zero())) / Decimal.fromInt(2); if (positions == null) { return const Center(child: CircularProgressIndicator()); @@ -35,12 +44,12 @@ class OpenPositionTable extends StatelessWidget { if (positions.isEmpty) { return const Center(child: Text('No data available')); } else { - return buildTable(positions, quote, context, channel); + return buildTable(positions, quote, context, channel, midMarket, currency); } } - Widget buildTable( - List positions, BestQuote? bestQuote, BuildContext context, DlcChannel? channel) { + Widget buildTable(List positions, BestQuote? bestQuote, BuildContext context, + DlcChannel? channel, Price midMarket, Currency currency) { Widget actionReplacementLabel = createActionReplacementLabel(channel); return Table( border: const TableBorder(verticalInside: BorderSide(width: 0.5, color: Colors.black)), @@ -80,9 +89,9 @@ class OpenPositionTable extends StatelessWidget { : "+${position.quantity}")), buildTableCell(Text(position.averageEntryPrice.toString())), buildTableCell(Text(position.liquidationPrice.toString())), - buildTableCell(Text(position.collateral.toString())), + buildAmountTableCell(position.collateral, currency, midMarket), buildTableCell(Text(position.leverage.formatted())), - buildTableCell(Text(position.pnlSats.toString())), + buildAmountTableCell(position.pnlSats, currency, midMarket), buildTableCell( Text("${DateFormat('dd-MM-yyyy – HH:mm').format(position.expiry)} UTC")), buildTableCell(Center( @@ -208,4 +217,19 @@ class OpenPositionTable extends StatelessWidget { child: Container( padding: const EdgeInsets.all(10), alignment: Alignment.center, child: child)), )); + + TableCell buildAmountTableCell(Amount? child, Currency currency, Price midMarket) { + if (child == null) { + return buildTableCell(const Text("")); + } + + switch (currency) { + case Currency.usd: + return buildTableCell(Text(formatUsd(child * midMarket, decimalPlaces: 2))); + case Currency.btc: + return buildTableCell(Text(formatBtc(child))); + case Currency.sats: + return buildTableCell(Text(formatSats(child))); + } + } }