From 94751b80156f5d9b15b02c9bf6408d7f8f5b8759 Mon Sep 17 00:00:00 2001 From: BrightDV <92821484+BrightDV@users.noreply.github.com> Date: Wed, 22 May 2024 20:24:57 +0200 Subject: [PATCH] Official API support (#148) * [results] first steps to new api support * [results] add support for race results from official api; add switch in the settings * [results] support offline save * [results] support qualifications; better offline save check * [results] add support for free practice * [results] support sprint & sprint qualifyings * [results] basic requests for drivers and teams standings * [schedule] add basic request template * [schedule] support official api (needs gmt offset parsing) * [schedule] support route from schedule to circuit screen with official api * [circuit] support seeing results when coming from official api; add timezone support * [race] prefer date saved offline for the race countdown * [sprint] fix results time/gap * [sprint] fix results view cutted * [standings] add support for official api * [results] overall improvements; fix offline race logic * [standings] fix car image regression * [results] supports race fastest lap; localize the settings with link to details page * [circuit] move dates parsing; code cleanup --- lib/Screens/circuit.dart | 134 ++-- lib/Screens/driver_details.dart | 16 +- lib/Screens/free_practice_screen.dart | 33 +- lib/Screens/race_details.dart | 425 +++++++----- lib/Screens/racehub.dart | 2 + lib/Screens/schedule.dart | 50 +- lib/Screens/session_screen.dart | 1 + lib/Screens/settings.dart | 43 +- lib/Screens/standings.dart | 59 +- lib/Screens/team_details.dart | 13 +- lib/api/article_parts.dart | 4 +- lib/api/driver_components.dart | 82 +-- lib/api/ergast.dart | 80 ++- lib/api/event_tracker.dart | 37 +- lib/api/formula1.dart | 610 ++++++++++++++++++ lib/api/race_components.dart | 241 ++++--- lib/api/team_components.dart | 205 +++--- lib/helpers/circuit_points.dart | 1 - .../convert_ergast_and_formula_one.dart | 47 ++ lib/helpers/driver_image.dart | 8 +- lib/helpers/driver_result_item.dart | 117 ++-- lib/helpers/news.dart | 1 + lib/helpers/racetracks_url.dart | 1 + lib/helpers/team_car_image.dart | 2 +- lib/l10n/app_en.arb | 4 + lib/l10n/app_fr.arb | 4 + lib/scraping/formula_one.dart | 14 +- 27 files changed, 1668 insertions(+), 566 deletions(-) diff --git a/lib/Screens/circuit.dart b/lib/Screens/circuit.dart index a07b86e..880d712 100644 --- a/lib/Screens/circuit.dart +++ b/lib/Screens/circuit.dart @@ -17,8 +17,6 @@ * Copyright (c) 2022-2024, BrightDV */ -import 'dart:async'; - import 'package:boxbox/Screens/article.dart'; import 'package:boxbox/Screens/race_details.dart'; import 'package:boxbox/api/ergast.dart'; @@ -52,6 +50,8 @@ class CircuitScreen extends StatelessWidget { @override Widget build(BuildContext context) { + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); return Scaffold( body: isFetched ?? true ? NestedScrollView( @@ -76,9 +76,12 @@ class CircuitScreen extends StatelessWidget { body: SingleChildScrollView( child: FutureBuilder( future: EventTracker().getCircuitDetails( - Convert().circuitIdFromErgastToFormulaOne( - race.circuitId, - ), + scheduleLastSavedFormat == 'ergast' + ? Convert().circuitIdFromErgastToFormulaOne( + race.circuitId, + ) + : race.meetingId, + race: scheduleLastSavedFormat == 'ergast' ? null : race, ), builder: (context, snapshot) => snapshot.hasData ? Column( @@ -92,7 +95,9 @@ class CircuitScreen extends StatelessWidget { Icons.arrow_forward_rounded, ), RaceDetailsScreen( - race, + scheduleLastSavedFormat == 'ergast' + ? race + : snapshot.data!['raceCustomBBParameter'], snapshot.data!['meetingContext']['timetables'] [2]['session'] == 's', @@ -131,14 +136,25 @@ class CircuitScreen extends StatelessWidget { ), snapshot.data!['raceResults'] != null && snapshot.data!['raceResults'].isNotEmpty - ? RaceResults(snapshot, race) + ? RaceResults( + snapshot, + scheduleLastSavedFormat == 'ergast' + ? race + : snapshot + .data!['raceCustomBBParameter'], + ) : Container(), snapshot.data!['curatedSection'] != null ? CuratedSection( - snapshot.data!['curatedSection']['items']) + snapshot.data!['curatedSection']['items'], + ) : Container(), TrackLayoutImage(race), - CircuitFactsAndHistory(race.circuitId), + CircuitFactsAndHistory( + race.detailsPath != null + ? race.detailsPath! + : race.circuitId, + ), ], ) : BoxBoxButton( @@ -205,7 +221,10 @@ class CircuitScreen extends StatelessWidget { Icons.arrow_forward_rounded, ), RaceDetailsScreen( - race, + scheduleLastSavedFormat == 'ergast' + ? race + : snapshot.data![ + 'raceCustomBBParameter'], snapshot.data!['meetingContext'] ['timetables'][2] ['session'] == @@ -251,7 +270,14 @@ class CircuitScreen extends StatelessWidget { snapshot.data!['raceResults'] != null && snapshot.data!['raceResults'] .isNotEmpty - ? RaceResults(snapshot, race) + ? RaceResults( + snapshot, + scheduleLastSavedFormat == + 'ergast' + ? race + : snapshot.data![ + 'raceCustomBBParameter'], + ) : Container(), snapshot.data!['curatedSection'] != null ? CuratedSection( @@ -259,7 +285,11 @@ class CircuitScreen extends StatelessWidget { ['items']) : Container(), TrackLayoutImage(race), - CircuitFactsAndHistory(race.circuitId), + CircuitFactsAndHistory( + race.detailsPath != null + ? race.detailsPath! + : race.circuitId, + ), ], ) : BoxBoxButton( @@ -281,42 +311,66 @@ class CircuitScreen extends StatelessWidget { } } +// top image behind the title in the sliver appbar class RaceImageProvider extends StatelessWidget { - Future getCircuitImageUrl(Race race) async { - return await RaceTracksUrls().getRaceTrackImageUrl(race.circuitId); + String getCircuitImageUrl(Race race) { + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); + if (scheduleLastSavedFormat == 'ergast') { + return RaceTracksUrls().getRaceTrackImageUrl(race.circuitId); + } else { + String coverUrl = race.raceCoverUrl!; + if (race.country == 'Great Britain') { + coverUrl = + race.raceCoverUrl!.replaceFirst('United_Kingdom', 'Great_Britain'); + } else if (race.circuitName == 'Monza') { + coverUrl = race.raceCoverUrl!.replaceFirst('Italy', 'Monza'); + } else if (race.circuitName == 'Las Vegas') { + coverUrl = + 'https://media.formula1.com/image/upload/f_auto/q_auto/v1677238736/content/dam/fom-website/2018-redesign-assets/Racehub%20header%20images%2016x9/Las Vegas.jpg.transform/fullbleed/image.jpg'; + } else if (race.country == 'Abu Dhabi') { + coverUrl = race.raceCoverUrl! + .replaceFirst('United_Arab_Emirates', 'Abu_Dhabi'); + } else if (race.country == 'Miami') { + coverUrl = race.raceCoverUrl!.replaceFirst('United_States', 'Miami'); + } + return coverUrl; + } } final Race race; const RaceImageProvider(this.race, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return FutureBuilder( - future: getCircuitImageUrl(race), - builder: (context, snapshot) { - if (snapshot.hasError) { - return RequestErrorWidget( - snapshot.error.toString(), - ); - } - return snapshot.hasData - ? CachedNetworkImage( - errorWidget: (context, url, error) => - const Icon(Icons.error_outlined), - fadeOutDuration: const Duration(seconds: 1), - fadeInDuration: const Duration(seconds: 1), - fit: BoxFit.cover, - imageUrl: snapshot.data!, - placeholder: (context, url) => const LoadingIndicatorUtil(), - ) - : const LoadingIndicatorUtil(); - }, + return CachedNetworkImage( + errorWidget: (context, url, error) => const Icon(Icons.error_outlined), + fadeOutDuration: const Duration(seconds: 1), + fadeInDuration: const Duration(seconds: 1), + fit: BoxFit.cover, + imageUrl: getCircuitImageUrl(race), + placeholder: (context, url) => const LoadingIndicatorUtil(), ); } } +// track layout in color above the history and facts section class TrackLayoutImage extends StatelessWidget { String getTrackLayoutImageUrl(Race race) { - return RaceTracksUrls().getTrackLayoutImageUrl(race.circuitId); + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); + String country = race.country; + if (country == 'Monaco') { + country = 'Monoco'; + } else if (country == 'Azerbaijan') { + country = 'Baku'; + } else if (race.circuitName == 'Austin') { + country = 'USA'; + } + return scheduleLastSavedFormat == 'ergast' + ? RaceTracksUrls().getTrackLayoutImageUrl( + race.circuitId, + ) + : 'https://media.formula1.com/image/upload/f_auto/q_auto/v1677244987/content/dam/fom-website/2018-redesign-assets/Circuit%20maps%2016x9/${country.replaceAll("-", "_").replaceAll(" ", "_")}_Circuit.png'; } final Race race; @@ -658,13 +712,17 @@ class CircuitFactsAndHistory extends StatelessWidget { Widget build(BuildContext context) { bool useDarkMode = Hive.box('settings').get('darkMode', defaultValue: true) as bool; + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); return Padding( padding: const EdgeInsets.all(10), child: FutureBuilder( future: FormulaOneScraper().scrapeCircuitFactsAndHistory( - Convert().circuitNameFromErgastToFormulaOneForRaceHub( - circuitId, - ), + scheduleLastSavedFormat == 'ergast' + ? Convert().circuitNameFromErgastToFormulaOneForRaceHub( + circuitId, + ) + : circuitId, context, ), builder: (context, snapshot) => snapshot.hasData diff --git a/lib/Screens/driver_details.dart b/lib/Screens/driver_details.dart index c0caf36..f695e52 100644 --- a/lib/Screens/driver_details.dart +++ b/lib/Screens/driver_details.dart @@ -38,11 +38,13 @@ class DriverDetailsScreen extends StatelessWidget { final String driverId; final String givenName; final String familyName; + final String? detailsPath; const DriverDetailsScreen( this.driverId, this.givenName, this.familyName, { super.key, + this.detailsPath, }); @override @@ -82,7 +84,7 @@ class DriverDetailsScreen extends StatelessWidget { ), body: TabBarView( children: [ - DriverInfo(driverId), + DriverInfo(driverId, detailsPath: detailsPath), DriverResults(driverId), ], ), @@ -93,7 +95,12 @@ class DriverDetailsScreen extends StatelessWidget { class DriverInfo extends StatelessWidget { final String driverId; - const DriverInfo(this.driverId, {super.key}); + final String? detailsPath; + const DriverInfo( + this.driverId, { + super.key, + this.detailsPath, + }); @override Widget build(BuildContext context) { @@ -106,7 +113,10 @@ class DriverInfo extends StatelessWidget { child: DriverImageProvider(driverId, 'driver'), ), FutureBuilder>( - future: FormulaOneScraper().scrapeDriversDetails(driverId), + future: FormulaOneScraper().scrapeDriversDetails( + driverId, + detailsPath, + ), builder: (context, snapshot) => snapshot.hasError ? RequestErrorWidget( snapshot.error.toString(), diff --git a/lib/Screens/free_practice_screen.dart b/lib/Screens/free_practice_screen.dart index 4f03242..87641e0 100644 --- a/lib/Screens/free_practice_screen.dart +++ b/lib/Screens/free_practice_screen.dart @@ -18,6 +18,7 @@ */ import 'package:boxbox/api/driver_components.dart'; +import 'package:boxbox/api/formula1.dart'; import 'package:boxbox/helpers/request_error.dart'; import 'package:boxbox/helpers/loading_indicator_util.dart'; import 'package:boxbox/helpers/team_background_color.dart'; @@ -33,6 +34,7 @@ class FreePracticeScreen extends StatelessWidget { final String sessionTitle; final int sessionIndex; final String circuitId; + final String meetingId; final int raceYear; final String raceName; final String? raceUrl; @@ -41,6 +43,7 @@ class FreePracticeScreen extends StatelessWidget { this.sessionTitle, this.sessionIndex, this.circuitId, + this.meetingId, this.raceYear, this.raceName, { Key? key, @@ -63,12 +66,14 @@ class FreePracticeScreen extends StatelessWidget { false, raceUrl: raceUrl, ) - : FormulaOneScraper().scrapeFreePracticeResult( + : Formula1().getFreePracticeStandings(meetingId, sessionIndex), + // disable scraping for the moment + /* FormulaOneScraper().scrapeFreePracticeResult( circuitId, sessionIndex, 'practice-$sessionIndex', true, - ), + ), */ builder: (context, snapshot) => snapshot.hasError ? snapshot.error.toString() == 'RangeError: Value not in range: 0' ? Center( @@ -85,16 +90,14 @@ class FreePracticeScreen extends StatelessWidget { '\n' + snapshot.stackTrace.toString(), ) - : snapshot.hasError - ? RequestErrorWidget(snapshot.error.toString()) - : snapshot.hasData - ? FreePracticeResultsList( - snapshot.data!, - raceYear, - raceName, - sessionIndex, - ) - : const LoadingIndicatorUtil(), + : snapshot.hasData + ? FreePracticeResultsList( + snapshot.data!, + raceYear, + raceName, + sessionIndex, + ) + : const LoadingIndicatorUtil(), ), ); } @@ -334,7 +337,11 @@ class FreePracticeResultItem extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(top: 5, bottom: 5), child: Text( - result.fastestLap == '' ? '--' : result.fastestLap, + index == 0 + ? '' + : result.fastestLap == '' + ? '--' + : result.fastestLap, textAlign: TextAlign.center, ), ), diff --git a/lib/Screens/race_details.dart b/lib/Screens/race_details.dart index efcba4b..16cda48 100644 --- a/lib/Screens/race_details.dart +++ b/lib/Screens/race_details.dart @@ -23,16 +23,16 @@ import 'package:add_2_calendar/add_2_calendar.dart'; import 'package:boxbox/Screens/FormulaYou/settings.dart'; import 'package:boxbox/api/driver_components.dart'; import 'package:boxbox/api/ergast.dart'; +import 'package:boxbox/api/formula1.dart'; import 'package:boxbox/api/race_components.dart'; import 'package:boxbox/helpers/convert_ergast_and_formula_one.dart'; import 'package:boxbox/helpers/driver_result_item.dart'; import 'package:boxbox/helpers/loading_indicator_util.dart'; -import 'package:boxbox/helpers/racetracks_url.dart'; import 'package:boxbox/helpers/request_error.dart'; +import 'package:boxbox/Screens/circuit.dart'; import 'package:boxbox/Screens/free_practice_screen.dart'; import 'package:boxbox/helpers/team_background_color.dart'; import 'package:boxbox/scraping/formula_one.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -154,10 +154,8 @@ class RaceDetailsScreen extends StatelessWidget { removeTop: true, child: SafeArea( child: QualificationResultsProvider( - raceUrl: - 'https://www.formula1.com/en/results.html/${DateTime.now().year}/races/${Convert().circuitIdFromErgastToFormulaOne(race.circuitId)}/${Convert().circuitNameFromErgastToFormulaOne(race.circuitId)}/sprint-qualifying.html', - hasSprint: hasSprint, race: race, + hasSprint: hasSprint, isSprintQualifying: true, ), ), @@ -296,6 +294,8 @@ class _FreePracticesResultsProviderState @override Widget build(BuildContext context) { + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); final Race race = widget.race; final bool hasSprint = widget.hasSprint; @@ -304,79 +304,139 @@ class _FreePracticesResultsProviderState AppLocalizations.of(context)!.freePracticeTwo, AppLocalizations.of(context)!.freePracticeThree, ]; - return FutureBuilder( - future: FormulaOneScraper().whichSessionsAreFinised( - Convert().circuitIdFromErgastToFormulaOne(race.circuitId), - Convert().circuitNameFromErgastToFormulaOne(race.circuitId), - ), - builder: (context, snapshot) => snapshot.hasError - ? RequestErrorWidget( - snapshot.error.toString(), - ) - : snapshot.hasData - ? race.sessionDates.isEmpty // TODO: update when Ergast not down - ? Padding( - padding: const EdgeInsets.all(15), - child: Center( - child: Text( - AppLocalizations.of(context)!.dataNotAvailable, - textAlign: TextAlign.center, + if (scheduleLastSavedFormat == 'ergast') { + return FutureBuilder( + future: FormulaOneScraper().whichSessionsAreFinised( + Convert().circuitIdFromErgastToFormulaOne(race.circuitId), + Convert().circuitNameFromErgastToFormulaOne(race.circuitId), + ), + builder: (context, snapshot) => snapshot.hasError + ? RequestErrorWidget( + snapshot.error.toString(), + ) + : snapshot.hasData + ? race.sessionDates.isEmpty // TODO: update when Ergast not down + ? Padding( + padding: const EdgeInsets.all(15), + child: Center( + child: Text( + AppLocalizations.of(context)!.dataNotAvailable, + textAlign: TextAlign.center, + ), ), - ), - ) - : ListView.builder( - itemCount: hasSprint ? 1 : 3, - itemBuilder: (context, index) => snapshot.data! > index - ? ListTile( - title: Text( - sessionsTitle[index], - textAlign: TextAlign.center, - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FreePracticeScreen( - sessionsTitle[index], - index + 1, - race.circuitId, - int.parse( - race.date.split('-')[2], + ) + : ListView.builder( + itemCount: hasSprint ? 1 : 3, + itemBuilder: (context, index) => snapshot.data! > index + ? ListTile( + title: Text( + sessionsTitle[index], + textAlign: TextAlign.center, + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FreePracticeScreen( + sessionsTitle[index], + index + 1, + race.circuitId, + race.meetingId, + int.parse( + race.date.split('-')[2], + ), + race.raceName, ), - race.raceName, ), ), - ), - ) - : Padding( - padding: const EdgeInsets.only(top: 25), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - sessionsTitle[index], - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, + ) + : Padding( + padding: const EdgeInsets.only(top: 25), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + sessionsTitle[index], + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), ), - ), - SessionCountdownTimer( - race, - index, - sessionsTitle[index], - update: update, - ), - Padding( - padding: const EdgeInsets.only( - top: 25, + SessionCountdownTimer( + race, + index, + sessionsTitle[index], + update: update, ), - child: Divider(), - ), - ], + Padding( + padding: const EdgeInsets.only( + top: 25, + ), + child: Divider(), + ), + ], + ), ), - ), - ) - : const LoadingIndicatorUtil(), - ); + ) + : const LoadingIndicatorUtil(), + ); + } else { + int maxSession = 0; + for (var session in race.sessionStates!) { + if (session == "completed") { + maxSession++; + } + } + return ListView.builder( + itemCount: hasSprint ? 1 : 3, + itemBuilder: (context, index) => maxSession > index + ? ListTile( + title: Text( + sessionsTitle[index], + textAlign: TextAlign.center, + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FreePracticeScreen( + sessionsTitle[index], + index + 1, + race.circuitId, + race.meetingId, + DateTime.parse(race.date).year, + race.raceName, + ), + ), + ), + ) + : Padding( + padding: const EdgeInsets.only(top: 25), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + sessionsTitle[index], + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + SessionCountdownTimer( + race, + index, + sessionsTitle[index], + update: update, + ), + Padding( + padding: const EdgeInsets.only( + top: 25, + ), + child: Divider(), + ), + ], + ), + ), + ); + } } } @@ -390,11 +450,18 @@ class RaceResultsProvider extends StatefulWidget { } class _RaceResultsProviderState extends State { - Future> getRaceStandingsFromErgast(String round) async { - return await ErgastApi().getRaceStandings(round); + Future> getRaceStandingsFromApi(Race race) async { + bool useOfficialDataSoure = Hive.box('settings') + .get('useOfficialDataSoure', defaultValue: false) as bool; + if (useOfficialDataSoure) { + return await Formula1().getRaceStandings(race.meetingId, race.round); + } else { + return await ErgastApi().getRaceStandings(race.round); + } } Future> getRaceStandingsFromF1(String raceUrl) async { + // TODO: prefer api instead of scraping? return await FormulaOneScraper().scrapeRaceResult( '', 0, @@ -413,7 +480,11 @@ class _RaceResultsProviderState extends State { late Map savedData; late Race race; late int timeToRace; + late DateTime raceFullDateParsed; String raceUrl = ''; + String scheduleLastSavedFormat = Hive.box('requests') + .get('scheduleLastSavedFormat', defaultValue: 'ergast'); + if (widget.raceUrl != null) { timeToRace = -1; raceUrl = widget.raceUrl!; @@ -421,9 +492,11 @@ class _RaceResultsProviderState extends State { race = widget.race!; savedData = Hive.box('requests') .get('race-${race.round}', defaultValue: {}) as Map; - DateTime raceFullDateParsed = DateTime.parse( - "${race.date} ${race.raceHour}", - ); + if (scheduleLastSavedFormat == 'ergast') { + raceFullDateParsed = DateTime.parse("${race.date} ${race.raceHour}"); + } else { + raceFullDateParsed = DateTime.parse(race.date); + } int timeBetween(DateTime from, DateTime to) { return to.difference(from).inSeconds; } @@ -441,6 +514,8 @@ class _RaceResultsProviderState extends State { update: _setState, ); } else { + String raceResultsLastSavedFormat = Hive.box('requests') + .get('raceResultsLastSavedFormat', defaultValue: 'ergast'); return raceUrl != '' ? FutureBuilder>( future: getRaceStandingsFromF1(raceUrl), @@ -494,9 +569,12 @@ class _RaceResultsProviderState extends State { : const LoadingIndicatorUtil(); }) : FutureBuilder>( - future: getRaceStandingsFromErgast(race.round), + future: getRaceStandingsFromApi(race), builder: (context, snapshot) => snapshot.hasError - ? savedData['MRData'] != null + ? savedData[raceResultsLastSavedFormat == 'ergast' + ? 'MRData' + : 'raceResultsRace'] != + null ? SingleChildScrollView( child: Column( children: [ @@ -512,7 +590,9 @@ class _RaceResultsProviderState extends State { onTap: () async {}, ), RaceDriversResultsList( - ErgastApi().formatRaceStandings(savedData), + raceResultsLastSavedFormat == 'ergast' + ? ErgastApi().formatRaceStandings(savedData) + : Formula1().formatRaceStandings(savedData), ), ], ), @@ -562,7 +642,10 @@ class _RaceResultsProviderState extends State { ], ), ) - : savedData['MRData'] != null + : savedData[raceResultsLastSavedFormat == 'ergast' + ? 'MRData' + : 'raceResultsRace'] != + null ? SingleChildScrollView( child: Column( children: [ @@ -578,7 +661,11 @@ class _RaceResultsProviderState extends State { onTap: () async {}, ), RaceDriversResultsList( - ErgastApi().formatRaceStandings(savedData), + raceResultsLastSavedFormat == 'ergast' + ? ErgastApi() + .formatRaceStandings(savedData) + : Formula1() + .formatRaceStandings(savedData), ), ], ), @@ -604,11 +691,15 @@ class SprintResultsProvider extends StatefulWidget { class _SprintResultsProviderState extends State { Future> getSprintStandings( - String round, + Race race, ) async { - return await ErgastApi().getSprintStandings( - round, - ); + bool useOfficialDataSoure = Hive.box('settings') + .get('useOfficialDataSoure', defaultValue: false) as bool; + if (useOfficialDataSoure) { + return await Formula1().getSprintStandings(race.meetingId, race.round); + } else { + return await ErgastApi().getSprintStandings(race.round); + } } void _setState() { @@ -623,7 +714,7 @@ class _SprintResultsProviderState extends State { padding: const EdgeInsets.all(15), child: Center( child: Text( - 'AppLocalizations.of(context)!.dataNotAvailable', + AppLocalizations.of(context)!.dataNotAvailable, textAlign: TextAlign.center, ), ), @@ -638,16 +729,14 @@ class _SprintResultsProviderState extends State { raceUrl: widget.raceUrl!, ) : getSprintStandings( - widget.race!.round, + widget.race!, ), builder: (context, snapshot) => snapshot.hasError ? Padding( padding: const EdgeInsets.all(15), child: Center( child: Text( - snapshot.error.toString() + - snapshot.stackTrace - .toString(), //AppLocalizations.of(context)!.dataNotAvailable, + AppLocalizations.of(context)!.dataNotAvailable, textAlign: TextAlign.center, style: TextStyle( fontSize: 15, @@ -663,42 +752,46 @@ class _SprintResultsProviderState extends State { AppLocalizations.of(context)!.sprint, update: _setState, ) - : Column( - children: [ - GestureDetector( - child: ListTile( - leading: const FaIcon( - FontAwesomeIcons.youtube, - ), - title: Text( - AppLocalizations.of(context)! - .watchOnYoutube, - textAlign: TextAlign.center, + : SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + child: Column( + children: [ + GestureDetector( + child: ListTile( + leading: const FaIcon( + FontAwesomeIcons.youtube, + ), + title: Text( + AppLocalizations.of(context)! + .watchOnYoutube, + textAlign: TextAlign.center, + ), + onTap: () async { + var yt = YoutubeExplode(); + final raceYear = + widget.race!.date.split('-')[0]; + final List