From bb12646584b924d71dd75ae70ce14b0b50d52b41 Mon Sep 17 00:00:00 2001 From: Ivo Fernandes Date: Sat, 16 Dec 2023 00:24:14 +0000 Subject: [PATCH] Improved UI --- .../ticker_details/lib/src/ticker_screen.dart | 49 ++--- .../state/big_picture_state_provider.dart | 5 + .../ui/resume/strategy_resume_details_ui.dart | 35 +++- .../ui/resume/strategy_resume_header_ui.dart | 175 +++++++++++------- .../ui/resume/strategy_resume_ui.dart | 25 ++- lib/home/home_screen.dart | 91 ++++----- lib/settings/settings_screen.dart | 2 +- packages/app_dependencies/pubspec.yaml | 4 +- .../src/ui/price/price_variation_chip_ui.dart | 24 ++- pubspec.lock | 8 +- 10 files changed, 262 insertions(+), 156 deletions(-) diff --git a/app_modules/ticker_details/lib/src/ticker_screen.dart b/app_modules/ticker_details/lib/src/ticker_screen.dart index 6fd9bb7..a0a7ece 100644 --- a/app_modules/ticker_details/lib/src/ticker_screen.dart +++ b/app_modules/ticker_details/lib/src/ticker_screen.dart @@ -13,30 +13,33 @@ class TickerScreen extends StatelessWidget { }); @override - Widget build(BuildContext context) => FutureBuilder( - future: YahooFinanceService().getTickerData(ticker.symbol), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == null) { - return const Center( - child: CircularProgressIndicator(), - ); - } + Widget build(BuildContext context) => GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + child: FutureBuilder( + future: YahooFinanceService().getTickerData(ticker.symbol), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } - return ChangeNotifierProvider( - create: (context) => TickerStateProvider(), - child: Consumer( - builder: (context, tickerState, child) { - tickerState.startAnalysis(snapshot.data as List); + return ChangeNotifierProvider( + create: (context) => TickerStateProvider(), + child: Consumer( + builder: (context, tickerState, child) { + tickerState.startAnalysis(snapshot.data as List); - return SafeArea( - child: TickerTabs( - ticker, - snapshot.data as List, - ), - ); - }, - ), - ); - }, + return SafeArea( + child: TickerTabs( + ticker, + snapshot.data as List, + ), + ); + }, + ), + ); + }, + ), ); } diff --git a/lib/big_picture/state/big_picture_state_provider.dart b/lib/big_picture/state/big_picture_state_provider.dart index 73f2691..820ff71 100644 --- a/lib/big_picture/state/big_picture_state_provider.dart +++ b/lib/big_picture/state/big_picture_state_provider.dart @@ -68,6 +68,11 @@ class BigPictureStateProvider with ChangeNotifier, ConnectivityState { } Future addTicker(StockTicker ticker) async { + // Treat multiple strategies + ticker = ticker.copyWith( + symbol: ticker.symbol.split(' ').map((String s) => s.trim()).toList().join(', '), + ); + _bigPictureData[ticker] = BuyAndHoldStrategyResult(); // Get data from yahoo finance diff --git a/lib/big_picture/ui/resume/strategy_resume_details_ui.dart b/lib/big_picture/ui/resume/strategy_resume_details_ui.dart index f7b0fab..b655478 100644 --- a/lib/big_picture/ui/resume/strategy_resume_details_ui.dart +++ b/lib/big_picture/ui/resume/strategy_resume_details_ui.dart @@ -22,12 +22,35 @@ class StrategyResumeDetails extends StatelessWidget { final double sma20sma50 = (strategy.movingAverages[20]! / strategy.movingAverages[50]! - 1) * 100; final double sma50sma200 = (strategy.movingAverages[50]! / strategy.movingAverages[200]! - 1) * 100; + String prefixVar20 = ''; + String prefixVar50 = ''; + String prefixVar200 = ''; + + if (!bigPictureState.isCompactView()) { + prefixVar20 = 'price/sma20'; + prefixVar50 = 'sma20/sma50'; + prefixVar200 = 'sma50/sma200'; + } + + final priceVarChip20 = PriceVariationChip( + prefix: prefixVar20, + value: pricesma20, + ); + final priceVarChip50 = PriceVariationChip( + prefix: prefixVar50, + value: sma20sma50, + ); + final priceVarChip200 = PriceVariationChip( + prefix: prefixVar200, + value: sma50sma200, + ); + return bigPictureState.isCompactView() ? Column( children: [ - PriceVariationChip(null, pricesma20), - PriceVariationChip(null, sma20sma50), - PriceVariationChip(null, sma50sma200) + priceVarChip20, + priceVarChip50, + priceVarChip200, ], ) : Row( @@ -73,9 +96,9 @@ class StrategyResumeDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - PriceVariationChip('price/sma20', pricesma20), - PriceVariationChip('sma20/sma50', sma20sma50), - PriceVariationChip('sma50/sma200', sma50sma200) + priceVarChip20, + priceVarChip50, + priceVarChip200, ], ) ], diff --git a/lib/big_picture/ui/resume/strategy_resume_header_ui.dart b/lib/big_picture/ui/resume/strategy_resume_header_ui.dart index 1fb2e9b..17581b3 100644 --- a/lib/big_picture/ui/resume/strategy_resume_header_ui.dart +++ b/lib/big_picture/ui/resume/strategy_resume_header_ui.dart @@ -1,95 +1,140 @@ +import 'package:app_dependencies/app_dependencies.dart'; import 'package:flutter/material.dart'; -import 'package:interactive_i18n/interactive_i18n.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; -import 'package:stock_market_data/stock_market_data.dart'; import 'package:td_ui/td_ui.dart'; import 'package:ticker_search/ticker_search.dart'; import 'package:turing_deal/big_picture/state/big_picture_state_provider.dart'; import 'package:turing_deal/home/home_screen.dart'; +/// A widget to display the header information for a strategy resume. +/// +/// This includes the stock ticker information, strategy results, and an option +/// to add more details or change the current selection. class StrategyResumeHeader extends StatelessWidget { final StockTicker ticker; - final BuyAndHoldStrategyResult? strategy; + final BuyAndHoldStrategyResult strategy; final double cardWidth; - const StrategyResumeHeader( - this.ticker, - this.strategy, - this.cardWidth, - ); + final double variation; + + final Color color; + + /// Constructs a [StrategyResumeHeader] widget. + /// + /// Takes in [ticker] for stock information, [strategy] for strategy results, + /// and [cardWidth] to determine the width of the card. + const StrategyResumeHeader({ + required this.ticker, + required this.strategy, + required this.cardWidth, + required this.variation, + required this.color, + super.key, + }); @override Widget build(BuildContext context) { final BigPictureStateProvider bigPictureState = Provider.of(context, listen: false); - final ThemeData theme = Theme.of(context); + final String tickerDescription = TickerResolve.getTickerDescription(ticker); - final String ticketDescription = TickerResolve.getTickerDescription(ticker); + final double titleWidth = bigPictureState.isCompactView() ? cardWidth - 30 : cardWidth / 2 - 20; - final TickerSearch tickerSearch = TickerSearch(searchFieldLabel: 'Add'.t, suggestions: HomeScreen.suggestions); + final PriceVariationChip priceVariationChip = PriceVariationChip( + value: variation, + minColor: theme.colorScheme.error, + maxColor: theme.colorScheme.primary, + minValue: -2.5, + maxValue: 2.5, + ); return Stack( alignment: Alignment.topCenter, children: [ - Column(children: [ - Row( - mainAxisAlignment: - bigPictureState.isCompactView() ? MainAxisAlignment.center : MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: bigPictureState.isCompactView() ? cardWidth - 30 : cardWidth / 2 - 20, - child: Text( - ticker.symbol, - style: theme.textTheme.titleLarge, - textAlign: bigPictureState.isCompactView() ? TextAlign.center : TextAlign.start, - ), - ), - bigPictureState.isCompactView() - ? Container() - : SizedBox( - width: cardWidth / 2 - 20, - child: Align( - alignment: Alignment.centerRight, - child: Text(ticketDescription, style: theme.textTheme.bodyLarge), - )) - ], - ), - Divider( - height: 5, - color: theme.textTheme.bodyLarge!.color, - ), - bigPictureState.isCompactView() ? Container() : const SizedBox(height: 10), - bigPictureState.isCompactView() - ? Container() - : Row( + Column( + children: [ + if (!bigPictureState.isCompactView()) + Container( + margin: EdgeInsets.symmetric(horizontal: 5), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('${strategy!.tradingYears.ceil()}y${(strategy!.tradingYears % 1 * 12).ceil()}m'), - strategy!.startDate != null && strategy!.endDate != null - ? Text( - '${DateFormat.yMd().format(strategy!.startDate!)} to ${DateFormat.yMd().format(strategy!.endDate!)}') - : Container() + _buildTickerTitle(titleWidth, theme, TextAlign.start), + SizedBox( + width: cardWidth / 2 - 30, + child: Align( + alignment: Alignment.centerRight, + child: Text(tickerDescription, style: theme.textTheme.bodyLarge), + ), + ), ], ), - bigPictureState.isCompactView() ? Container() : const SizedBox(height: 10) - ]), - bigPictureState.isCompactView() - ? Container() - : InkWell( - child: Container( - color: Colors.lightBlue.withOpacity(0), - padding: EdgeInsets.only(left: 40, right: 40, bottom: AppTheme.isDesktopWeb() ? 0 : 30), - child: const Icon( - Icons.add, - )), - onTap: () async { - final List? tickers = await showSearch(context: context, delegate: tickerSearch); - await bigPictureState.joinTicker(ticker, tickers); - await bigPictureState.persistTickers(); - }, ), + if (bigPictureState.isCompactView()) _buildTickerTitle(titleWidth, theme, TextAlign.center), + if (bigPictureState.isCompactView()) priceVariationChip, + if (!bigPictureState.isCompactView()) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('\$${strategy.endPrice.toStringAsFixed(2)}'), + priceVariationChip, + ], + ), + Divider(height: 5, color: theme.textTheme.bodyLarge?.color), + if (!bigPictureState.isCompactView()) + Column( + children: [ + const SizedBox(height: 10), + _buildDateRow(), + const SizedBox(height: 10), + ], + ), + ], + ), + if (!bigPictureState.isCompactView()) _buildAddButton(bigPictureState, context), ], ); } + + /// Ticker symbol + Widget _buildTickerTitle(double width, ThemeData theme, TextAlign textAlign) => Center( + child: SizedBox( + width: width, + child: Text( + ticker.symbol, + style: theme.textTheme.titleLarge, + textAlign: textAlign, + ), + ), + ); + + /// Builds the row displaying the strategy duration. + /// + /// Visible only in the expanded view mode. + Widget _buildDateRow() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${strategy!.tradingYears.ceil()}y${(strategy!.tradingYears % 1 * 12).ceil()}m'), + if (strategy?.startDate != null && strategy?.endDate != null) + Text('${DateFormat.yMd().format(strategy!.startDate!)} to ${DateFormat.yMd().format(strategy!.endDate!)}'), + ], + ); + + /// Builds the add button for adding more details to the strategy. + /// + /// Visible only in the expanded view mode. + Widget _buildAddButton(BigPictureStateProvider bigPictureState, BuildContext context) => InkWell( + child: Container( + color: Colors.lightBlue.withOpacity(0), + padding: EdgeInsets.only(left: 40, right: 40, bottom: AppTheme.isDesktopWeb() ? 0 : 30), + child: const Icon(Icons.add), + ), + onTap: () async { + final List? tickers = await showSearch( + context: context, delegate: TickerSearch(searchFieldLabel: 'Add'.t, suggestions: HomeScreen.suggestions)); + if (tickers != null) { + await bigPictureState.joinTicker(ticker, tickers); + await bigPictureState.persistTickers(); + } + }, + ); } diff --git a/lib/big_picture/ui/resume/strategy_resume_ui.dart b/lib/big_picture/ui/resume/strategy_resume_ui.dart index 5ec749a..3174aff 100644 --- a/lib/big_picture/ui/resume/strategy_resume_ui.dart +++ b/lib/big_picture/ui/resume/strategy_resume_ui.dart @@ -1,9 +1,7 @@ import 'dart:ui'; +import 'package:app_dependencies/app_dependencies.dart'; import 'package:flutter/material.dart'; -import 'package:pinch_zoom_release_unzoom/pinch_zoom_release_unzoom.dart'; -import 'package:provider/provider.dart'; -import 'package:stock_market_data/stock_market_data.dart'; import 'package:ticker_details/ticker_details.dart'; import 'package:turing_deal/big_picture/state/big_picture_state_provider.dart'; import 'package:turing_deal/big_picture/ui/resume/strategy_resume_details_ui.dart'; @@ -44,9 +42,18 @@ class StrategyResume extends StatelessWidget { if (bigPictureState.isCompactView()) { cardWidth /= 3; cardWidth -= 5; - print('card width: $cardWidth'); } + final double variation = (strategy.endPrice / strategy.yesterdayPrice - 1) * 100; + final ColorScaleWidget colorScaleWidget = ColorScaleWidget( + value: variation, + minColor: Theme.of(context).colorScheme.error, + maxColor: Theme.of(context).colorScheme.primary, + minValue: -2.5, + maxValue: 2.5, + ); + final Color color = colorScaleWidget.getColorForValue(variation); + // Main widget structure return Dismissible( key: GlobalKey(), @@ -76,7 +83,7 @@ class StrategyResume extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Theme.of(context).colorScheme.primary, + color: color, blurRadius: 2, offset: const Offset(2, 2), // Adjust the offset to control the shadow direction ), @@ -99,7 +106,13 @@ class StrategyResume extends StatelessWidget { child: strategy.progress > 0 ? Column( children: [ - StrategyResumeHeader(ticker, strategy, cardWidth), + StrategyResumeHeader( + ticker: ticker, + strategy: strategy, + cardWidth: cardWidth, + variation: variation, + color: color, + ), StrategyResumeDetails(strategy), strategy.progress < 100 ? const CircularProgressIndicator() : Container() ], diff --git a/lib/home/home_screen.dart b/lib/home/home_screen.dart index 364a1c1..5239aac 100644 --- a/lib/home/home_screen.dart +++ b/lib/home/home_screen.dart @@ -60,63 +60,66 @@ class _HomeScreenState extends State { MyAppContext.context = context; - return MyScaffold( - nav: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: () => Navigator.of(context).pushNamed(SettingsScreen.route), - icon: const Icon(Icons.menu), - ), - IconButton( - onPressed: () { - onItemSelected(0); - }, - icon: const Icon( - Icons.show_chart, + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + child: MyScaffold( + nav: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: () => Navigator.of(context).pushNamed(SettingsScreen.route), + icon: const Icon(Icons.menu), ), - ), - if (Environment.hasPortfolio) IconButton( onPressed: () { - onItemSelected(1); + onItemSelected(0); }, icon: const Icon( - Icons.work, + Icons.show_chart, ), ), - IconButton( - icon: const Icon(Icons.search), - onPressed: () async { - final List? tickers = await showSearch( - context: context, - delegate: TickerSearch( - searchFieldLabel: 'Search'.t, - suggestions: HomeScreen.suggestions, + if (Environment.hasPortfolio) + IconButton( + onPressed: () { + onItemSelected(1); + }, + icon: const Icon( + Icons.work, ), - ); + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: () async { + final List? tickers = await showSearch( + context: context, + delegate: TickerSearch( + searchFieldLabel: 'Search'.t, + suggestions: HomeScreen.suggestions, + ), + ); - appState.search(tickers); - }, - ), - ], - ), - body: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _pageViewController, - children: [ - Center( - child: BigPictureScreen( - key: UniqueKey(), + appState.search(tickers); + }, ), - ), - if (Environment.hasPortfolio) + ], + ), + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageViewController, + children: [ Center( - child: PortfolioScreen( - stocks: bigPictureState.getBigPictureData(), + child: BigPictureScreen( + key: UniqueKey(), ), ), - ], + if (Environment.hasPortfolio) + Center( + child: PortfolioScreen( + stocks: bigPictureState.getBigPictureData(), + ), + ), + ], + ), ), ); } diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index d7603b5..deeaabb 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -33,7 +33,7 @@ class SettingsScreen extends StatelessWidget { await appState.setIsDark(false); appState.refresh(); }, - icon: const Icon(Icons.brightness_5), + icon: const Icon(Icons.light_mode), ) : IconButton( onPressed: () async { diff --git a/packages/app_dependencies/pubspec.yaml b/packages/app_dependencies/pubspec.yaml index b07067e..f33c7ec 100644 --- a/packages/app_dependencies/pubspec.yaml +++ b/packages/app_dependencies/pubspec.yaml @@ -17,14 +17,14 @@ dependencies: sembast_web: ^2.2.0 cupertino_icons: ^1.0.5 intl: ^0.17.0 - url_launcher: ^6.2.1 + url_launcher: ^6.2.2 connectivity_plus: ^5.0.2 interactive_chart: ^0.3.4 shared_preferences: ^2.2.2 country_icons: ^2.0.2 stats: ^2.1.0 pinch_zoom_release_unzoom: ^1.0.1 - stock_market_data: ^0.0.9 + stock_market_data: ^0.0.12 yahoo_finance_data_reader: ^1.0.8 color_scale: ^0.0.4 anchor_tabs: ^0.0.9 diff --git a/packages/td_ui/lib/src/ui/price/price_variation_chip_ui.dart b/packages/td_ui/lib/src/ui/price/price_variation_chip_ui.dart index 857ecea..25a0727 100644 --- a/packages/td_ui/lib/src/ui/price/price_variation_chip_ui.dart +++ b/packages/td_ui/lib/src/ui/price/price_variation_chip_ui.dart @@ -2,12 +2,22 @@ import 'package:app_dependencies/app_dependencies.dart'; import 'package:flutter/material.dart'; class PriceVariationChip extends StatelessWidget { - final String? prefix; + final String prefix; final double value; - const PriceVariationChip( - this.prefix, - this.value, { + final Color minColor; + final double minValue; + + final Color maxColor; + final double maxValue; + + const PriceVariationChip({ + this.prefix = '', + this.value = 0, + this.minColor = Colors.red, + this.minValue = -20, + this.maxColor = Colors.green, + this.maxValue = 20, super.key, }); @@ -20,7 +30,7 @@ class PriceVariationChip extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.end, children: [ - prefix != null + prefix != '' ? Text( '${prefix!}: ', style: Theme.of(context).textTheme.bodyText2!.copyWith(fontSize: 10), @@ -33,6 +43,10 @@ class PriceVariationChip extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(20)), child: ColorScaleWidget( value: value, + minColor: minColor, + minValue: minValue, + maxColor: maxColor, + maxValue: maxValue, child: Container( padding: const EdgeInsets.all(5), alignment: Alignment.bottomRight, diff --git a/pubspec.lock b/pubspec.lock index 898bd6b..df4bb95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -617,10 +617,10 @@ packages: dependency: transitive description: name: stock_market_data - sha256: "103eee141d95345e4c0f90e0f5c2a654d9a9ba1d3b4df9fad93465d0e3448e29" + sha256: "84c5209097fa1b1190925d0f322c123532c262369b57b49a60d72cbeb9da9674" url: "https://pub.dev" source: hosted - version: "0.0.9" + version: "0.0.12" stocks_portfolio: dependency: "direct main" description: @@ -718,10 +718,10 @@ packages: dependency: transitive description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.2" url_launcher_android: dependency: transitive description: