From 251b574a9be1a19a45a9b3eb04be3901c32913cb Mon Sep 17 00:00:00 2001 From: Mikolaj Kieres Date: Sat, 19 Nov 2022 15:45:44 +1100 Subject: [PATCH] 1.7.3 - New tab showing played games history (#146) * progress on play history page * progress on grouping and UI of the play history page * updating UI of the play history item * fixing an issue with null reference when removing bgg games * ensuring that play history updates when playthroughs updated * adding empty state to the play history page --- board_games_companion/lib/app.dart | 2 + .../lib/common/app_text.dart | 16 + .../lib/common/dimensions.dart | 3 + .../lib/extensions/date_time_extensions.dart | 47 ++- .../lib/extensions/int_extensions.dart | 2 +- .../extensions/player_score_extensions.dart | 12 + .../lib/extensions/scores_extensions.dart | 88 +++-- .../lib/extensions/string_extensions.dart | 8 + .../lib/injectable.config.dart | 165 +++++---- .../lib/models/hive/score.dart | 2 + .../board_game_details_page_arguments.dart | 23 +- ...d_game_details_page_arguments.freezed.dart | 231 ++++++++++++ .../playthroughs_page_arguments.dart | 12 +- .../playthroughs_page_arguments.freezed.dart | 170 +++++++++ .../lib/models/player_score.dart | 2 + .../lib/models/playthrough_details.dart | 2 +- .../board_game_details_expansions.dart | 7 +- .../board_game_details_page.dart | 10 +- .../board_game_details_view_model.dart | 6 + .../edit_playthrough_view_model.dart | 4 +- .../playthrough_note_view_model.dart | 4 +- .../lib/pages/games/games_page.dart | 45 ++- .../lib/pages/games/games_view_model.dart | 9 + .../lib/pages/home/home_page.dart | 16 +- .../lib/pages/home/home_view_model.dart | 6 +- .../lib/pages/players/players_page.dart | 6 +- .../playthrough_statistics_view_model.dart | 57 ++- .../playthrough_statistics_view_model.g.dart | 14 + ...playthroughs_game_settings_view_model.dart | 4 +- .../playthroughs_history_view_model.dart | 4 +- .../playthroughs_log_game_page.dart | 24 +- .../playthroughs_log_game_view_model.dart | 12 +- .../pages/playthroughs/playthroughs_page.dart | 19 +- .../playthroughs_statistics_page.dart | 19 +- .../playthroughs/playthroughs_view_model.dart | 15 +- .../board_game_playthrough.dart | 26 ++ .../board_game_playthrough.freezed.dart | 181 +++++++++ .../grouped_board_game_playthroughs.dart | 37 ++ ...ouped_board_game_playthroughs.freezed.dart | 168 +++++++++ .../playthroughs_history_page.dart | 345 ++++++++++++++++++ .../playthroughs_history_view_model.dart | 106 ++++++ .../playthroughs_history_view_model.g.dart | 89 +++++ .../search_board_games_page.dart | 7 +- .../lib/services/playthroughs_service.dart | 14 +- .../lib/services/score_service.dart | 15 +- .../lib/stores/board_games_store.dart | 2 +- .../lib/stores/game_playthroughs_store.dart | 160 ++++++++ .../lib/stores/game_playthroughs_store.g.dart | 123 +++++++ .../lib/stores/players_store.dart | 3 + .../lib/stores/players_store.g.dart | 11 +- .../lib/stores/playthroughs_store.dart | 123 ++----- .../lib/stores/playthroughs_store.g.dart | 95 ++--- .../lib/stores/scores_store.dart | 51 +++ .../lib/stores/scores_store.g.dart | 33 ++ board_games_companion/pubspec.lock | 7 + board_games_companion/pubspec.yaml | 1 + 56 files changed, 2238 insertions(+), 425 deletions(-) create mode 100644 board_games_companion/lib/extensions/player_score_extensions.dart create mode 100644 board_games_companion/lib/models/navigation/board_game_details_page_arguments.freezed.dart create mode 100644 board_games_companion/lib/models/navigation/playthroughs_page_arguments.freezed.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.freezed.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.freezed.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/playthroughs_history_page.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.dart create mode 100644 board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.g.dart create mode 100644 board_games_companion/lib/stores/game_playthroughs_store.dart create mode 100644 board_games_companion/lib/stores/game_playthroughs_store.g.dart create mode 100644 board_games_companion/lib/stores/scores_store.dart create mode 100644 board_games_companion/lib/stores/scores_store.g.dart diff --git a/board_games_companion/lib/app.dart b/board_games_companion/lib/app.dart index 0de68649..e771d447 100644 --- a/board_games_companion/lib/app.dart +++ b/board_games_companion/lib/app.dart @@ -72,6 +72,7 @@ class BoardGamesCompanionAppState extends State { viewModel.setBoardGameId(arguments.boardGameId); viewModel.setBoardGameName(arguments.boardGameName); viewModel.setBoardGameImageUrl(arguments.boardGameImageUrl); + viewModel.setBoardGameImageHeroId(arguments.boardGameImageHeroId); return MaterialPageRoute( settings: routeSettings, @@ -98,6 +99,7 @@ class BoardGamesCompanionAppState extends State { final viewModel = getIt(); viewModel.setBoardGame(arguments.boardGameDetails); + viewModel.setBoardGameImageHeroId(arguments.boardGameImageHeroId); return MaterialPageRoute( settings: routeSettings, diff --git a/board_games_companion/lib/common/app_text.dart b/board_games_companion/lib/common/app_text.dart index 9a2454dc..b66c87a5 100644 --- a/board_games_companion/lib/common/app_text.dart +++ b/board_games_companion/lib/common/app_text.dart @@ -16,6 +16,16 @@ class AppText { static const goBack = 'Go Back'; + static const today = 'today'; + static const yesteday = 'yesterday'; + static const dayBeforeYesteday = 'day before yesterday'; + static const daysAgoFormat = '%s days ago'; + + static const homePageGamesTabTitle = 'Games'; + static const homePageSearchTabTitle = 'Search'; + static const homePageGamesHistoryTabTitle = 'Play History'; + static const homePageGamesPlayersTabTitle = 'Players'; + static const aboutPageAuthorSectionTitle = 'Author'; static const aboutPageDesignAndArtSectionTitle = 'Design & Art'; static const aboutPageContentAndDataSectionTitle = 'Content & Data'; @@ -172,6 +182,11 @@ class AppText { static const settingsPageRestoreFailedMessage = 'Unfortunately we ran into a problem with restoring your data. Please try again or contact support at feedback@progrunning.net'; + static const playHistoryPageEmptyTitle = "You haven't played any games yet"; + static const playHistoryPageEmptyTextPartOne = 'Nothing to worry about though! '; + static const playHistoryPageEmptyTextPartTwo = + 'Start recording your plays in the app and this screen will automatically populate with a history of your playthroughs.'; + static const gamePlaytimeFormat = '%s min'; static const gamePlayersSingularFormat = '%i players'; static const gamePlayersPluralFormat = '%i players'; @@ -181,6 +196,7 @@ class AppText { static const collectionsPageTitle = 'Collections'; static const settingsPageTitle = 'Settings'; static const newPlayerPageTitle = 'New Player'; + static const playHistoryPageTitle = 'Play History'; static const drawerVersionFormat = 'Version %s'; static const drawerReleaseNotes = 'Release notes'; diff --git a/board_games_companion/lib/common/dimensions.dart b/board_games_companion/lib/common/dimensions.dart index 7fce525e..01c9d714 100644 --- a/board_games_companion/lib/common/dimensions.dart +++ b/board_games_companion/lib/common/dimensions.dart @@ -9,6 +9,9 @@ class Dimensions { static const double boardGameDetailsImageHeight = 80; + static const double emptyPageTitleTopSpacing = 40; + static const double emptyPageTitleIconSize = 80; + static const double extraSmallFontSize = 10; static const double smallFontSize = 12; static const double standardFontSize = 14; diff --git a/board_games_companion/lib/extensions/date_time_extensions.dart b/board_games_companion/lib/extensions/date_time_extensions.dart index b3746333..23354e64 100644 --- a/board_games_companion/lib/extensions/date_time_extensions.dart +++ b/board_games_companion/lib/extensions/date_time_extensions.dart @@ -1,4 +1,6 @@ +import 'package:board_games_companion/common/app_text.dart'; import 'package:intl/intl.dart'; +import 'package:sprintf/sprintf.dart'; import '../common/constants.dart'; @@ -20,24 +22,47 @@ extension DateTimeExtensions on DateTime? { } String toDaysAgo() { - const String daysAgoText = 'days ago'; if (this == null) { - return '- $daysAgoText'; + return sprintf(AppText.daysAgoFormat, ['-']); } - final nowUtc = DateTime.now().toUtc(); - final daysAgo = nowUtc.difference(this!).inDays; - if (daysAgo == 0) { - return 'today'; + if (isToday) { + return AppText.today; } - if (daysAgo == 1) { - return 'yesterday'; + if (isYesterday) { + return AppText.yesteday; } - if (daysAgo == 2) { - return 'day before yesterday'; + if (isDayBeforeYesterday) { + return AppText.dayBeforeYesteday; } - return '$daysAgo $daysAgoText'; + return sprintf(AppText.daysAgoFormat, [daysAgo]); + } + + int get daysAgo { + final nowUtc = DateTime.now().toUtc(); + return nowUtc.difference(this!).inDays; + } + + bool get isToday { + final nowUtc = DateTime.now().toUtc(); + final daysAgo = nowUtc.difference(this!).inDays; + + return daysAgo == 0; + } + + bool get isYesterday { + final nowUtc = DateTime.now().toUtc(); + final daysAgo = nowUtc.difference(this!).inDays; + + return daysAgo == 1; + } + + bool get isDayBeforeYesterday { + final nowUtc = DateTime.now().toUtc(); + final daysAgo = nowUtc.difference(this!).inDays; + + return daysAgo == 2; } int safeCompareTo(DateTime? dateTimeToCompare) { diff --git a/board_games_companion/lib/extensions/int_extensions.dart b/board_games_companion/lib/extensions/int_extensions.dart index f4efbec6..d2b39110 100644 --- a/board_games_companion/lib/extensions/int_extensions.dart +++ b/board_games_companion/lib/extensions/int_extensions.dart @@ -28,7 +28,7 @@ extension IntExtensions on int? { } } - String toPlaytimeDuration([String? fallbackValue, bool showSeconds = true]) { + String toPlaytimeDuration({String? fallbackValue, bool showSeconds = true}) { if (this == null) { return fallbackValue ?? ''; } diff --git a/board_games_companion/lib/extensions/player_score_extensions.dart b/board_games_companion/lib/extensions/player_score_extensions.dart new file mode 100644 index 00000000..d9409771 --- /dev/null +++ b/board_games_companion/lib/extensions/player_score_extensions.dart @@ -0,0 +1,12 @@ +import '../common/enums/game_winning_condition.dart'; +import '../extensions/scores_extensions.dart'; +import '../models/player_score.dart'; + +extension PlayerScoresExtesions on List { + List sortByScore(GameWinningCondition winningCondition) { + return this + ..sort((PlayerScore playerScore, PlayerScore otherPlayerScore) { + return compareScores(playerScore.score, otherPlayerScore.score, winningCondition); + }); + } +} diff --git a/board_games_companion/lib/extensions/scores_extensions.dart b/board_games_companion/lib/extensions/scores_extensions.dart index 9f564faf..7a4de680 100644 --- a/board_games_companion/lib/extensions/scores_extensions.dart +++ b/board_games_companion/lib/extensions/scores_extensions.dart @@ -5,6 +5,12 @@ import 'package:board_games_companion/common/enums/game_winning_condition.dart'; import '../models/hive/score.dart'; +extension ScoreExtesions on Score { + String toMapKey() { + return '$playthroughId$playerId'; + } +} + extension ScoresExtesions on List? { List onlyScoresWithValue() { return this @@ -16,45 +22,7 @@ extension ScoresExtesions on List? { List? sortByScore(GameWinningCondition winningCondition) { return this ?..sort((Score score, Score otherScore) { - switch (winningCondition) { - case GameWinningCondition.LowestScore: - // MK Swap scores around - final buffer = otherScore; - otherScore = score; - score = buffer; - break; - case GameWinningCondition.HighestScore: - // MK No swapping needed - break; - } - - if (score.value == null && otherScore.value == null) { - return Constants.leaveAsIs; - } - - if (score.value == null) { - return Constants.moveBelow; - } - - if (otherScore.value == null) { - return Constants.moveAbove; - } - - final num? aNumber = num.tryParse(score.value!); - final num? bNumber = num.tryParse(otherScore.value!); - if (aNumber == null && bNumber == null) { - return Constants.leaveAsIs; - } - - if (aNumber == null) { - return Constants.moveBelow; - } - - if (bNumber == null) { - return Constants.moveAbove; - } - - return bNumber.compareTo(aNumber); + return compareScores(score, otherScore, winningCondition); }); } @@ -84,3 +52,45 @@ extension ScoresExtesions on List? { return scores.reduce((a, b) => a + b) / scores.length; } } + +int compareScores(Score score, Score otherScore, GameWinningCondition winningCondition) { + switch (winningCondition) { + case GameWinningCondition.LowestScore: + // MK Swap scores around + final buffer = otherScore; + otherScore = score; + score = buffer; + break; + case GameWinningCondition.HighestScore: + // MK No swapping needed + break; + } + + if (score.value == null && otherScore.value == null) { + return Constants.leaveAsIs; + } + + if (score.value == null) { + return Constants.moveBelow; + } + + if (otherScore.value == null) { + return Constants.moveAbove; + } + + final num? aNumber = num.tryParse(score.value!); + final num? bNumber = num.tryParse(otherScore.value!); + if (aNumber == null && bNumber == null) { + return Constants.leaveAsIs; + } + + if (aNumber == null) { + return Constants.moveBelow; + } + + if (bNumber == null) { + return Constants.moveAbove; + } + + return bNumber.compareTo(aNumber); +} diff --git a/board_games_companion/lib/extensions/string_extensions.dart b/board_games_companion/lib/extensions/string_extensions.dart index 7c1aea24..42714363 100644 --- a/board_games_companion/lib/extensions/string_extensions.dart +++ b/board_games_companion/lib/extensions/string_extensions.dart @@ -16,4 +16,12 @@ extension StringExtensions on String? { return this!.compareTo(stringToCompare!); } + + String toCapitalized() { + if (this == null) { + return ''; + } + + return this!.isNotEmpty ? '${this![0].toUpperCase()}${this!.substring(1).toLowerCase()}' : ''; + } } diff --git a/board_games_companion/lib/injectable.config.dart b/board_games_companion/lib/injectable.config.dart index 24d762d7..ffa1a88b 100644 --- a/board_games_companion/lib/injectable.config.dart +++ b/board_games_companion/lib/injectable.config.dart @@ -9,39 +9,43 @@ import 'package:firebase_analytics/observer.dart' as _i8; import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; -import 'pages/board_game_details/board_game_details_view_model.dart' as _i37; -import 'pages/edit_playthrough/edit_playthrough_view_model.dart' as _i28; +import 'pages/board_game_details/board_game_details_view_model.dart' as _i39; +import 'pages/edit_playthrough/edit_playthrough_view_model.dart' as _i40; import 'pages/edit_playthrough/playthrough_note_view_model.dart' as _i30; import 'pages/games/collection_search_result_view_model.dart' as _i27; import 'pages/games/games_view_model.dart' as _i29; -import 'pages/home/home_view_model.dart' as _i38; -import 'pages/player/player_view_model.dart' as _i21; +import 'pages/home/home_view_model.dart' as _i41; +import 'pages/player/player_view_model.dart' as _i22; import 'pages/players/players_view_model.dart' as _i11; import 'pages/playthroughs/playthrough_statistics_view_model.dart' as _i31; import 'pages/playthroughs/playthroughs_game_settings_view_model.dart' as _i32; import 'pages/playthroughs/playthroughs_history_view_model.dart' as _i33; -import 'pages/playthroughs/playthroughs_log_game_view_model.dart' as _i34; -import 'pages/playthroughs/playthroughs_view_model.dart' as _i24; -import 'pages/search_board_games/search_board_games_view_model.dart' as _i35; -import 'pages/settings/settings_view_model.dart' as _i36; -import 'services/analytics_service.dart' as _i17; +import 'pages/playthroughs/playthroughs_log_game_view_model.dart' as _i35; +import 'pages/playthroughs/playthroughs_view_model.dart' as _i36; +import 'pages/playthroughs_history/playthroughs_history_view_model.dart' + as _i34; +import 'pages/search_board_games/search_board_games_view_model.dart' as _i37; +import 'pages/settings/settings_view_model.dart' as _i38; +import 'services/analytics_service.dart' as _i18; import 'services/board_games_filters_service.dart' as _i4; -import 'services/board_games_geek_service.dart' as _i19; -import 'services/board_games_service.dart' as _i20; +import 'services/board_games_geek_service.dart' as _i20; +import 'services/board_games_service.dart' as _i21; import 'services/file_service.dart' as _i6; -import 'services/injectable_register_module.dart' as _i39; +import 'services/injectable_register_module.dart' as _i42; import 'services/player_service.dart' as _i9; -import 'services/playthroughs_service.dart' as _i22; +import 'services/playthroughs_service.dart' as _i23; import 'services/preferences_service.dart' as _i12; import 'services/rate_and_review_service.dart' as _i13; import 'services/score_service.dart' as _i14; -import 'services/user_service.dart' as _i15; +import 'services/user_service.dart' as _i16; import 'stores/app_store.dart' as _i3; -import 'stores/board_games_filters_store.dart' as _i18; +import 'stores/board_games_filters_store.dart' as _i19; import 'stores/board_games_store.dart' as _i26; +import 'stores/game_playthroughs_store.dart' as _i28; import 'stores/players_store.dart' as _i10; -import 'stores/playthroughs_store.dart' as _i23; -import 'stores/user_store.dart' as _i16; +import 'stores/playthroughs_store.dart' as _i24; +import 'stores/scores_store.dart' as _i15; +import 'stores/user_store.dart' as _i17; import 'utilities/analytics_route_observer.dart' as _i25; import 'utilities/custom_http_client_adapter.dart' as _i5; // ignore_for_file: unnecessary_lambdas @@ -68,91 +72,100 @@ _i1.GetIt $initGetIt(_i1.GetIt get, gh.singleton<_i13.RateAndReviewService>( _i13.RateAndReviewService(get<_i12.PreferencesService>())); gh.singleton<_i14.ScoreService>(_i14.ScoreService()); - gh.singleton<_i15.UserService>(_i15.UserService()); - gh.singleton<_i16.UserStore>( - _i16.UserStore(get<_i15.UserService>(), get<_i3.AppStore>())); - gh.singleton<_i17.AnalyticsService>(_i17.AnalyticsService( + gh.singleton<_i15.ScoresStore>(_i15.ScoresStore(get<_i14.ScoreService>())); + gh.singleton<_i16.UserService>(_i16.UserService()); + gh.singleton<_i17.UserStore>( + _i17.UserStore(get<_i16.UserService>(), get<_i3.AppStore>())); + gh.singleton<_i18.AnalyticsService>(_i18.AnalyticsService( get<_i7.FirebaseAnalytics>(), get<_i13.RateAndReviewService>())); - gh.singleton<_i18.BoardGamesFiltersStore>(_i18.BoardGamesFiltersStore( - get<_i4.BoardGamesFiltersService>(), get<_i17.AnalyticsService>())); - gh.singleton<_i19.BoardGamesGeekService>( - _i19.BoardGamesGeekService(get<_i5.CustomHttpClientAdapter>())); - gh.singleton<_i20.BoardGamesService>( - _i20.BoardGamesService(get<_i19.BoardGamesGeekService>())); - gh.factory<_i21.PlayerViewModel>( - () => _i21.PlayerViewModel(get<_i10.PlayersStore>())); - gh.singleton<_i22.PlaythroughService>( - _i22.PlaythroughService(get<_i14.ScoreService>())); - gh.singleton<_i23.PlaythroughsStore>(_i23.PlaythroughsStore( - get<_i22.PlaythroughService>(), - get<_i14.ScoreService>(), - get<_i9.PlayerService>())); - gh.factory<_i24.PlaythroughsViewModel>(() => _i24.PlaythroughsViewModel( - get<_i23.PlaythroughsStore>(), - get<_i10.PlayersStore>(), - get<_i17.AnalyticsService>(), - get<_i20.BoardGamesService>(), - get<_i16.UserStore>())); + gh.singleton<_i19.BoardGamesFiltersStore>(_i19.BoardGamesFiltersStore( + get<_i4.BoardGamesFiltersService>(), get<_i18.AnalyticsService>())); + gh.singleton<_i20.BoardGamesGeekService>( + _i20.BoardGamesGeekService(get<_i5.CustomHttpClientAdapter>())); + gh.singleton<_i21.BoardGamesService>( + _i21.BoardGamesService(get<_i20.BoardGamesGeekService>())); + gh.factory<_i22.PlayerViewModel>( + () => _i22.PlayerViewModel(get<_i10.PlayersStore>())); + gh.singleton<_i23.PlaythroughService>( + _i23.PlaythroughService(get<_i14.ScoreService>())); + gh.singleton<_i24.PlaythroughsStore>(_i24.PlaythroughsStore( + get<_i23.PlaythroughService>(), get<_i15.ScoresStore>())); gh.factory<_i25.AnalyticsRouteObserver>( - () => _i25.AnalyticsRouteObserver(get<_i17.AnalyticsService>())); + () => _i25.AnalyticsRouteObserver(get<_i18.AnalyticsService>())); gh.singleton<_i26.BoardGamesStore>(_i26.BoardGamesStore( - get<_i20.BoardGamesService>(), - get<_i22.PlaythroughService>(), + get<_i21.BoardGamesService>(), + get<_i23.PlaythroughService>(), get<_i3.AppStore>())); gh.factory<_i27.CollectionSearchResultViewModel>( () => _i27.CollectionSearchResultViewModel(get<_i26.BoardGamesStore>())); - gh.factory<_i28.EditPlaythoughViewModel>( - () => _i28.EditPlaythoughViewModel(get<_i23.PlaythroughsStore>())); + gh.singleton<_i28.GamePlaythroughsStore>(_i28.GamePlaythroughsStore( + get<_i24.PlaythroughsStore>(), + get<_i15.ScoresStore>(), + get<_i9.PlayerService>())); gh.factory<_i29.GamesViewModel>(() => _i29.GamesViewModel( - get<_i16.UserStore>(), + get<_i17.UserStore>(), get<_i26.BoardGamesStore>(), - get<_i18.BoardGamesFiltersStore>())); + get<_i19.BoardGamesFiltersStore>(), + get<_i15.ScoresStore>(), + get<_i24.PlaythroughsStore>())); gh.factory<_i30.PlaythroughNoteViewModel>( - () => _i30.PlaythroughNoteViewModel(get<_i23.PlaythroughsStore>())); + () => _i30.PlaythroughNoteViewModel(get<_i28.GamePlaythroughsStore>())); gh.singleton<_i31.PlaythroughStatisticsViewModel>( - _i31.PlaythroughStatisticsViewModel( - get<_i9.PlayerService>(), - get<_i14.ScoreService>(), - get<_i22.PlaythroughService>(), - get<_i23.PlaythroughsStore>())); + _i31.PlaythroughStatisticsViewModel(get<_i9.PlayerService>(), + get<_i15.ScoresStore>(), get<_i28.GamePlaythroughsStore>())); gh.factory<_i32.PlaythroughsGameSettingsViewModel>(() => _i32.PlaythroughsGameSettingsViewModel( - get<_i26.BoardGamesStore>(), get<_i23.PlaythroughsStore>())); - gh.factory<_i33.PlaythroughsHistoryViewModel>( - () => _i33.PlaythroughsHistoryViewModel(get<_i23.PlaythroughsStore>())); - gh.factory<_i34.PlaythroughsLogGameViewModel>(() => - _i34.PlaythroughsLogGameViewModel(get<_i10.PlayersStore>(), - get<_i23.PlaythroughsStore>(), get<_i17.AnalyticsService>())); - gh.singleton<_i35.SearchBoardGamesViewModel>(_i35.SearchBoardGamesViewModel( + get<_i26.BoardGamesStore>(), get<_i28.GamePlaythroughsStore>())); + gh.factory<_i33.PlaythroughsHistoryViewModel>(() => + _i33.PlaythroughsHistoryViewModel(get<_i28.GamePlaythroughsStore>())); + gh.factory<_i34.PlaythroughsHistoryViewModel>(() => + _i34.PlaythroughsHistoryViewModel( + get<_i24.PlaythroughsStore>(), + get<_i26.BoardGamesStore>(), + get<_i10.PlayersStore>(), + get<_i15.ScoresStore>())); + gh.factory<_i35.PlaythroughsLogGameViewModel>(() => + _i35.PlaythroughsLogGameViewModel(get<_i10.PlayersStore>(), + get<_i28.GamePlaythroughsStore>(), get<_i18.AnalyticsService>())); + gh.factory<_i36.PlaythroughsViewModel>(() => _i36.PlaythroughsViewModel( + get<_i28.GamePlaythroughsStore>(), + get<_i10.PlayersStore>(), + get<_i18.AnalyticsService>(), + get<_i21.BoardGamesService>(), + get<_i17.UserStore>())); + gh.singleton<_i37.SearchBoardGamesViewModel>(_i37.SearchBoardGamesViewModel( get<_i26.BoardGamesStore>(), - get<_i19.BoardGamesGeekService>(), - get<_i17.AnalyticsService>())); - gh.singleton<_i36.SettingsViewModel>(_i36.SettingsViewModel( + get<_i20.BoardGamesGeekService>(), + get<_i18.AnalyticsService>())); + gh.singleton<_i38.SettingsViewModel>(_i38.SettingsViewModel( get<_i6.FileService>(), - get<_i20.BoardGamesService>(), + get<_i21.BoardGamesService>(), get<_i4.BoardGamesFiltersService>(), get<_i9.PlayerService>(), - get<_i15.UserService>(), - get<_i22.PlaythroughService>(), + get<_i16.UserService>(), + get<_i23.PlaythroughService>(), get<_i14.ScoreService>(), get<_i12.PreferencesService>(), get<_i3.AppStore>(), - get<_i16.UserStore>(), + get<_i17.UserStore>(), get<_i26.BoardGamesStore>())); - gh.factory<_i37.BoardGameDetailsViewModel>(() => - _i37.BoardGameDetailsViewModel( - get<_i26.BoardGamesStore>(), get<_i17.AnalyticsService>())); - gh.factory<_i38.HomeViewModel>(() => _i38.HomeViewModel( - get<_i17.AnalyticsService>(), + gh.factory<_i39.BoardGameDetailsViewModel>(() => + _i39.BoardGameDetailsViewModel( + get<_i26.BoardGamesStore>(), get<_i18.AnalyticsService>())); + gh.factory<_i40.EditPlaythoughViewModel>( + () => _i40.EditPlaythoughViewModel(get<_i28.GamePlaythroughsStore>())); + gh.factory<_i41.HomeViewModel>(() => _i41.HomeViewModel( + get<_i18.AnalyticsService>(), get<_i13.RateAndReviewService>(), get<_i11.PlayersViewModel>(), - get<_i18.BoardGamesFiltersStore>(), + get<_i19.BoardGamesFiltersStore>(), get<_i29.GamesViewModel>(), - get<_i35.SearchBoardGamesViewModel>())); + get<_i37.SearchBoardGamesViewModel>(), + get<_i34.PlaythroughsHistoryViewModel>())); return get; } -class _$RegisterModule extends _i39.RegisterModule { +class _$RegisterModule extends _i42.RegisterModule { @override _i7.FirebaseAnalytics get firebaseAnalytics => _i7.FirebaseAnalytics(); } diff --git a/board_games_companion/lib/models/hive/score.dart b/board_games_companion/lib/models/hive/score.dart index d17c1c06..fb9f078c 100644 --- a/board_games_companion/lib/models/hive/score.dart +++ b/board_games_companion/lib/models/hive/score.dart @@ -3,6 +3,8 @@ import 'package:hive/hive.dart'; import '../../common/hive_boxes.dart'; +export '../../extensions/scores_extensions.dart'; + part 'score.freezed.dart'; part 'score.g.dart'; diff --git a/board_games_companion/lib/models/navigation/board_game_details_page_arguments.dart b/board_games_companion/lib/models/navigation/board_game_details_page_arguments.dart index 822ef015..8301ef76 100644 --- a/board_games_companion/lib/models/navigation/board_game_details_page_arguments.dart +++ b/board_games_companion/lib/models/navigation/board_game_details_page_arguments.dart @@ -1,13 +1,14 @@ -class BoardGameDetailsPageArguments { - const BoardGameDetailsPageArguments( - this.boardGameId, - this.boardGameName, - this.navigatingFromType, { - this.boardGameImageUrl, - }); +import 'package:freezed_annotation/freezed_annotation.dart'; - final String boardGameId; - final String boardGameName; - final String? boardGameImageUrl; - final Type navigatingFromType; +part 'board_game_details_page_arguments.freezed.dart'; + +@freezed +class BoardGameDetailsPageArguments with _$BoardGameDetailsPageArguments { + const factory BoardGameDetailsPageArguments({ + required String boardGameId, + required String boardGameName, + required Type navigatingFromType, + required String boardGameImageHeroId, + String? boardGameImageUrl, + }) = _BoardGameDetailsPageArguments; } diff --git a/board_games_companion/lib/models/navigation/board_game_details_page_arguments.freezed.dart b/board_games_companion/lib/models/navigation/board_game_details_page_arguments.freezed.dart new file mode 100644 index 00000000..ceadb0ef --- /dev/null +++ b/board_games_companion/lib/models/navigation/board_game_details_page_arguments.freezed.dart @@ -0,0 +1,231 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'board_game_details_page_arguments.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$BoardGameDetailsPageArguments { + String get boardGameId => throw _privateConstructorUsedError; + String get boardGameName => throw _privateConstructorUsedError; + Type get navigatingFromType => throw _privateConstructorUsedError; + String get boardGameImageHeroId => throw _privateConstructorUsedError; + String? get boardGameImageUrl => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $BoardGameDetailsPageArgumentsCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BoardGameDetailsPageArgumentsCopyWith<$Res> { + factory $BoardGameDetailsPageArgumentsCopyWith( + BoardGameDetailsPageArguments value, + $Res Function(BoardGameDetailsPageArguments) then) = + _$BoardGameDetailsPageArgumentsCopyWithImpl<$Res>; + $Res call( + {String boardGameId, + String boardGameName, + Type navigatingFromType, + String boardGameImageHeroId, + String? boardGameImageUrl}); +} + +/// @nodoc +class _$BoardGameDetailsPageArgumentsCopyWithImpl<$Res> + implements $BoardGameDetailsPageArgumentsCopyWith<$Res> { + _$BoardGameDetailsPageArgumentsCopyWithImpl(this._value, this._then); + + final BoardGameDetailsPageArguments _value; + // ignore: unused_field + final $Res Function(BoardGameDetailsPageArguments) _then; + + @override + $Res call({ + Object? boardGameId = freezed, + Object? boardGameName = freezed, + Object? navigatingFromType = freezed, + Object? boardGameImageHeroId = freezed, + Object? boardGameImageUrl = freezed, + }) { + return _then(_value.copyWith( + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + boardGameName: boardGameName == freezed + ? _value.boardGameName + : boardGameName // ignore: cast_nullable_to_non_nullable + as String, + navigatingFromType: navigatingFromType == freezed + ? _value.navigatingFromType + : navigatingFromType // ignore: cast_nullable_to_non_nullable + as Type, + boardGameImageHeroId: boardGameImageHeroId == freezed + ? _value.boardGameImageHeroId + : boardGameImageHeroId // ignore: cast_nullable_to_non_nullable + as String, + boardGameImageUrl: boardGameImageUrl == freezed + ? _value.boardGameImageUrl + : boardGameImageUrl // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +abstract class _$$_BoardGameDetailsPageArgumentsCopyWith<$Res> + implements $BoardGameDetailsPageArgumentsCopyWith<$Res> { + factory _$$_BoardGameDetailsPageArgumentsCopyWith( + _$_BoardGameDetailsPageArguments value, + $Res Function(_$_BoardGameDetailsPageArguments) then) = + __$$_BoardGameDetailsPageArgumentsCopyWithImpl<$Res>; + @override + $Res call( + {String boardGameId, + String boardGameName, + Type navigatingFromType, + String boardGameImageHeroId, + String? boardGameImageUrl}); +} + +/// @nodoc +class __$$_BoardGameDetailsPageArgumentsCopyWithImpl<$Res> + extends _$BoardGameDetailsPageArgumentsCopyWithImpl<$Res> + implements _$$_BoardGameDetailsPageArgumentsCopyWith<$Res> { + __$$_BoardGameDetailsPageArgumentsCopyWithImpl( + _$_BoardGameDetailsPageArguments _value, + $Res Function(_$_BoardGameDetailsPageArguments) _then) + : super(_value, (v) => _then(v as _$_BoardGameDetailsPageArguments)); + + @override + _$_BoardGameDetailsPageArguments get _value => + super._value as _$_BoardGameDetailsPageArguments; + + @override + $Res call({ + Object? boardGameId = freezed, + Object? boardGameName = freezed, + Object? navigatingFromType = freezed, + Object? boardGameImageHeroId = freezed, + Object? boardGameImageUrl = freezed, + }) { + return _then(_$_BoardGameDetailsPageArguments( + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + boardGameName: boardGameName == freezed + ? _value.boardGameName + : boardGameName // ignore: cast_nullable_to_non_nullable + as String, + navigatingFromType: navigatingFromType == freezed + ? _value.navigatingFromType + : navigatingFromType // ignore: cast_nullable_to_non_nullable + as Type, + boardGameImageHeroId: boardGameImageHeroId == freezed + ? _value.boardGameImageHeroId + : boardGameImageHeroId // ignore: cast_nullable_to_non_nullable + as String, + boardGameImageUrl: boardGameImageUrl == freezed + ? _value.boardGameImageUrl + : boardGameImageUrl // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$_BoardGameDetailsPageArguments + implements _BoardGameDetailsPageArguments { + const _$_BoardGameDetailsPageArguments( + {required this.boardGameId, + required this.boardGameName, + required this.navigatingFromType, + required this.boardGameImageHeroId, + this.boardGameImageUrl}); + + @override + final String boardGameId; + @override + final String boardGameName; + @override + final Type navigatingFromType; + @override + final String boardGameImageHeroId; + @override + final String? boardGameImageUrl; + + @override + String toString() { + return 'BoardGameDetailsPageArguments(boardGameId: $boardGameId, boardGameName: $boardGameName, navigatingFromType: $navigatingFromType, boardGameImageHeroId: $boardGameImageHeroId, boardGameImageUrl: $boardGameImageUrl)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_BoardGameDetailsPageArguments && + const DeepCollectionEquality() + .equals(other.boardGameId, boardGameId) && + const DeepCollectionEquality() + .equals(other.boardGameName, boardGameName) && + const DeepCollectionEquality() + .equals(other.navigatingFromType, navigatingFromType) && + const DeepCollectionEquality() + .equals(other.boardGameImageHeroId, boardGameImageHeroId) && + const DeepCollectionEquality() + .equals(other.boardGameImageUrl, boardGameImageUrl)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(boardGameId), + const DeepCollectionEquality().hash(boardGameName), + const DeepCollectionEquality().hash(navigatingFromType), + const DeepCollectionEquality().hash(boardGameImageHeroId), + const DeepCollectionEquality().hash(boardGameImageUrl)); + + @JsonKey(ignore: true) + @override + _$$_BoardGameDetailsPageArgumentsCopyWith<_$_BoardGameDetailsPageArguments> + get copyWith => __$$_BoardGameDetailsPageArgumentsCopyWithImpl< + _$_BoardGameDetailsPageArguments>(this, _$identity); +} + +abstract class _BoardGameDetailsPageArguments + implements BoardGameDetailsPageArguments { + const factory _BoardGameDetailsPageArguments( + {required final String boardGameId, + required final String boardGameName, + required final Type navigatingFromType, + required final String boardGameImageHeroId, + final String? boardGameImageUrl}) = _$_BoardGameDetailsPageArguments; + + @override + String get boardGameId; + @override + String get boardGameName; + @override + Type get navigatingFromType; + @override + String get boardGameImageHeroId; + @override + String? get boardGameImageUrl; + @override + @JsonKey(ignore: true) + _$$_BoardGameDetailsPageArgumentsCopyWith<_$_BoardGameDetailsPageArguments> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/models/navigation/playthroughs_page_arguments.dart b/board_games_companion/lib/models/navigation/playthroughs_page_arguments.dart index c5947c9a..b16064d1 100644 --- a/board_games_companion/lib/models/navigation/playthroughs_page_arguments.dart +++ b/board_games_companion/lib/models/navigation/playthroughs_page_arguments.dart @@ -1,7 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + import '../hive/board_game_details.dart'; -class PlaythroughsPageArguments { - const PlaythroughsPageArguments(this.boardGameDetails); +part 'playthroughs_page_arguments.freezed.dart'; - final BoardGameDetails boardGameDetails; +@freezed +class PlaythroughsPageArguments with _$PlaythroughsPageArguments { + const factory PlaythroughsPageArguments({ + required BoardGameDetails boardGameDetails, + required String boardGameImageHeroId, + }) = _PlaythroughsPageArguments; } diff --git a/board_games_companion/lib/models/navigation/playthroughs_page_arguments.freezed.dart b/board_games_companion/lib/models/navigation/playthroughs_page_arguments.freezed.dart new file mode 100644 index 00000000..9bae0943 --- /dev/null +++ b/board_games_companion/lib/models/navigation/playthroughs_page_arguments.freezed.dart @@ -0,0 +1,170 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'playthroughs_page_arguments.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$PlaythroughsPageArguments { + BoardGameDetails get boardGameDetails => throw _privateConstructorUsedError; + String get boardGameImageHeroId => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PlaythroughsPageArgumentsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaythroughsPageArgumentsCopyWith<$Res> { + factory $PlaythroughsPageArgumentsCopyWith(PlaythroughsPageArguments value, + $Res Function(PlaythroughsPageArguments) then) = + _$PlaythroughsPageArgumentsCopyWithImpl<$Res>; + $Res call({BoardGameDetails boardGameDetails, String boardGameImageHeroId}); + + $BoardGameDetailsCopyWith<$Res> get boardGameDetails; +} + +/// @nodoc +class _$PlaythroughsPageArgumentsCopyWithImpl<$Res> + implements $PlaythroughsPageArgumentsCopyWith<$Res> { + _$PlaythroughsPageArgumentsCopyWithImpl(this._value, this._then); + + final PlaythroughsPageArguments _value; + // ignore: unused_field + final $Res Function(PlaythroughsPageArguments) _then; + + @override + $Res call({ + Object? boardGameDetails = freezed, + Object? boardGameImageHeroId = freezed, + }) { + return _then(_value.copyWith( + boardGameDetails: boardGameDetails == freezed + ? _value.boardGameDetails + : boardGameDetails // ignore: cast_nullable_to_non_nullable + as BoardGameDetails, + boardGameImageHeroId: boardGameImageHeroId == freezed + ? _value.boardGameImageHeroId + : boardGameImageHeroId // ignore: cast_nullable_to_non_nullable + as String, + )); + } + + @override + $BoardGameDetailsCopyWith<$Res> get boardGameDetails { + return $BoardGameDetailsCopyWith<$Res>(_value.boardGameDetails, (value) { + return _then(_value.copyWith(boardGameDetails: value)); + }); + } +} + +/// @nodoc +abstract class _$$_PlaythroughsPageArgumentsCopyWith<$Res> + implements $PlaythroughsPageArgumentsCopyWith<$Res> { + factory _$$_PlaythroughsPageArgumentsCopyWith( + _$_PlaythroughsPageArguments value, + $Res Function(_$_PlaythroughsPageArguments) then) = + __$$_PlaythroughsPageArgumentsCopyWithImpl<$Res>; + @override + $Res call({BoardGameDetails boardGameDetails, String boardGameImageHeroId}); + + @override + $BoardGameDetailsCopyWith<$Res> get boardGameDetails; +} + +/// @nodoc +class __$$_PlaythroughsPageArgumentsCopyWithImpl<$Res> + extends _$PlaythroughsPageArgumentsCopyWithImpl<$Res> + implements _$$_PlaythroughsPageArgumentsCopyWith<$Res> { + __$$_PlaythroughsPageArgumentsCopyWithImpl( + _$_PlaythroughsPageArguments _value, + $Res Function(_$_PlaythroughsPageArguments) _then) + : super(_value, (v) => _then(v as _$_PlaythroughsPageArguments)); + + @override + _$_PlaythroughsPageArguments get _value => + super._value as _$_PlaythroughsPageArguments; + + @override + $Res call({ + Object? boardGameDetails = freezed, + Object? boardGameImageHeroId = freezed, + }) { + return _then(_$_PlaythroughsPageArguments( + boardGameDetails: boardGameDetails == freezed + ? _value.boardGameDetails + : boardGameDetails // ignore: cast_nullable_to_non_nullable + as BoardGameDetails, + boardGameImageHeroId: boardGameImageHeroId == freezed + ? _value.boardGameImageHeroId + : boardGameImageHeroId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_PlaythroughsPageArguments implements _PlaythroughsPageArguments { + const _$_PlaythroughsPageArguments( + {required this.boardGameDetails, required this.boardGameImageHeroId}); + + @override + final BoardGameDetails boardGameDetails; + @override + final String boardGameImageHeroId; + + @override + String toString() { + return 'PlaythroughsPageArguments(boardGameDetails: $boardGameDetails, boardGameImageHeroId: $boardGameImageHeroId)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PlaythroughsPageArguments && + const DeepCollectionEquality() + .equals(other.boardGameDetails, boardGameDetails) && + const DeepCollectionEquality() + .equals(other.boardGameImageHeroId, boardGameImageHeroId)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(boardGameDetails), + const DeepCollectionEquality().hash(boardGameImageHeroId)); + + @JsonKey(ignore: true) + @override + _$$_PlaythroughsPageArgumentsCopyWith<_$_PlaythroughsPageArguments> + get copyWith => __$$_PlaythroughsPageArgumentsCopyWithImpl< + _$_PlaythroughsPageArguments>(this, _$identity); +} + +abstract class _PlaythroughsPageArguments implements PlaythroughsPageArguments { + const factory _PlaythroughsPageArguments( + {required final BoardGameDetails boardGameDetails, + required final String boardGameImageHeroId}) = + _$_PlaythroughsPageArguments; + + @override + BoardGameDetails get boardGameDetails; + @override + String get boardGameImageHeroId; + @override + @JsonKey(ignore: true) + _$$_PlaythroughsPageArgumentsCopyWith<_$_PlaythroughsPageArguments> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/models/player_score.dart b/board_games_companion/lib/models/player_score.dart index 5d3bbcbc..e47b8c1d 100644 --- a/board_games_companion/lib/models/player_score.dart +++ b/board_games_companion/lib/models/player_score.dart @@ -3,6 +3,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'hive/player.dart'; import 'hive/score.dart'; +export '../extensions/player_score_extensions.dart'; + part 'player_score.freezed.dart'; @freezed diff --git a/board_games_companion/lib/models/playthrough_details.dart b/board_games_companion/lib/models/playthrough_details.dart index caa9b4ad..d87a87a2 100644 --- a/board_games_companion/lib/models/playthrough_details.dart +++ b/board_games_companion/lib/models/playthrough_details.dart @@ -1,9 +1,9 @@ -import 'package:board_games_companion/models/hive/playthrough_note.dart'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../common/enums/playthrough_status.dart'; import 'hive/playthrough.dart'; +import 'hive/playthrough_note.dart'; import 'player_score.dart'; part 'playthrough_details.freezed.dart'; diff --git a/board_games_companion/lib/pages/board_game_details/board_game_details_expansions.dart b/board_games_companion/lib/pages/board_game_details/board_game_details_expansions.dart index 0d302514..5466c5f4 100644 --- a/board_games_companion/lib/pages/board_game_details/board_game_details_expansions.dart +++ b/board_games_companion/lib/pages/board_game_details/board_game_details_expansions.dart @@ -150,9 +150,10 @@ class _Expansion extends StatelessWidget { context, BoardGamesDetailsPage.pageRoute, arguments: BoardGameDetailsPageArguments( - _boardGameExpansion.id, - _boardGameExpansion.name, - BoardGamesDetailsPage, + boardGameId: _boardGameExpansion.id, + boardGameImageHeroId: _boardGameExpansion.id, + boardGameName: _boardGameExpansion.name, + navigatingFromType: BoardGamesDetailsPage, ), ); }, diff --git a/board_games_companion/lib/pages/board_game_details/board_game_details_page.dart b/board_games_companion/lib/pages/board_game_details/board_game_details_page.dart index 99fe94f1..58281e75 100644 --- a/board_games_companion/lib/pages/board_game_details/board_game_details_page.dart +++ b/board_games_companion/lib/pages/board_game_details/board_game_details_page.dart @@ -70,7 +70,7 @@ class BoardGamesDetailsPageState extends BasePageState { Observer( builder: (_) { return _Header( - boardGameId: widget.viewModel.boardGameId, + boardGameImageHeroId: widget.viewModel.boardGameImageHeroId, boardGameName: widget.viewModel.boardGameName, boardGameImageUrl: widget.viewModel.boardGameImageUrl, ); @@ -112,16 +112,16 @@ class BoardGamesDetailsPageState extends BasePageState { class _Header extends StatelessWidget { const _Header({ Key? key, - required String boardGameId, + required String boardGameImageHeroId, required String boardGameName, required String? boardGameImageUrl, - }) : _boardGameId = boardGameId, + }) : _boardGameImageHeroId = boardGameImageHeroId, _boardGameName = boardGameName, _boardGameImageUrl = boardGameImageUrl, super(key: key); final String _boardGameName; - final String _boardGameId; + final String _boardGameImageHeroId; final String? _boardGameImageUrl; @override @@ -156,7 +156,7 @@ class _Header extends StatelessWidget { background: _boardGameImageUrl == null ? const LoadingIndicator() : BoardGameImage( - id: _boardGameId, + id: _boardGameImageHeroId, url: _boardGameImageUrl, minImageHeight: Constants.boardGameDetailsImageHeight, ), diff --git a/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart b/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart index 78668cc5..30c0b0e3 100644 --- a/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart +++ b/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart @@ -29,6 +29,7 @@ abstract class _BoardGameDetailsViewModel with Store { late HtmlUnescape _htmlUnescape; late String _boardGameId; late String _boardGameName; + late String _boardGameImageHeroId; String? _boardGameImageUrl; @computed @@ -40,6 +41,8 @@ abstract class _BoardGameDetailsViewModel with Store { String get boardGameId => _boardGameId; + String get boardGameImageHeroId => _boardGameImageHeroId; + @computed String? get boardGameImageUrl { if (_boardGameImageUrl.isNotNullOrBlank) { @@ -123,6 +126,9 @@ abstract class _BoardGameDetailsViewModel with Store { void setBoardGameImageUrl(String? boardGameImageUrl) => _boardGameImageUrl = boardGameImageUrl; + void setBoardGameImageHeroId(String boardGameImageHeroId) => + _boardGameImageHeroId = boardGameImageHeroId; + Future captureLinkAnalytics(String linkName) async { await _analyticsService.logEvent( name: Analytics.boardGameDetailsLinks, diff --git a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.dart b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.dart index c8be2cc9..a2bafacb 100644 --- a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.dart +++ b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.dart @@ -4,7 +4,7 @@ import 'package:board_games_companion/models/hive/playthrough.dart'; import 'package:board_games_companion/models/hive/playthrough_note.dart'; import 'package:board_games_companion/models/player_score.dart'; import 'package:board_games_companion/models/playthrough_details.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:injectable/injectable.dart'; @@ -21,7 +21,7 @@ abstract class _EditPlaythoughViewModel with Store { _EditPlaythoughViewModel(this._playthroughsStore); late final String _playthroughId; - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _playthroughsStore; ValueNotifier isSpeedDialContextMenuOpen = ValueNotifier(false); diff --git a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.dart b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.dart index 78914a28..39248607 100644 --- a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.dart +++ b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.dart @@ -3,7 +3,7 @@ import 'package:basics/basics.dart'; import 'package:board_games_companion/models/hive/playthrough.dart'; import 'package:board_games_companion/models/playthrough_details.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:uuid/uuid.dart'; @@ -19,7 +19,7 @@ class PlaythroughNoteViewModel = _PlaythroughNoteViewModel with _$PlaythroughNot abstract class _PlaythroughNoteViewModel with Store { _PlaythroughNoteViewModel(this._playthroughsStore); - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _playthroughsStore; PlaythroughDetails get _playthroughDetails => _playthroughsStore.playthroughsDetails .firstWhere((playthroughDetails) => playthroughDetails.id == _playthrough!.id); diff --git a/board_games_companion/lib/pages/games/games_page.dart b/board_games_companion/lib/pages/games/games_page.dart index 0af07351..960a9b79 100644 --- a/board_games_companion/lib/pages/games/games_page.dart +++ b/board_games_companion/lib/pages/games/games_page.dart @@ -194,7 +194,7 @@ class _Collection extends StatelessWidget { ), ), ), - _Grid(boardGames: mainGames, analyticsService: analyticsService), + _Grid(boardGamesDetails: mainGames, analyticsService: analyticsService), ], if (hasExpansions) ...[ for (var expansionsMapEntry in expansionsMap.entries) ...[ @@ -207,7 +207,7 @@ class _Collection extends StatelessWidget { ), ), _Grid( - boardGames: expansionsMapEntry.value, + boardGamesDetails: expansionsMapEntry.value, analyticsService: analyticsService, ), ] @@ -327,17 +327,20 @@ class _AppBarState extends State<_AppBar> { } Future _handleSearchResultAction( - BoardGameDetails boardGame, BoardGameResultActionType actionType) async { + BoardGameDetails boardGameDetails, + BoardGameResultActionType actionType, + ) async { switch (actionType) { case BoardGameResultActionType.details: unawaited(Navigator.pushNamed( context, BoardGamesDetailsPage.pageRoute, arguments: BoardGameDetailsPageArguments( - boardGame.id, - boardGame.name, - GamesPage, - boardGameImageUrl: boardGame.imageUrl, + boardGameId: boardGameDetails.id, + boardGameName: boardGameDetails.name, + boardGameImageHeroId: boardGameDetails.id, + navigatingFromType: GamesPage, + boardGameImageUrl: boardGameDetails.imageUrl, ), )); break; @@ -345,7 +348,10 @@ class _AppBarState extends State<_AppBar> { unawaited(Navigator.pushNamed( context, PlaythroughsPage.pageRoute, - arguments: PlaythroughsPageArguments(boardGame), + arguments: PlaythroughsPageArguments( + boardGameDetails: boardGameDetails, + boardGameImageHeroId: boardGameDetails.id, + ), )); break; } @@ -355,11 +361,11 @@ class _AppBarState extends State<_AppBar> { class _Grid extends StatelessWidget { const _Grid({ Key? key, - required this.boardGames, + required this.boardGamesDetails, required this.analyticsService, }) : super(key: key); - final List boardGames; + final List boardGamesDetails; final AnalyticsService analyticsService; @override @@ -371,17 +377,20 @@ class _Grid extends StatelessWidget { mainAxisSpacing: Dimensions.standardSpacing, maxCrossAxisExtent: Dimensions.boardGameItemCollectionImageWidth, children: [ - for (var boardGame in boardGames) + for (var boardGameDetails in boardGamesDetails) BoardGameTile( - id: boardGame.id, - name: boardGame.name, - imageUrl: boardGame.thumbnailUrl ?? '', - rank: boardGame.rank, + id: boardGameDetails.id, + name: boardGameDetails.name, + imageUrl: boardGameDetails.thumbnailUrl ?? '', + rank: boardGameDetails.rank, elevation: AppStyles.defaultElevation, onTap: () => Navigator.pushNamed( context, PlaythroughsPage.pageRoute, - arguments: PlaythroughsPageArguments(boardGame), + arguments: PlaythroughsPageArguments( + boardGameDetails: boardGameDetails, + boardGameImageHeroId: boardGameDetails.id, + ), ), heroTag: AnimationTags.boardGameHeroTag, ) @@ -405,7 +414,7 @@ class _Empty extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: const [ - SizedBox(height: 40), + SizedBox(height: Dimensions.emptyPageTitleTopSpacing), Center( child: Text( 'Your games collection is empty', @@ -415,7 +424,7 @@ class _Empty extends StatelessWidget { SizedBox(height: Dimensions.doubleStandardSpacing), Icon( Icons.sentiment_dissatisfied_sharp, - size: 80, + size: Dimensions.emptyPageTitleIconSize, color: AppColors.primaryColor, ), SizedBox(height: Dimensions.doubleStandardSpacing), diff --git a/board_games_companion/lib/pages/games/games_view_model.dart b/board_games_companion/lib/pages/games/games_view_model.dart index 737a8c65..09b4355a 100644 --- a/board_games_companion/lib/pages/games/games_view_model.dart +++ b/board_games_companion/lib/pages/games/games_view_model.dart @@ -7,6 +7,8 @@ import 'package:board_games_companion/common/enums/order_by.dart'; import 'package:board_games_companion/common/enums/sort_by_option.dart'; import 'package:board_games_companion/models/sort_by.dart'; import 'package:board_games_companion/stores/board_games_filters_store.dart'; +import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/scores_store.dart'; import 'package:board_games_companion/stores/user_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -33,11 +35,15 @@ abstract class _GamesViewModel with Store { this._userStore, this._boardGamesStore, this._boardGamesFiltersStore, + this._scoresStore, + this._playthroughsStore, ); final UserStore _userStore; final BoardGamesStore _boardGamesStore; final BoardGamesFiltersStore _boardGamesFiltersStore; + final ScoresStore _scoresStore; + final PlaythroughsStore _playthroughsStore; @computed List get allMainGames => @@ -252,9 +258,12 @@ abstract class _GamesViewModel with Store { Future _loadBoardGames() async { try { + // TODO MK Think about if we could potentially load all of the data once and then use it across the app await _userStore.loadUser(); await _boardGamesStore.loadBoardGames(); await _boardGamesFiltersStore.loadFilterPreferences(); + await _playthroughsStore.loadPlaythroughs(); + await _scoresStore.loadScores(); } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); } diff --git a/board_games_companion/lib/pages/home/home_page.dart b/board_games_companion/lib/pages/home/home_page.dart index 6e34c53d..1fea9d25 100644 --- a/board_games_companion/lib/pages/home/home_page.dart +++ b/board_games_companion/lib/pages/home/home_page.dart @@ -1,4 +1,6 @@ +import 'package:board_games_companion/common/app_text.dart'; import 'package:board_games_companion/pages/home/home_view_model.dart'; +import 'package:board_games_companion/pages/playthroughs_history/playthroughs_history_page.dart'; import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:flutter/material.dart'; @@ -32,7 +34,7 @@ class HomePage extends StatefulWidget { class HomePageState extends BasePageState with SingleTickerProviderStateMixin { late final TabController tabController; - static const int _numberOfTabs = 3; + static const int _numberOfTabs = 4; static const int _initialTabIndex = 0; @override @@ -64,6 +66,7 @@ class HomePageState extends BasePageState with SingleTickerProviderSta widget.viewModel.rateAndReviewService, ), SearchBoardGamesPage(viewModel: widget.viewModel.searchBoardGamesViewModel), + PlaythroughsHistoryPage(viewModel: widget.viewModel.playthroughsHistoryViewModel), PlayersPage(viewModel: widget.viewModel.playersViewModel), ], ), @@ -75,17 +78,22 @@ class HomePageState extends BasePageState with SingleTickerProviderSta top: -Dimensions.bottomTabTopHeight, items: const [ TabItem( - title: 'Games', + title: AppText.homePageGamesTabTitle, icon: BottomTabIcon(iconData: Icons.video_library), activeIcon: BottomTabIcon(iconData: Icons.video_library, isActive: true), ), TabItem( - title: 'Search', + title: AppText.homePageSearchTabTitle, icon: BottomTabIcon(iconData: Icons.search), activeIcon: BottomTabIcon(iconData: Icons.search, isActive: true), ), TabItem( - title: 'Players', + title: AppText.homePageGamesHistoryTabTitle, + icon: BottomTabIcon(iconData: Icons.history), + activeIcon: BottomTabIcon(iconData: Icons.history, isActive: true), + ), + TabItem( + title: AppText.homePageGamesPlayersTabTitle, icon: BottomTabIcon(iconData: Icons.group), activeIcon: BottomTabIcon(iconData: Icons.group, isActive: true), ), diff --git a/board_games_companion/lib/pages/home/home_view_model.dart b/board_games_companion/lib/pages/home/home_view_model.dart index 22e07c78..da89fb86 100644 --- a/board_games_companion/lib/pages/home/home_view_model.dart +++ b/board_games_companion/lib/pages/home/home_view_model.dart @@ -1,5 +1,6 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/pages/playthroughs_history/playthroughs_history_view_model.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:tuple/tuple.dart'; @@ -24,6 +25,7 @@ abstract class _HomeViewModelBase with Store { this.boardGamesFiltersStore, this.gamesViewModel, this.searchBoardGamesViewModel, + this.playthroughsHistoryViewModel, ); final AnalyticsService analyticsService; @@ -32,11 +34,13 @@ abstract class _HomeViewModelBase with Store { final BoardGamesFiltersStore boardGamesFiltersStore; final GamesViewModel gamesViewModel; final SearchBoardGamesViewModel searchBoardGamesViewModel; + final PlaythroughsHistoryViewModel playthroughsHistoryViewModel; static const Map> _screenViewByTabIndex = { 0: Tuple2('Games', 'GamesPage'), 1: Tuple2('Search', 'SearchBoardGamesPage'), - 2: Tuple2('Players', 'PlayersPage'), + 2: Tuple2('Games History', 'PlaythroughsHistoryPage'), + 3: Tuple2('Players', 'PlayersPage'), }; Future trackTabChange(int tabIndex) async { diff --git a/board_games_companion/lib/pages/players/players_page.dart b/board_games_companion/lib/pages/players/players_page.dart index 674a1a43..7c70e3a8 100644 --- a/board_games_companion/lib/pages/players/players_page.dart +++ b/board_games_companion/lib/pages/players/players_page.dart @@ -236,7 +236,7 @@ class _NoPlayers extends StatelessWidget { padding: const EdgeInsets.all(Dimensions.doubleStandardSpacing), child: Column( children: const [ - SizedBox(height: 40), + SizedBox(height: Dimensions.emptyPageTitleTopSpacing), Center( child: Text( AppText.playersPageNoPlayersTitle, @@ -245,8 +245,8 @@ class _NoPlayers extends StatelessWidget { ), SizedBox(height: Dimensions.doubleStandardSpacing), Icon( - Icons.sentiment_dissatisfied_sharp, - size: 80, + Icons.people, + size: Dimensions.emptyPageTitleIconSize, color: AppColors.primaryColor, ), SizedBox(height: Dimensions.doubleStandardSpacing), diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart index 0cf50364..91f571cf 100644 --- a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart @@ -1,15 +1,14 @@ // ignore_for_file: library_private_types_in_public_api import 'package:board_games_companion/common/enums/game_winning_condition.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; +import 'package:board_games_companion/stores/scores_store.dart'; import 'package:collection/collection.dart'; import 'package:fimber/fimber.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:tuple/tuple.dart'; -import '../../common/enums/playthrough_status.dart'; -import '../../extensions/scores_extensions.dart'; import '../../models/board_game_statistics.dart'; import '../../models/hive/player.dart'; import '../../models/hive/playthrough.dart'; @@ -17,8 +16,6 @@ import '../../models/hive/score.dart'; import '../../models/player_score.dart'; import '../../models/player_statistics.dart'; import '../../services/player_service.dart'; -import '../../services/playthroughs_service.dart'; -import '../../services/score_service.dart'; part 'playthrough_statistics_view_model.g.dart'; @@ -29,15 +26,13 @@ class PlaythroughStatisticsViewModel = _PlaythroughStatisticsViewModel abstract class _PlaythroughStatisticsViewModel with Store { _PlaythroughStatisticsViewModel( this._playerService, - this._scoreService, - this._playthroughService, - this._playthroughsStore, + this._scoresStore, + this._gamePlaythroughsStore, ); final PlayerService _playerService; - final ScoreService _scoreService; - final PlaythroughService _playthroughService; - final PlaythroughsStore _playthroughsStore; + final ScoresStore _scoresStore; + final GamePlaythroughsStore _gamePlaythroughsStore; static const int _maxNumberOfTopScoresToDisplay = 5; @@ -47,13 +42,21 @@ abstract class _PlaythroughStatisticsViewModel with Store { ObservableFuture? futureLoadBoardGamesStatistics; @computed - String get boardGameId => _playthroughsStore.boardGameId; + List get _playthroughIds => + _gamePlaythroughsStore.playthroughs.map((playthrough) => playthrough.id).toList(); @computed - String? get boardGameImageUrl => _playthroughsStore.boardGameImageUrl; + List get _playthroughsScores => + _scoresStore.scores.where((score) => _playthroughIds.contains(score.playthroughId)).toList(); @computed - String get boardGameName => _playthroughsStore.boardGameName; + String get boardGameId => _gamePlaythroughsStore.boardGameId; + + @computed + String? get boardGameImageUrl => _gamePlaythroughsStore.boardGameImageUrl; + + @computed + String get boardGameName => _gamePlaythroughsStore.boardGameName; @action void loadBoardGamesStatistics() => @@ -61,31 +64,24 @@ abstract class _PlaythroughStatisticsViewModel with Store { Future _loadBoardGamesStatistics() async { Fimber.d( - 'Loading stats for game ${_playthroughsStore.boardGameName} [${_playthroughsStore.boardGameId}]'); - final boardGameId = _playthroughsStore.boardGameId; - final gameWinningCondition = _playthroughsStore.gameWinningCondition; + 'Loading stats for game ${_gamePlaythroughsStore.boardGameName} [${_gamePlaythroughsStore.boardGameId}]'); + final boardGameId = _gamePlaythroughsStore.boardGameId; + final gameWinningCondition = _gamePlaythroughsStore.gameWinningCondition; final players = await _playerService.retrievePlayers(includeDeleted: true); final playersById = {for (Player player in players) player.id: player}; - final boardGamePlaythroughs = await _playthroughService.retrievePlaythroughs([boardGameId]); - if (boardGamePlaythroughs.isEmpty) { + if (_gamePlaythroughsStore.playthroughs.isEmpty) { boardGameStatistics = BoardGameStatistics(); return; } - // MK Retrieve scores - final Iterable playthroughIds = boardGamePlaythroughs.map((p) => p.id); - final List playthroughsScores = await _scoreService.retrieveScores(playthroughIds); final Map> playthroughScoresByPlaythroughId = - groupBy(playthroughsScores, (s) => s.playthroughId!); + groupBy(_playthroughsScores, (s) => s.playthroughId!); final Map> playthroughScoresByBoardGameId = - groupBy(playthroughsScores, (s) => s.boardGameId); - - final List finishedPlaythroughs = boardGamePlaythroughs - .where((p) => p.status == PlaythroughStatus.Finished && p.endDate != null) - .toList(); - finishedPlaythroughs.sort((a, b) => b.startDate.compareTo(a.startDate)); + groupBy(_playthroughsScores, (s) => s.boardGameId); + // MK Creating a local variable of finished playthroughs to avoid multitude of retrievals with a getter + final finishedPlaythroughs = _gamePlaythroughsStore.finishedPlaythroughs; _updateLastPlayedAndWinner( finishedPlaythroughs, boardGameStatistics, @@ -222,7 +218,8 @@ abstract class _PlaythroughStatisticsViewModel with Store { final Map playerWins = {}; for (final Playthrough finishedPlaythrough in finishedPlaythroughs) { final List playthroughScores = playthroughScoresByPlaythroughId[finishedPlaythrough.id] - .sortByScore(gameWinningCondition)!; + .sortByScore(gameWinningCondition) ?? + []; if (playthroughScores.isEmpty) { continue; } diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.g.dart b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.g.dart index f2e6de73..6af51306 100644 --- a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.g.dart +++ b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.g.dart @@ -10,6 +10,20 @@ part of 'playthrough_statistics_view_model.dart'; mixin _$PlaythroughStatisticsViewModel on _PlaythroughStatisticsViewModel, Store { + Computed>? _$_playthroughIdsComputed; + + @override + List get _playthroughIds => (_$_playthroughIdsComputed ??= + Computed>(() => super._playthroughIds, + name: '_PlaythroughStatisticsViewModel._playthroughIds')) + .value; + Computed>? _$_playthroughsScoresComputed; + + @override + List get _playthroughsScores => (_$_playthroughsScoresComputed ??= + Computed>(() => super._playthroughsScores, + name: '_PlaythroughStatisticsViewModel._playthroughsScores')) + .value; Computed? _$boardGameIdComputed; @override diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_game_settings_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_game_settings_view_model.dart index c6560144..a5392f8d 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_game_settings_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_game_settings_view_model.dart @@ -1,6 +1,6 @@ import 'package:board_games_companion/common/enums/game_winning_condition.dart'; import 'package:board_games_companion/models/hive/board_game_settings.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:injectable/injectable.dart'; import '../../stores/board_games_store.dart'; @@ -10,7 +10,7 @@ class PlaythroughsGameSettingsViewModel { PlaythroughsGameSettingsViewModel(this._boardGamesStore, this._playthroughsStore); final BoardGamesStore _boardGamesStore; - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _playthroughsStore; GameWinningCondition get winningCondition => _playthroughsStore.gameWinningCondition; diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart index 12f9a257..62048f45 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart @@ -1,6 +1,6 @@ // ignore_for_file: library_private_types_in_public_api -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; @@ -15,7 +15,7 @@ class PlaythroughsHistoryViewModel = _PlaythroughsHistoryViewModel abstract class _PlaythroughsHistoryViewModel with Store { _PlaythroughsHistoryViewModel(this._playthroughsStore); - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _playthroughsStore; @observable ObservableFuture? futureloadPlaythroughs; diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_page.dart index 96a48c9b..fff8a9f8 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_page.dart @@ -638,20 +638,16 @@ class _Players extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: Observer( - builder: (_) { - return SizedBox( - height: 34, - width: 34, - child: Checkbox( - checkColor: AppColors.accentColor, - activeColor: AppColors.primaryColor.withOpacity(0.7), - value: playthroughPlayer.isChecked, - onChanged: (bool? isChecked) => - onPlayerSelectionChanged(isChecked, playthroughPlayer), - ), - ); - }, + child: SizedBox( + height: 34, + width: 34, + child: Checkbox( + checkColor: AppColors.accentColor, + activeColor: AppColors.primaryColor.withOpacity(0.7), + value: playthroughPlayer.isChecked, + onChanged: (bool? isChecked) => + onPlayerSelectionChanged(isChecked, playthroughPlayer), + ), ), ), ], diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.dart index 1204c7a1..e5c73453 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.dart @@ -1,7 +1,7 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:board_games_companion/stores/players_store.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:uuid/uuid.dart'; @@ -24,12 +24,12 @@ class PlaythroughsLogGameViewModel = _PlaythroughsLogGameViewModel abstract class _PlaythroughsLogGameViewModel with Store { _PlaythroughsLogGameViewModel( this._playersStore, - this._playthroughsStore, + this._gamePlaythroughsStore, this._analyticsService, ); final PlayersStore _playersStore; - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _gamePlaythroughsStore; final AnalyticsService _analyticsService; @observable @@ -54,7 +54,7 @@ abstract class _PlaythroughsLogGameViewModel with Store { ObservableList playthroughPlayers = ObservableList.of([]); @computed - String get boardGameId => _playthroughsStore.boardGameId; + String get boardGameId => _gamePlaythroughsStore.boardGameId; @computed bool get anyPlayerSelected => playthroughPlayers.any((player) => player.isChecked); @@ -77,7 +77,7 @@ abstract class _PlaythroughsLogGameViewModel with Store { score: Score( id: const Uuid().v4(), playerId: playthroughPlayer.player.id, - boardGameId: _playthroughsStore.boardGameId, + boardGameId: _gamePlaythroughsStore.boardGameId, ), ); } @@ -98,7 +98,7 @@ abstract class _PlaythroughsLogGameViewModel with Store { @action Future createPlaythrough(String boardGameId) async { - final PlaythroughDetails? newPlaythrough = await _playthroughsStore.createPlaythrough( + final PlaythroughDetails? newPlaythrough = await _gamePlaythroughsStore.createPlaythrough( boardGameId, _selectedPlaythroughPlayers, playerScores, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart index 279a8aea..10fcff69 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart @@ -96,11 +96,13 @@ class PlaythroughsPageState extends BasePageState child: PageContainer( child: TabBarView( controller: tabController, - children: const [ - PlaythroughStatistcsPage(), - PlaythroughsHistoryPage(), - PlaythroughsLogGamePage(), - PlaythroughsGameSettingsPage() + children: [ + PlaythroughStatistcsPage( + boardGameImageHeroId: widget.viewModel.boardGameImageHeroId, + ), + const PlaythroughsHistoryPage(), + const PlaythroughsLogGamePage(), + const PlaythroughsGameSettingsPage() ], ), ), @@ -152,9 +154,10 @@ class PlaythroughsPageState extends BasePageState context, BoardGamesDetailsPage.pageRoute, arguments: BoardGameDetailsPageArguments( - widget.viewModel.boardGameId, - widget.viewModel.boardGameName, - PlaythroughsPage, + boardGameId: widget.viewModel.boardGameId, + boardGameImageHeroId: widget.viewModel.boardGameId, + boardGameName: widget.viewModel.boardGameName, + navigatingFromType: PlaythroughsPage, ), ); } diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_statistics_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_statistics_page.dart index adc2cd4a..36a71974 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_statistics_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_statistics_page.dart @@ -27,7 +27,12 @@ import '../../widgets/playthrough/player_score_rank_avatar.dart'; import 'playthrough_statistics_view_model.dart'; class PlaythroughStatistcsPage extends StatefulWidget { - const PlaythroughStatistcsPage({Key? key}) : super(key: key); + const PlaythroughStatistcsPage({ + required this.boardGameImageHeroId, + Key? key, + }) : super(key: key); + + final String boardGameImageHeroId; @override PlaythroughStatistcsPageState createState() => PlaythroughStatistcsPageState(); @@ -57,7 +62,7 @@ class PlaythroughStatistcsPageState extends State { collapseMode: CollapseMode.parallax, centerTitle: true, background: BoardGameImage( - id: viewModel.boardGameId, + id: widget.boardGameImageHeroId, url: viewModel.boardGameImageUrl, minImageHeight: Constants.boardGameDetailsImageHeight, ), @@ -586,16 +591,18 @@ class _OverallStatsSection extends StatelessWidget { Column( children: [ _StatisticsItem( - value: - boardGameStatistics?.averagePlaytimeInSeconds?.toPlaytimeDuration('-') ?? '-', + value: boardGameStatistics?.averagePlaytimeInSeconds + ?.toPlaytimeDuration(fallbackValue: '-') ?? + '-', icon: Icons.av_timer, iconColor: AppColors.averagePlaytimeStatColor, subtitle: AppText.playthroughsStatisticsPageOverallStatsAvgPlaytime, ), const SizedBox(height: Dimensions.doubleStandardSpacing), _StatisticsItem( - value: - boardGameStatistics?.totalPlaytimeInSeconds?.toPlaytimeDuration('-') ?? '-', + value: boardGameStatistics?.totalPlaytimeInSeconds + ?.toPlaytimeDuration(fallbackValue: '-') ?? + '-', icon: Icons.timelapse, iconColor: AppColors.totalPlaytimeStatColor, subtitle: AppText.playthroughsStatisticsPageOverallStatsTotalPlaytime, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart index 7eef572f..045f57ff 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart @@ -3,7 +3,7 @@ import 'package:basics/basics.dart'; import 'package:board_games_companion/models/import_result.dart'; import 'package:board_games_companion/models/playthrough_details.dart'; -import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/game_playthroughs_store.dart'; import 'package:board_games_companion/stores/user_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:injectable/injectable.dart'; @@ -47,16 +47,20 @@ abstract class _PlaythroughsViewModel with Store { }; final PlayersStore _playersStore; - final PlaythroughsStore _playthroughsStore; + final GamePlaythroughsStore _playthroughsStore; final AnalyticsService _analyticsService; final BoardGamesService _boardGamesService; final UserStore _userStore; + late String _boardGameImageHeroId; + List? _playthroughPlayers; List? get playthroughPlayers => _playthroughPlayers; BggPlaysImportRaport? bggPlaysImportRaport; + String get boardGameImageHeroId => _boardGameImageHeroId; + @computed String get boardGameId => _playthroughsStore.boardGameId; @@ -73,9 +77,10 @@ abstract class _PlaythroughsViewModel with Store { String get gamePlaylistUrl => '$melodicePlaylistUrl/$boardGameId'; @action - void setBoardGame(BoardGameDetails boardGame) { - _playthroughsStore.setBoardGame(boardGame); - } + void setBoardGame(BoardGameDetails boardGame) => _playthroughsStore.setBoardGame(boardGame); + + void setBoardGameImageHeroId(String boardGameImageHeroId) => + _boardGameImageHeroId = boardGameImageHeroId; Future importPlays(String username, String boardGameId) async { await _analyticsService.logEvent( diff --git a/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.dart b/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.dart new file mode 100644 index 00000000..ccf3b0e4 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.dart @@ -0,0 +1,26 @@ +import 'package:board_games_companion/common/enums/game_winning_condition.dart'; +import 'package:board_games_companion/models/hive/board_game_details.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../models/player_score.dart'; +import '../../models/playthrough_details.dart'; + +part 'board_game_playthrough.freezed.dart'; + +@freezed +class BoardGamePlaythrough with _$BoardGamePlaythrough { + const factory BoardGamePlaythrough({ + required PlaythroughDetails playthrough, + required BoardGameDetails boardGameDetails, + }) = _BoardGamePlaythrough; + + const BoardGamePlaythrough._(); + + PlayerScore get winner { + final sortedPlayerScores = playthrough.playerScores.toList().sortByScore( + boardGameDetails.settings?.winningCondition ?? GameWinningCondition.HighestScore); + return sortedPlayerScores.first; + } + + String get id => '${boardGameDetails.id}${playthrough.id}'; +} diff --git a/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.freezed.dart b/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.freezed.dart new file mode 100644 index 00000000..32d0a506 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/board_game_playthrough.freezed.dart @@ -0,0 +1,181 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'board_game_playthrough.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$BoardGamePlaythrough { + PlaythroughDetails get playthrough => throw _privateConstructorUsedError; + BoardGameDetails get boardGameDetails => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $BoardGamePlaythroughCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BoardGamePlaythroughCopyWith<$Res> { + factory $BoardGamePlaythroughCopyWith(BoardGamePlaythrough value, + $Res Function(BoardGamePlaythrough) then) = + _$BoardGamePlaythroughCopyWithImpl<$Res>; + $Res call( + {PlaythroughDetails playthrough, BoardGameDetails boardGameDetails}); + + $PlaythroughDetailsCopyWith<$Res> get playthrough; + $BoardGameDetailsCopyWith<$Res> get boardGameDetails; +} + +/// @nodoc +class _$BoardGamePlaythroughCopyWithImpl<$Res> + implements $BoardGamePlaythroughCopyWith<$Res> { + _$BoardGamePlaythroughCopyWithImpl(this._value, this._then); + + final BoardGamePlaythrough _value; + // ignore: unused_field + final $Res Function(BoardGamePlaythrough) _then; + + @override + $Res call({ + Object? playthrough = freezed, + Object? boardGameDetails = freezed, + }) { + return _then(_value.copyWith( + playthrough: playthrough == freezed + ? _value.playthrough + : playthrough // ignore: cast_nullable_to_non_nullable + as PlaythroughDetails, + boardGameDetails: boardGameDetails == freezed + ? _value.boardGameDetails + : boardGameDetails // ignore: cast_nullable_to_non_nullable + as BoardGameDetails, + )); + } + + @override + $PlaythroughDetailsCopyWith<$Res> get playthrough { + return $PlaythroughDetailsCopyWith<$Res>(_value.playthrough, (value) { + return _then(_value.copyWith(playthrough: value)); + }); + } + + @override + $BoardGameDetailsCopyWith<$Res> get boardGameDetails { + return $BoardGameDetailsCopyWith<$Res>(_value.boardGameDetails, (value) { + return _then(_value.copyWith(boardGameDetails: value)); + }); + } +} + +/// @nodoc +abstract class _$$_BoardGamePlaythroughCopyWith<$Res> + implements $BoardGamePlaythroughCopyWith<$Res> { + factory _$$_BoardGamePlaythroughCopyWith(_$_BoardGamePlaythrough value, + $Res Function(_$_BoardGamePlaythrough) then) = + __$$_BoardGamePlaythroughCopyWithImpl<$Res>; + @override + $Res call( + {PlaythroughDetails playthrough, BoardGameDetails boardGameDetails}); + + @override + $PlaythroughDetailsCopyWith<$Res> get playthrough; + @override + $BoardGameDetailsCopyWith<$Res> get boardGameDetails; +} + +/// @nodoc +class __$$_BoardGamePlaythroughCopyWithImpl<$Res> + extends _$BoardGamePlaythroughCopyWithImpl<$Res> + implements _$$_BoardGamePlaythroughCopyWith<$Res> { + __$$_BoardGamePlaythroughCopyWithImpl(_$_BoardGamePlaythrough _value, + $Res Function(_$_BoardGamePlaythrough) _then) + : super(_value, (v) => _then(v as _$_BoardGamePlaythrough)); + + @override + _$_BoardGamePlaythrough get _value => super._value as _$_BoardGamePlaythrough; + + @override + $Res call({ + Object? playthrough = freezed, + Object? boardGameDetails = freezed, + }) { + return _then(_$_BoardGamePlaythrough( + playthrough: playthrough == freezed + ? _value.playthrough + : playthrough // ignore: cast_nullable_to_non_nullable + as PlaythroughDetails, + boardGameDetails: boardGameDetails == freezed + ? _value.boardGameDetails + : boardGameDetails // ignore: cast_nullable_to_non_nullable + as BoardGameDetails, + )); + } +} + +/// @nodoc + +class _$_BoardGamePlaythrough extends _BoardGamePlaythrough { + const _$_BoardGamePlaythrough( + {required this.playthrough, required this.boardGameDetails}) + : super._(); + + @override + final PlaythroughDetails playthrough; + @override + final BoardGameDetails boardGameDetails; + + @override + String toString() { + return 'BoardGamePlaythrough(playthrough: $playthrough, boardGameDetails: $boardGameDetails)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_BoardGamePlaythrough && + const DeepCollectionEquality() + .equals(other.playthrough, playthrough) && + const DeepCollectionEquality() + .equals(other.boardGameDetails, boardGameDetails)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(playthrough), + const DeepCollectionEquality().hash(boardGameDetails)); + + @JsonKey(ignore: true) + @override + _$$_BoardGamePlaythroughCopyWith<_$_BoardGamePlaythrough> get copyWith => + __$$_BoardGamePlaythroughCopyWithImpl<_$_BoardGamePlaythrough>( + this, _$identity); +} + +abstract class _BoardGamePlaythrough extends BoardGamePlaythrough { + const factory _BoardGamePlaythrough( + {required final PlaythroughDetails playthrough, + required final BoardGameDetails boardGameDetails}) = + _$_BoardGamePlaythrough; + const _BoardGamePlaythrough._() : super._(); + + @override + PlaythroughDetails get playthrough; + @override + BoardGameDetails get boardGameDetails; + @override + @JsonKey(ignore: true) + _$$_BoardGamePlaythroughCopyWith<_$_BoardGamePlaythrough> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.dart b/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.dart new file mode 100644 index 00000000..7f01d350 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; + +import '../../common/app_text.dart'; +import '../../extensions/date_time_extensions.dart'; +import '../../extensions/string_extensions.dart'; +import 'board_game_playthrough.dart'; + +part 'grouped_board_game_playthroughs.freezed.dart'; + +final DateFormat playthroughGroupingDateFormat = DateFormat('d MMMM y'); + +@freezed +class GroupedBoardGamePlaythroughs with _$GroupedBoardGamePlaythroughs { + const factory GroupedBoardGamePlaythroughs({ + required DateTime date, + required List boardGamePlaythroughs, + }) = _GroupedBoardGamePlaythroughs; + + const GroupedBoardGamePlaythroughs._(); + + String get dateFormtted { + if (date.isToday) { + return AppText.today.toCapitalized(); + } + + if (date.isYesterday) { + return AppText.yesteday.toCapitalized(); + } + + if (date.isDayBeforeYesterday) { + return AppText.dayBeforeYesteday.toCapitalized(); + } + + return playthroughGroupingDateFormat.format(date); + } +} diff --git a/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.freezed.dart b/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.freezed.dart new file mode 100644 index 00000000..b22145bb --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/grouped_board_game_playthroughs.freezed.dart @@ -0,0 +1,168 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'grouped_board_game_playthroughs.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$GroupedBoardGamePlaythroughs { + DateTime get date => throw _privateConstructorUsedError; + List get boardGamePlaythroughs => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $GroupedBoardGamePlaythroughsCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GroupedBoardGamePlaythroughsCopyWith<$Res> { + factory $GroupedBoardGamePlaythroughsCopyWith( + GroupedBoardGamePlaythroughs value, + $Res Function(GroupedBoardGamePlaythroughs) then) = + _$GroupedBoardGamePlaythroughsCopyWithImpl<$Res>; + $Res call({DateTime date, List boardGamePlaythroughs}); +} + +/// @nodoc +class _$GroupedBoardGamePlaythroughsCopyWithImpl<$Res> + implements $GroupedBoardGamePlaythroughsCopyWith<$Res> { + _$GroupedBoardGamePlaythroughsCopyWithImpl(this._value, this._then); + + final GroupedBoardGamePlaythroughs _value; + // ignore: unused_field + final $Res Function(GroupedBoardGamePlaythroughs) _then; + + @override + $Res call({ + Object? date = freezed, + Object? boardGamePlaythroughs = freezed, + }) { + return _then(_value.copyWith( + date: date == freezed + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + boardGamePlaythroughs: boardGamePlaythroughs == freezed + ? _value.boardGamePlaythroughs + : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +abstract class _$$_GroupedBoardGamePlaythroughsCopyWith<$Res> + implements $GroupedBoardGamePlaythroughsCopyWith<$Res> { + factory _$$_GroupedBoardGamePlaythroughsCopyWith( + _$_GroupedBoardGamePlaythroughs value, + $Res Function(_$_GroupedBoardGamePlaythroughs) then) = + __$$_GroupedBoardGamePlaythroughsCopyWithImpl<$Res>; + @override + $Res call({DateTime date, List boardGamePlaythroughs}); +} + +/// @nodoc +class __$$_GroupedBoardGamePlaythroughsCopyWithImpl<$Res> + extends _$GroupedBoardGamePlaythroughsCopyWithImpl<$Res> + implements _$$_GroupedBoardGamePlaythroughsCopyWith<$Res> { + __$$_GroupedBoardGamePlaythroughsCopyWithImpl( + _$_GroupedBoardGamePlaythroughs _value, + $Res Function(_$_GroupedBoardGamePlaythroughs) _then) + : super(_value, (v) => _then(v as _$_GroupedBoardGamePlaythroughs)); + + @override + _$_GroupedBoardGamePlaythroughs get _value => + super._value as _$_GroupedBoardGamePlaythroughs; + + @override + $Res call({ + Object? date = freezed, + Object? boardGamePlaythroughs = freezed, + }) { + return _then(_$_GroupedBoardGamePlaythroughs( + date: date == freezed + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + boardGamePlaythroughs: boardGamePlaythroughs == freezed + ? _value._boardGamePlaythroughs + : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_GroupedBoardGamePlaythroughs extends _GroupedBoardGamePlaythroughs { + const _$_GroupedBoardGamePlaythroughs( + {required this.date, + required final List boardGamePlaythroughs}) + : _boardGamePlaythroughs = boardGamePlaythroughs, + super._(); + + @override + final DateTime date; + final List _boardGamePlaythroughs; + @override + List get boardGamePlaythroughs { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_boardGamePlaythroughs); + } + + @override + String toString() { + return 'GroupedBoardGamePlaythroughs(date: $date, boardGamePlaythroughs: $boardGamePlaythroughs)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GroupedBoardGamePlaythroughs && + const DeepCollectionEquality().equals(other.date, date) && + const DeepCollectionEquality() + .equals(other._boardGamePlaythroughs, _boardGamePlaythroughs)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(date), + const DeepCollectionEquality().hash(_boardGamePlaythroughs)); + + @JsonKey(ignore: true) + @override + _$$_GroupedBoardGamePlaythroughsCopyWith<_$_GroupedBoardGamePlaythroughs> + get copyWith => __$$_GroupedBoardGamePlaythroughsCopyWithImpl< + _$_GroupedBoardGamePlaythroughs>(this, _$identity); +} + +abstract class _GroupedBoardGamePlaythroughs + extends GroupedBoardGamePlaythroughs { + const factory _GroupedBoardGamePlaythroughs( + {required final DateTime date, + required final List boardGamePlaythroughs}) = + _$_GroupedBoardGamePlaythroughs; + const _GroupedBoardGamePlaythroughs._() : super._(); + + @override + DateTime get date; + @override + List get boardGamePlaythroughs; + @override + @JsonKey(ignore: true) + _$$_GroupedBoardGamePlaythroughsCopyWith<_$_GroupedBoardGamePlaythroughs> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_page.dart b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_page.dart new file mode 100644 index 00000000..367c7a80 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_page.dart @@ -0,0 +1,345 @@ +import 'package:board_games_companion/extensions/int_extensions.dart'; +import 'package:board_games_companion/pages/playthroughs/playthroughs_page.dart'; +import 'package:board_games_companion/pages/playthroughs_history/board_game_playthrough.dart'; +import 'package:board_games_companion/pages/playthroughs_history/grouped_board_game_playthroughs.dart'; +import 'package:board_games_companion/pages/playthroughs_history/playthroughs_history_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mobx/mobx.dart'; + +import '../../common/app_colors.dart'; +import '../../common/app_text.dart'; +import '../../common/app_theme.dart'; +import '../../common/dimensions.dart'; +import '../../models/navigation/board_game_details_page_arguments.dart'; +import '../../models/navigation/playthroughs_page_arguments.dart'; +import '../../widgets/board_games/board_game_tile.dart'; +import '../../widgets/common/loading_indicator_widget.dart'; +import '../../widgets/common/panel_container.dart'; +import '../../widgets/common/slivers/bgc_sliver_header_delegate.dart'; +import '../board_game_details/board_game_details_page.dart'; + +class PlaythroughsHistoryPage extends StatefulWidget { + const PlaythroughsHistoryPage({ + required this.viewModel, + Key? key, + }) : super(key: key); + + final PlaythroughsHistoryViewModel viewModel; + + @override + State createState() => _PlaythroughsHistoryPageState(); +} + +class _PlaythroughsHistoryPageState extends State { + @override + void initState() { + super.initState(); + + widget.viewModel.loadGamesPlaythroughs(); + } + + @override + Widget build(BuildContext context) => Observer( + builder: (_) { + switch (widget.viewModel.futureLoadGamesPlaythroughs?.status ?? FutureStatus.pending) { + case FutureStatus.pending: + case FutureStatus.rejected: + return const CustomScrollView( + slivers: [ + _AppBar(), + SliverFillRemaining(child: LoadingIndicator()), + ], + ); + case FutureStatus.fulfilled: + return CustomScrollView( + slivers: [ + const _AppBar(), + if (!widget.viewModel.hasAnyFinishedPlaythroughs) const _NoPlaythroughsSliver(), + for (final groupedBoardGamePlaythroughs + in widget.viewModel.finishedBoardGamePlaythroughs) ...[ + _PlaythroughGroupHeaderSliver( + widget: widget, + groupedBoardGamePlaythroughs: groupedBoardGamePlaythroughs, + ), + _PlaythroughGroupListSliver( + groupedBoardGamePlaythroughs: groupedBoardGamePlaythroughs, + ), + ] + ], + ); + } + }, + ); +} + +class _PlaythroughGroupListSliver extends StatelessWidget { + const _PlaythroughGroupListSliver({ + Key? key, + required this.groupedBoardGamePlaythroughs, + }) : super(key: key); + + final GroupedBoardGamePlaythroughs groupedBoardGamePlaythroughs; + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (_, index) { + final boardGamePlaythrough = groupedBoardGamePlaythroughs.boardGamePlaythroughs[index]; + return Padding( + padding: const EdgeInsets.only( + top: Dimensions.standardSpacing, + bottom: Dimensions.standardSpacing, + left: Dimensions.standardSpacing, + right: Dimensions.standardSpacing, + ), + child: PanelContainer( + child: Padding( + padding: const EdgeInsets.all(Dimensions.standardSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Row( + children: [ + SizedBox( + height: Dimensions.collectionSearchResultBoardGameImageHeight, + width: Dimensions.collectionSearchResultBoardGameImageWidth, + child: BoardGameTile( + id: boardGamePlaythrough.id, + imageUrl: boardGamePlaythrough.boardGameDetails.thumbnailUrl ?? '', + ), + ), + const SizedBox(width: Dimensions.standardSpacing), + Expanded( + child: _PlaythroughDetails(boardGamePlaythrough: boardGamePlaythrough), + ), + _PlaythroughActions( + onTapBoardGameDetails: () => + _navigateToBoardGameDetails(context, boardGamePlaythrough), + onTapPlaythroughs: () => + _navigateToPlaythrough(context, boardGamePlaythrough), + ), + ], + ), + ) + ], + ), + ), + ), + ); + }, + childCount: groupedBoardGamePlaythroughs.boardGamePlaythroughs.length, + ), + ); + } + + Future _navigateToPlaythrough( + BuildContext context, + BoardGamePlaythrough boardGamePlaythrough, + ) => + Navigator.pushNamed( + context, + PlaythroughsPage.pageRoute, + arguments: PlaythroughsPageArguments( + boardGameDetails: boardGamePlaythrough.boardGameDetails, + boardGameImageHeroId: boardGamePlaythrough.id, + ), + ); + + void _navigateToBoardGameDetails( + BuildContext context, + BoardGamePlaythrough boardGamePlaythrough, + ) { + Navigator.pushNamed( + context, + BoardGamesDetailsPage.pageRoute, + arguments: BoardGameDetailsPageArguments( + boardGameId: boardGamePlaythrough.boardGameDetails.id, + boardGameName: boardGamePlaythrough.boardGameDetails.name, + boardGameImageHeroId: boardGamePlaythrough.id, + navigatingFromType: PlaythroughsHistoryPage, + ), + ); + } +} + +class _PlaythroughDetails extends StatelessWidget { + const _PlaythroughDetails({ + Key? key, + required this.boardGamePlaythrough, + }) : super(key: key); + + static const double _playthroughStatsIconSize = 16; + static const double _playthroughStatsFontAwesomeIconSize = _playthroughStatsIconSize - 4; + + final BoardGamePlaythrough boardGamePlaythrough; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + boardGamePlaythrough.boardGameDetails.name, + overflow: TextOverflow.ellipsis, + style: AppTheme.theme.textTheme.bodyLarge, + ), + const SizedBox(height: Dimensions.standardSpacing), + _PlaythroughGeneralStats( + icon: const Icon( + FontAwesomeIcons.trophy, + size: _playthroughStatsFontAwesomeIconSize, + ), + statistic: + '${boardGamePlaythrough.winner.player?.name ?? ''} (${boardGamePlaythrough.winner.score.valueInt} points)', + ), + _PlaythroughGeneralStats( + icon: const Icon(Icons.people, size: _playthroughStatsIconSize), + statistic: '${boardGamePlaythrough.playthrough.playerScores.length} players', + ), + _PlaythroughGeneralStats( + icon: const Icon(Icons.hourglass_bottom, size: _playthroughStatsIconSize), + statistic: boardGamePlaythrough.playthrough.duration.inSeconds + .toPlaytimeDuration(showSeconds: false), + ), + ], + ); + } +} + +class _PlaythroughGeneralStats extends StatelessWidget { + const _PlaythroughGeneralStats({ + Key? key, + required this.icon, + required this.statistic, + }) : super(key: key); + + final Widget icon; + final String statistic; + + static const double _uniformedIconSize = 20; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: _uniformedIconSize, child: Center(child: icon)), + const SizedBox(width: Dimensions.standardSpacing), + Text( + statistic, + overflow: TextOverflow.ellipsis, + style: AppTheme.subTitleTextStyle.copyWith(color: AppColors.whiteColor), + ), + ], + ); + } +} + +class _PlaythroughActions extends StatelessWidget { + const _PlaythroughActions({ + Key? key, + required this.onTapBoardGameDetails, + required this.onTapPlaythroughs, + }) : super(key: key); + + final VoidCallback onTapBoardGameDetails; + final VoidCallback onTapPlaythroughs; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + IconButton( + icon: const Icon(Icons.info), + onPressed: () => onTapBoardGameDetails(), + ), + const Expanded(child: SizedBox.shrink()), + IconButton( + icon: const FaIcon(FontAwesomeIcons.dice), + onPressed: () => onTapPlaythroughs(), + ), + ], + ); + } +} + +class _PlaythroughGroupHeaderSliver extends StatelessWidget { + const _PlaythroughGroupHeaderSliver({ + Key? key, + required this.widget, + required this.groupedBoardGamePlaythroughs, + }) : super(key: key); + + final PlaythroughsHistoryPage widget; + final GroupedBoardGamePlaythroughs groupedBoardGamePlaythroughs; + + @override + Widget build(BuildContext context) { + return SliverPersistentHeader( + delegate: BgcSliverHeaderDelegate(primaryTitle: groupedBoardGamePlaythroughs.dateFormtted), + ); + } +} + +class _NoPlaythroughsSliver extends StatelessWidget { + const _NoPlaythroughsSliver({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => SliverPadding( + padding: const EdgeInsets.all(Dimensions.doubleStandardSpacing), + sliver: SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + SizedBox(height: Dimensions.emptyPageTitleTopSpacing), + Center( + child: Text( + AppText.playHistoryPageEmptyTitle, + style: TextStyle(fontSize: Dimensions.extraLargeFontSize), + ), + ), + SizedBox(height: Dimensions.doubleStandardSpacing), + FaIcon( + FontAwesomeIcons.dice, + size: Dimensions.emptyPageTitleIconSize, + color: AppColors.primaryColor, + ), + SizedBox(height: Dimensions.doubleStandardSpacing), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: AppText.playHistoryPageEmptyTextPartOne, + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: AppText.playHistoryPageEmptyTextPartTwo), + ], + ), + textAlign: TextAlign.justify, + style: TextStyle(fontSize: Dimensions.mediumFontSize), + ), + ], + ), + ), + ); +} + +class _AppBar extends StatelessWidget { + const _AppBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SliverAppBar( + pinned: true, + floating: true, + elevation: Dimensions.defaultElevation, + titleSpacing: Dimensions.standardSpacing, + foregroundColor: AppColors.accentColor, + title: Text(AppText.playHistoryPageTitle, style: AppTheme.titleTextStyle), + ); + } +} diff --git a/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.dart b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.dart new file mode 100644 index 00000000..c0816ff2 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.dart @@ -0,0 +1,106 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:basics/basics.dart'; +import 'package:board_games_companion/models/hive/playthrough.dart'; +import 'package:board_games_companion/models/player_score.dart'; +import 'package:board_games_companion/models/playthrough_details.dart'; +import 'package:board_games_companion/stores/board_games_store.dart'; +import 'package:board_games_companion/stores/players_store.dart'; +import 'package:board_games_companion/stores/scores_store.dart'; +import 'package:collection/collection.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; + +import '../../models/hive/score.dart'; +import '../../stores/playthroughs_store.dart'; +import 'board_game_playthrough.dart'; +import 'grouped_board_game_playthroughs.dart'; + +part 'playthroughs_history_view_model.g.dart'; + +@injectable +class PlaythroughsHistoryViewModel = _PlaythroughsHistoryViewModel + with _$PlaythroughsHistoryViewModel; + +abstract class _PlaythroughsHistoryViewModel with Store { + _PlaythroughsHistoryViewModel( + this._playthroughsStore, + this._boardGamesStore, + this._playersStore, + this._scoreStore, + ); + + final PlaythroughsStore _playthroughsStore; + final BoardGamesStore _boardGamesStore; + final PlayersStore _playersStore; + final ScoresStore _scoreStore; + + @computed + Map get _scores { + return { + for (final Score score + in _scoreStore.scores.where((Score score) => score.playthroughId.isNotNullOrBlank)) + score.toMapKey(): score + }; + } + + @observable + ObservableFuture? futureLoadGamesPlaythroughs; + + @computed + List get finishedPlaythroughs => _playthroughsStore.finishedPlaythroughs.toList(); + + @computed + List get finishedBoardGamePlaythroughs { + final result = []; + final finishedPlaythroughsGrouped = groupBy( + finishedPlaythroughs + ..sort((playthroughA, playthroughB) => + playthroughB.endDate!.compareTo(playthroughA.endDate!)), + (Playthrough playthrough) => playthroughGroupingDateFormat.format(playthrough.endDate!)); + + for (final playthroughsEntry in finishedPlaythroughsGrouped.entries) { + result.add( + GroupedBoardGamePlaythroughs( + date: playthroughGroupingDateFormat.parse(playthroughsEntry.key), + boardGamePlaythroughs: playthroughsEntry.value + .map((playthrough) => BoardGamePlaythrough( + playthrough: PlaythroughDetails( + playthrough: playthrough, + playerScores: [ + for (final playerId in playthrough.playerIds) + PlayerScore( + player: _playersStore.playersById[playerId], + score: _scores['${playthrough.id}$playerId'] ?? + Score( + id: '', + playerId: playerId, + boardGameId: playthrough.boardGameId, + ), + ) + ], + ), + boardGameDetails: + _boardGamesStore.allBoardGamesInCollectionsMap[playthrough.boardGameId]!, + )) + .toList(), + ), + ); + } + + return result; + } + + @computed + bool get hasAnyFinishedPlaythroughs => finishedPlaythroughs.isNotEmpty; + + @action + void loadGamesPlaythroughs() => + futureLoadGamesPlaythroughs = ObservableFuture(_loadGamesPlaythroughs()); + + Future _loadGamesPlaythroughs() async { + await _playthroughsStore.loadPlaythroughs(); + await _playersStore.loadPlayers(); + await _scoreStore.loadScores(); + } +} diff --git a/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.g.dart b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.g.dart new file mode 100644 index 00000000..6a0f8a40 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs_history/playthroughs_history_view_model.g.dart @@ -0,0 +1,89 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'playthroughs_history_view_model.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$PlaythroughsHistoryViewModel on _PlaythroughsHistoryViewModel, Store { + Computed>? _$_scoresComputed; + + @override + Map get _scores => + (_$_scoresComputed ??= Computed>(() => super._scores, + name: '_PlaythroughsHistoryViewModel._scores')) + .value; + Computed>? _$finishedPlaythroughsComputed; + + @override + List get finishedPlaythroughs => + (_$finishedPlaythroughsComputed ??= Computed>( + () => super.finishedPlaythroughs, + name: '_PlaythroughsHistoryViewModel.finishedPlaythroughs')) + .value; + Computed>? + _$finishedBoardGamePlaythroughsComputed; + + @override + List get finishedBoardGamePlaythroughs => + (_$finishedBoardGamePlaythroughsComputed ??= Computed< + List>( + () => super.finishedBoardGamePlaythroughs, + name: + '_PlaythroughsHistoryViewModel.finishedBoardGamePlaythroughs')) + .value; + Computed? _$hasAnyFinishedPlaythroughsComputed; + + @override + bool get hasAnyFinishedPlaythroughs => + (_$hasAnyFinishedPlaythroughsComputed ??= Computed( + () => super.hasAnyFinishedPlaythroughs, + name: '_PlaythroughsHistoryViewModel.hasAnyFinishedPlaythroughs')) + .value; + + late final _$futureLoadGamesPlaythroughsAtom = Atom( + name: '_PlaythroughsHistoryViewModel.futureLoadGamesPlaythroughs', + context: context); + + @override + ObservableFuture? get futureLoadGamesPlaythroughs { + _$futureLoadGamesPlaythroughsAtom.reportRead(); + return super.futureLoadGamesPlaythroughs; + } + + @override + set futureLoadGamesPlaythroughs(ObservableFuture? value) { + _$futureLoadGamesPlaythroughsAtom + .reportWrite(value, super.futureLoadGamesPlaythroughs, () { + super.futureLoadGamesPlaythroughs = value; + }); + } + + late final _$_PlaythroughsHistoryViewModelActionController = + ActionController(name: '_PlaythroughsHistoryViewModel', context: context); + + @override + void loadGamesPlaythroughs() { + final _$actionInfo = + _$_PlaythroughsHistoryViewModelActionController.startAction( + name: '_PlaythroughsHistoryViewModel.loadGamesPlaythroughs'); + try { + return super.loadGamesPlaythroughs(); + } finally { + _$_PlaythroughsHistoryViewModelActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +futureLoadGamesPlaythroughs: ${futureLoadGamesPlaythroughs}, +finishedPlaythroughs: ${finishedPlaythroughs}, +finishedBoardGamePlaythroughs: ${finishedBoardGamePlaythroughs}, +hasAnyFinishedPlaythroughs: ${hasAnyFinishedPlaythroughs} + '''; + } +} diff --git a/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart b/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart index 0fbcf2c1..45efd7c1 100644 --- a/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart +++ b/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart @@ -87,9 +87,10 @@ class SearchBoardGamesPageState extends State { context, BoardGamesDetailsPage.pageRoute, arguments: BoardGameDetailsPageArguments( - boardGame.id, - boardGame.name, - SearchBoardGamesPage, + boardGameId: boardGame.id, + boardGameImageHeroId: boardGame.id, + boardGameName: boardGame.name, + navigatingFromType: SearchBoardGamesPage, boardGameImageUrl: widget.viewModel.getHotBoardGameDetails(boardGame.id)?.imageUrl, ), ); diff --git a/board_games_companion/lib/services/playthroughs_service.dart b/board_games_companion/lib/services/playthroughs_service.dart index a857c605..6de427b5 100644 --- a/board_games_companion/lib/services/playthroughs_service.dart +++ b/board_games_companion/lib/services/playthroughs_service.dart @@ -15,7 +15,19 @@ class PlaythroughService extends BaseHiveService> retrievePlaythroughs( + Future> retrievePlaythroughs() async { + if (!await ensureBoxOpen()) { + return []; + } + + return storageBox + .toMap() + .values + .where((playthrough) => !(playthrough.isDeleted ?? false)) + .toList(); + } + + Future> retrieveGamePlaythroughs( List boardGameIds, { bool includeDeleted = false, }) async { diff --git a/board_games_companion/lib/services/score_service.dart b/board_games_companion/lib/services/score_service.dart index 7e30838b..c753e174 100644 --- a/board_games_companion/lib/services/score_service.dart +++ b/board_games_companion/lib/services/score_service.dart @@ -1,3 +1,4 @@ +import 'package:basics/basics.dart'; import 'package:injectable/injectable.dart'; import '../models/hive/score.dart'; @@ -21,15 +22,23 @@ class ScoreService extends BaseHiveService { return true; } - Future> retrieveScores(Iterable playthroughIds) async { - if ((playthroughIds.isEmpty) || !await ensureBoxOpen()) { + Future> retrieveScores() async { + if (!await ensureBoxOpen()) { + return []; + } + + return storageBox.toMap().values.toList(); + } + + Future> retrieveScoresForPlaythrough(String playthroughId) async { + if (playthroughId.isNullOrBlank || !await ensureBoxOpen()) { return []; } return storageBox .toMap() .values - .where((score) => playthroughIds.contains(score.playthroughId)) + .where((score) => playthroughId == score.playthroughId) .toList(); } } diff --git a/board_games_companion/lib/stores/board_games_store.dart b/board_games_companion/lib/stores/board_games_store.dart index 42f8ce26..e5674636 100644 --- a/board_games_companion/lib/stores/board_games_store.dart +++ b/board_games_companion/lib/stores/board_games_store.dart @@ -139,7 +139,7 @@ abstract class _BoardGamesStore with Store { Future removeAllBggBoardGames() async { try { final bggSyncedBoardGames = allBoardGames - .where((boardGame) => boardGame.isBggSynced!) + .where((boardGame) => boardGame.isBggSynced ?? false) .map((boardGame) => boardGame.id) .toList(); await _boardGamesService.removeBoardGames(bggSyncedBoardGames); diff --git a/board_games_companion/lib/stores/game_playthroughs_store.dart b/board_games_companion/lib/stores/game_playthroughs_store.dart new file mode 100644 index 00000000..1f2db7c4 --- /dev/null +++ b/board_games_companion/lib/stores/game_playthroughs_store.dart @@ -0,0 +1,160 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:board_games_companion/models/player_score.dart'; +import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/scores_store.dart'; +import 'package:collection/collection.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; + +import '../common/enums/game_winning_condition.dart'; +import '../models/hive/board_game_details.dart'; +import '../models/hive/player.dart'; +import '../models/hive/playthrough.dart'; +import '../models/hive/score.dart'; +import '../models/playthrough_details.dart'; +import '../models/playthrough_player.dart'; +import '../services/player_service.dart'; + +part 'game_playthroughs_store.g.dart'; + +@singleton +class GamePlaythroughsStore = _GamePlaythroughsStore with _$GamePlaythroughsStore; + +abstract class _GamePlaythroughsStore with Store { + _GamePlaythroughsStore(this._playthroughsStore, this._scoresStore, this._playerService); + + final PlaythroughsStore _playthroughsStore; + final ScoresStore _scoresStore; + final PlayerService _playerService; + + @observable + BoardGameDetails? _boardGame; + + @observable + ObservableList playthroughsDetails = ObservableList.of([]); + + @computed + List get playthroughs => _playthroughsStore.playthroughs + .where((playthrough) => playthrough.boardGameId == boardGameId) + .toList(); + + @computed + List get finishedPlaythroughs => _playthroughsStore.finishedPlaythroughs + .where((playthrough) => playthrough.boardGameId == boardGameId) + .toList() + ..sort((playthrough, otherPlaythrough) => + otherPlaythrough.startDate.compareTo(playthrough.startDate)); + + @computed + String get boardGameName => _boardGame!.name; + + @computed + String get boardGameId => _boardGame!.id; + + @computed + String? get boardGameImageUrl => _boardGame!.imageUrl; + + @computed + GameWinningCondition get gameWinningCondition => + _boardGame!.settings?.winningCondition ?? GameWinningCondition.HighestScore; + + @action + Future loadPlaythroughs() async { + try { + final loadedPlaythroughDetails = []; + await _playthroughsStore.loadPlaythroughs(); + for (final playthrough in playthroughs) { + final playthroughDetails = await createPlaythroughDetails(playthrough); + loadedPlaythroughDetails.add(playthroughDetails); + } + + playthroughsDetails = loadedPlaythroughDetails.asObservable(); + } catch (e, stack) { + FirebaseCrashlytics.instance.recordError(e, stack); + } + } + + @action + void setBoardGame(BoardGameDetails boardGame) => _boardGame = boardGame; + + Future createPlaythrough( + String boardGameId, + List playthoughPlayers, + Map playerScores, + DateTime startDate, + Duration? duration, { + int? bggPlayId, + }) async { + final newPlaythrough = await _playthroughsStore.createPlaythrough( + boardGameId, + playthoughPlayers, + playerScores, + startDate, + duration, + bggPlayId: bggPlayId, + ); + + if (newPlaythrough == null) { + return null; + } + + final newPlaythroughDetails = await createPlaythroughDetails(newPlaythrough); + playthroughsDetails.add(newPlaythroughDetails); + return newPlaythroughDetails; + } + + Future updatePlaythrough(PlaythroughDetails? playthroughDetails) async { + if (playthroughDetails?.id.isEmpty ?? true) { + return; + } + + try { + final updateSuceeded = + await _playthroughsStore.updatePlaythrough(playthroughDetails!.playthrough); + if (updateSuceeded) { + for (final PlayerScore playerScore in playthroughDetails.playerScores) { + await _scoresStore.addOrUpdateScore(playerScore.score); + } + + await loadPlaythroughs(); + } + } catch (e, stack) { + FirebaseCrashlytics.instance.recordError(e, stack); + } + } + + Future deletePlaythrough(String playthroughId) async { + try { + final deleteSucceeded = await _playthroughsStore.deletePlaythrough(playthroughId); + if (deleteSucceeded) { + playthroughsDetails.removeWhere((p) => p.playthrough.id == playthroughId); + } + + return deleteSucceeded; + } catch (e, stack) { + FirebaseCrashlytics.instance.recordError(e, stack); + } + + return false; + } + + Future createPlaythroughDetails(Playthrough playthrough) async { + final scores = + _scoresStore.scores.where((score) => score.playthroughId == playthrough.id).toList() + ..sortByScore(gameWinningCondition) + ..toList(); + final players = await _playerService.retrievePlayers( + playerIds: playthrough.playerIds, + includeDeleted: true, + ); + + final playerScores = scores.mapIndexed((int index, Score score) { + final player = players.firstWhereOrNull((Player p) => score.playerId == p.id); + return PlayerScore(player: player, score: score, place: index + 1); + }).toList(); + + return PlaythroughDetails(playthrough: playthrough, playerScores: playerScores); + } +} diff --git a/board_games_companion/lib/stores/game_playthroughs_store.g.dart b/board_games_companion/lib/stores/game_playthroughs_store.g.dart new file mode 100644 index 00000000..32535176 --- /dev/null +++ b/board_games_companion/lib/stores/game_playthroughs_store.g.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'game_playthroughs_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$GamePlaythroughsStore on _GamePlaythroughsStore, Store { + Computed>? _$playthroughsComputed; + + @override + List get playthroughs => (_$playthroughsComputed ??= + Computed>(() => super.playthroughs, + name: '_GamePlaythroughsStore.playthroughs')) + .value; + Computed>? _$finishedPlaythroughsComputed; + + @override + List get finishedPlaythroughs => + (_$finishedPlaythroughsComputed ??= Computed>( + () => super.finishedPlaythroughs, + name: '_GamePlaythroughsStore.finishedPlaythroughs')) + .value; + Computed? _$boardGameNameComputed; + + @override + String get boardGameName => + (_$boardGameNameComputed ??= Computed(() => super.boardGameName, + name: '_GamePlaythroughsStore.boardGameName')) + .value; + Computed? _$boardGameIdComputed; + + @override + String get boardGameId => + (_$boardGameIdComputed ??= Computed(() => super.boardGameId, + name: '_GamePlaythroughsStore.boardGameId')) + .value; + Computed? _$boardGameImageUrlComputed; + + @override + String? get boardGameImageUrl => (_$boardGameImageUrlComputed ??= + Computed(() => super.boardGameImageUrl, + name: '_GamePlaythroughsStore.boardGameImageUrl')) + .value; + Computed? _$gameWinningConditionComputed; + + @override + GameWinningCondition get gameWinningCondition => + (_$gameWinningConditionComputed ??= Computed( + () => super.gameWinningCondition, + name: '_GamePlaythroughsStore.gameWinningCondition')) + .value; + + late final _$_boardGameAtom = + Atom(name: '_GamePlaythroughsStore._boardGame', context: context); + + @override + BoardGameDetails? get _boardGame { + _$_boardGameAtom.reportRead(); + return super._boardGame; + } + + @override + set _boardGame(BoardGameDetails? value) { + _$_boardGameAtom.reportWrite(value, super._boardGame, () { + super._boardGame = value; + }); + } + + late final _$playthroughsDetailsAtom = Atom( + name: '_GamePlaythroughsStore.playthroughsDetails', context: context); + + @override + ObservableList get playthroughsDetails { + _$playthroughsDetailsAtom.reportRead(); + return super.playthroughsDetails; + } + + @override + set playthroughsDetails(ObservableList value) { + _$playthroughsDetailsAtom.reportWrite(value, super.playthroughsDetails, () { + super.playthroughsDetails = value; + }); + } + + late final _$loadPlaythroughsAsyncAction = + AsyncAction('_GamePlaythroughsStore.loadPlaythroughs', context: context); + + @override + Future loadPlaythroughs() { + return _$loadPlaythroughsAsyncAction.run(() => super.loadPlaythroughs()); + } + + late final _$_GamePlaythroughsStoreActionController = + ActionController(name: '_GamePlaythroughsStore', context: context); + + @override + void setBoardGame(BoardGameDetails boardGame) { + final _$actionInfo = _$_GamePlaythroughsStoreActionController.startAction( + name: '_GamePlaythroughsStore.setBoardGame'); + try { + return super.setBoardGame(boardGame); + } finally { + _$_GamePlaythroughsStoreActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +playthroughsDetails: ${playthroughsDetails}, +playthroughs: ${playthroughs}, +finishedPlaythroughs: ${finishedPlaythroughs}, +boardGameName: ${boardGameName}, +boardGameId: ${boardGameId}, +boardGameImageUrl: ${boardGameImageUrl}, +gameWinningCondition: ${gameWinningCondition} + '''; + } +} diff --git a/board_games_companion/lib/stores/players_store.dart b/board_games_companion/lib/stores/players_store.dart index df340dc0..553c28ec 100644 --- a/board_games_companion/lib/stores/players_store.dart +++ b/board_games_companion/lib/stores/players_store.dart @@ -31,6 +31,9 @@ abstract class _PlayersStore with Store { @observable ObservableList players = ObservableList.of([]); + @computed + Map get playersById => {for (final player in players) player.id: player}; + @action Future loadPlayers() async { if (players.isNotEmpty) { diff --git a/board_games_companion/lib/stores/players_store.g.dart b/board_games_companion/lib/stores/players_store.g.dart index 3e89a03c..286965cf 100644 --- a/board_games_companion/lib/stores/players_store.g.dart +++ b/board_games_companion/lib/stores/players_store.g.dart @@ -9,6 +9,14 @@ part of 'players_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers mixin _$PlayersStore on _PlayersStore, Store { + Computed>? _$playersByIdComputed; + + @override + Map get playersById => (_$playersByIdComputed ??= + Computed>(() => super.playersById, + name: '_PlayersStore.playersById')) + .value; + late final _$playersAtom = Atom(name: '_PlayersStore.players', context: context); @@ -53,7 +61,8 @@ mixin _$PlayersStore on _PlayersStore, Store { @override String toString() { return ''' -players: ${players} +players: ${players}, +playersById: ${playersById} '''; } } diff --git a/board_games_companion/lib/stores/playthroughs_store.dart b/board_games_companion/lib/stores/playthroughs_store.dart index dda06e34..5918492a 100644 --- a/board_games_companion/lib/stores/playthroughs_store.dart +++ b/board_games_companion/lib/stores/playthroughs_store.dart @@ -1,22 +1,15 @@ // ignore_for_file: library_private_types_in_public_api -import 'package:board_games_companion/extensions/scores_extensions.dart'; -import 'package:board_games_companion/models/player_score.dart'; -import 'package:board_games_companion/services/score_service.dart'; -import 'package:collection/collection.dart'; +import 'package:board_games_companion/common/enums/playthrough_status.dart'; +import 'package:board_games_companion/models/hive/playthrough.dart'; +import 'package:board_games_companion/services/playthroughs_service.dart'; +import 'package:board_games_companion/stores/scores_store.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; -import '../common/enums/game_winning_condition.dart'; -import '../models/hive/board_game_details.dart'; -import '../models/hive/player.dart'; -import '../models/hive/playthrough.dart'; -import '../models/hive/score.dart'; -import '../models/playthrough_details.dart'; +import '../models/player_score.dart'; import '../models/playthrough_player.dart'; -import '../services/player_service.dart'; -import '../services/playthroughs_service.dart'; part 'playthroughs_store.g.dart'; @@ -24,51 +17,31 @@ part 'playthroughs_store.g.dart'; class PlaythroughsStore = _PlaythroughsStore with _$PlaythroughsStore; abstract class _PlaythroughsStore with Store { - _PlaythroughsStore(this._playthroughService, this._scoreService, this._playerService); + _PlaythroughsStore(this._playthroughService, this._scoresStore); final PlaythroughService _playthroughService; - final ScoreService _scoreService; - final PlayerService _playerService; + final ScoresStore _scoresStore; @observable - BoardGameDetails? _boardGame; - - @observable - ObservableList playthroughsDetails = ObservableList.of([]); - - @computed - String get boardGameName => _boardGame!.name; - - @computed - String get boardGameId => _boardGame!.id; + ObservableList playthroughs = ObservableList.of([]); @computed - String? get boardGameImageUrl => _boardGame!.imageUrl; + List get finishedPlaythroughs => + playthroughs.where((p) => p.status == PlaythroughStatus.Finished).toList(); @computed - GameWinningCondition get gameWinningCondition => - _boardGame!.settings?.winningCondition ?? GameWinningCondition.HighestScore; + List get ongoingPlaythroughs => + playthroughs.where((p) => p.status == PlaythroughStatus.Started).toList(); - @action Future loadPlaythroughs() async { try { - final loadedPlaythroughDetails = []; - final playthroughs = await _playthroughService.retrievePlaythroughs([boardGameId]); - for (final playthrough in playthroughs) { - final playthroughDetails = await createPlaythroughDetails(playthrough); - loadedPlaythroughDetails.add(playthroughDetails); - } - - playthroughsDetails = loadedPlaythroughDetails.asObservable(); + playthroughs = ObservableList.of(await _playthroughService.retrievePlaythroughs()); } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); } } - @action - void setBoardGame(BoardGameDetails boardGame) => _boardGame = boardGame; - - Future createPlaythrough( + Future createPlaythrough( String boardGameId, List playthoughPlayers, Map playerScores, @@ -76,7 +49,7 @@ abstract class _PlaythroughsStore with Store { Duration? duration, { int? bggPlayId, }) async { - final newHivePlaythrough = await _playthroughService.createPlaythrough( + final newPlaythrough = await _playthroughService.createPlaythrough( boardGameId, playthoughPlayers, playerScores, @@ -85,7 +58,7 @@ abstract class _PlaythroughsStore with Store { bggPlayId: bggPlayId, ); - if (newHivePlaythrough == null) { + if (newPlaythrough == null) { FirebaseCrashlytics.instance.log( 'Faild to new playthrough for a board game $boardGameId with ${playthoughPlayers.length} players', ); @@ -93,60 +66,32 @@ abstract class _PlaythroughsStore with Store { return null; } - final playthrough = await createPlaythroughDetails(newHivePlaythrough); - playthroughsDetails.add(playthrough); - return playthrough; - } + await _scoresStore.refreshScores(newPlaythrough.id); - Future updatePlaythrough(PlaythroughDetails? playthroughDetails) async { - if (playthroughDetails?.id.isEmpty ?? true) { - return; - } + playthroughs.add(newPlaythrough); - try { - final updateSuceeded = - await _playthroughService.updatePlaythrough(playthroughDetails!.playthrough); - if (updateSuceeded) { - for (final PlayerScore playerScore in playthroughDetails.playerScores) { - await _scoreService.addOrUpdateScore(playerScore.score); - } - - await loadPlaythroughs(); - } - } catch (e, stack) { - FirebaseCrashlytics.instance.recordError(e, stack); - } + return newPlaythrough; } - Future deletePlaythrough(String playthroughId) async { - try { - final deleteSucceeded = await _playthroughService.deletePlaythrough(playthroughId); - if (deleteSucceeded) { - playthroughsDetails.removeWhere((p) => p.playthrough.id == playthroughId); - } - - return deleteSucceeded; - } catch (e, stack) { - FirebaseCrashlytics.instance.recordError(e, stack); + Future updatePlaythrough(Playthrough playthroughToUpdate) async { + final updateSuceeded = await _playthroughService.updatePlaythrough(playthroughToUpdate); + if (updateSuceeded) { + final playthroughIndex = + playthroughs.indexWhere((playthrough) => playthrough.id == playthroughToUpdate.id); + playthroughs[playthroughIndex] = playthroughToUpdate; } - return false; + return updateSuceeded; } - Future createPlaythroughDetails(Playthrough hivePlaythrough) async { - final scores = (await _scoreService.retrieveScores([hivePlaythrough.id])) - ..sortByScore(gameWinningCondition) - ..toList(); - final players = await _playerService.retrievePlayers( - playerIds: hivePlaythrough.playerIds, - includeDeleted: true, - ); - - final playerScores = scores.mapIndexed((int index, Score score) { - final player = players.firstWhereOrNull((Player p) => score.playerId == p.id); - return PlayerScore(player: player, score: score, place: index + 1); - }).toList(); + Future deletePlaythrough(String playthroughId) async { + final deleteSuceeded = await _playthroughService.deletePlaythrough(playthroughId); + if (deleteSuceeded) { + final playthroughIndex = + playthroughs.indexWhere((playthrough) => playthrough.id == playthroughId); + playthroughs.removeAt(playthroughIndex); + } - return PlaythroughDetails(playthrough: hivePlaythrough, playerScores: playerScores); + return deleteSuceeded; } } diff --git a/board_games_companion/lib/stores/playthroughs_store.g.dart b/board_games_companion/lib/stores/playthroughs_store.g.dart index 665cf76b..461f9525 100644 --- a/board_games_companion/lib/stores/playthroughs_store.g.dart +++ b/board_games_companion/lib/stores/playthroughs_store.g.dart @@ -9,98 +9,45 @@ part of 'playthroughs_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers mixin _$PlaythroughsStore on _PlaythroughsStore, Store { - Computed? _$boardGameNameComputed; + Computed>? _$finishedPlaythroughsComputed; @override - String get boardGameName => - (_$boardGameNameComputed ??= Computed(() => super.boardGameName, - name: '_PlaythroughsStore.boardGameName')) + List get finishedPlaythroughs => + (_$finishedPlaythroughsComputed ??= Computed>( + () => super.finishedPlaythroughs, + name: '_PlaythroughsStore.finishedPlaythroughs')) .value; - Computed? _$boardGameIdComputed; + Computed>? _$ongoingPlaythroughsComputed; @override - String get boardGameId => - (_$boardGameIdComputed ??= Computed(() => super.boardGameId, - name: '_PlaythroughsStore.boardGameId')) + List get ongoingPlaythroughs => + (_$ongoingPlaythroughsComputed ??= Computed>( + () => super.ongoingPlaythroughs, + name: '_PlaythroughsStore.ongoingPlaythroughs')) .value; - Computed? _$boardGameImageUrlComputed; - @override - String? get boardGameImageUrl => (_$boardGameImageUrlComputed ??= - Computed(() => super.boardGameImageUrl, - name: '_PlaythroughsStore.boardGameImageUrl')) - .value; - Computed? _$gameWinningConditionComputed; - - @override - GameWinningCondition get gameWinningCondition => - (_$gameWinningConditionComputed ??= Computed( - () => super.gameWinningCondition, - name: '_PlaythroughsStore.gameWinningCondition')) - .value; - - late final _$_boardGameAtom = - Atom(name: '_PlaythroughsStore._boardGame', context: context); + late final _$playthroughsAtom = + Atom(name: '_PlaythroughsStore.playthroughs', context: context); @override - BoardGameDetails? get _boardGame { - _$_boardGameAtom.reportRead(); - return super._boardGame; + ObservableList get playthroughs { + _$playthroughsAtom.reportRead(); + return super.playthroughs; } @override - set _boardGame(BoardGameDetails? value) { - _$_boardGameAtom.reportWrite(value, super._boardGame, () { - super._boardGame = value; + set playthroughs(ObservableList value) { + _$playthroughsAtom.reportWrite(value, super.playthroughs, () { + super.playthroughs = value; }); } - late final _$playthroughsDetailsAtom = - Atom(name: '_PlaythroughsStore.playthroughsDetails', context: context); - - @override - ObservableList get playthroughsDetails { - _$playthroughsDetailsAtom.reportRead(); - return super.playthroughsDetails; - } - - @override - set playthroughsDetails(ObservableList value) { - _$playthroughsDetailsAtom.reportWrite(value, super.playthroughsDetails, () { - super.playthroughsDetails = value; - }); - } - - late final _$loadPlaythroughsAsyncAction = - AsyncAction('_PlaythroughsStore.loadPlaythroughs', context: context); - - @override - Future loadPlaythroughs() { - return _$loadPlaythroughsAsyncAction.run(() => super.loadPlaythroughs()); - } - - late final _$_PlaythroughsStoreActionController = - ActionController(name: '_PlaythroughsStore', context: context); - - @override - void setBoardGame(BoardGameDetails boardGame) { - final _$actionInfo = _$_PlaythroughsStoreActionController.startAction( - name: '_PlaythroughsStore.setBoardGame'); - try { - return super.setBoardGame(boardGame); - } finally { - _$_PlaythroughsStoreActionController.endAction(_$actionInfo); - } - } - @override String toString() { return ''' -playthroughsDetails: ${playthroughsDetails}, -boardGameName: ${boardGameName}, -boardGameId: ${boardGameId}, -boardGameImageUrl: ${boardGameImageUrl}, -gameWinningCondition: ${gameWinningCondition} +playthroughs: ${playthroughs}, +finishedPlaythroughs: ${finishedPlaythroughs}, +ongoingPlaythroughs: ${ongoingPlaythroughs} '''; } } diff --git a/board_games_companion/lib/stores/scores_store.dart b/board_games_companion/lib/stores/scores_store.dart new file mode 100644 index 00000000..754eb46f --- /dev/null +++ b/board_games_companion/lib/stores/scores_store.dart @@ -0,0 +1,51 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:board_games_companion/services/score_service.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; + +import '../models/hive/score.dart'; + +part 'scores_store.g.dart'; + +@singleton +class ScoresStore = _ScoresStore with _$ScoresStore; + +abstract class _ScoresStore with Store { + _ScoresStore(this._scoreService); + + final ScoreService _scoreService; + + @observable + ObservableList scores = ObservableList.of([]); + + Future loadScores() async { + scores = (await _scoreService.retrieveScores()).asObservable(); + } + + Future refreshScores(String playthroughId) async { + final playthroughScores = await _scoreService.retrieveScoresForPlaythrough(playthroughId); + for (final score in playthroughScores) { + final scoreIndex = scores.indexWhere((s) => s.id == score.id); + if (scoreIndex == -1) { + scores.add(score); + } else { + scores[scoreIndex] = score; + } + } + } + + Future addOrUpdateScore(Score score) async { + final operationSucceeded = await _scoreService.addOrUpdateScore(score); + if (operationSucceeded) { + final scoreIndex = scores.indexWhere((element) => element.id == score.id); + if (scoreIndex == -1) { + scores.add(score); + } else { + scores[scoreIndex] = score; + } + } + + return operationSucceeded; + } +} diff --git a/board_games_companion/lib/stores/scores_store.g.dart b/board_games_companion/lib/stores/scores_store.g.dart new file mode 100644 index 00000000..86139e23 --- /dev/null +++ b/board_games_companion/lib/stores/scores_store.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scores_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$ScoresStore on _ScoresStore, Store { + late final _$scoresAtom = Atom(name: '_ScoresStore.scores', context: context); + + @override + ObservableList get scores { + _$scoresAtom.reportRead(); + return super.scores; + } + + @override + set scores(ObservableList value) { + _$scoresAtom.reportWrite(value, super.scores, () { + super.scores = value; + }); + } + + @override + String toString() { + return ''' +scores: ${scores} + '''; + } +} diff --git a/board_games_companion/pubspec.lock b/board_games_companion/pubspec.lock index b9bc2640..52b344e8 100644 --- a/board_games_companion/pubspec.lock +++ b/board_games_companion/pubspec.lock @@ -959,6 +959,13 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" source_gen: dependency: transitive description: diff --git a/board_games_companion/pubspec.yaml b/board_games_companion/pubspec.yaml index cf2b1685..46ed4d0b 100644 --- a/board_games_companion/pubspec.yaml +++ b/board_games_companion/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: package_info: ^2.0.2 retry: ^3.1.0 share_plus: ^4.0.10+1 + sliver_tools: 0.2.8 sprintf: ^6.0.0 tuple: ^2.0.0 url_launcher: ^6.1.6