diff --git a/board_games_companion/assets/mockups/Board Games Companion.mockup b/board_games_companion/assets/mockups/Board Games Companion.mockup index 24836f13..ebc49155 100644 Binary files a/board_games_companion/assets/mockups/Board Games Companion.mockup and b/board_games_companion/assets/mockups/Board Games Companion.mockup differ diff --git a/board_games_companion/lib/app.dart b/board_games_companion/lib/app.dart index 995b9844..b76190d8 100644 --- a/board_games_companion/lib/app.dart +++ b/board_games_companion/lib/app.dart @@ -1,5 +1,6 @@ import 'package:animations/animations.dart'; import 'package:basics/basics.dart'; +import 'package:board_games_companion/models/hive/playthrough_note.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:flutter/material.dart'; @@ -151,10 +152,9 @@ class BoardGamesCompanionAppState extends State { case PlaythroughNotePage.pageRoute: final arguments = routeSettings.arguments as PlaythroughNotePageArguments; final viewModel = getIt(); - viewModel.setPlaythrough(arguments.playthrough); - viewModel.setNoteId(arguments.noteId); + viewModel.setNote(arguments.note); - return MaterialPageRoute( + return MaterialPageRoute( settings: routeSettings, builder: (BuildContext context) => PlaythroughNotePage(viewModel: viewModel), ); diff --git a/board_games_companion/lib/common/app_text.dart b/board_games_companion/lib/common/app_text.dart index 907407ed..13a473b2 100644 --- a/board_games_companion/lib/common/app_text.dart +++ b/board_games_companion/lib/common/app_text.dart @@ -131,6 +131,7 @@ class AppText { static const playthroughsLogDateSectionTitle = 'Date'; static const playthroughsLogDurationSectionTitle = 'Duration'; static const playthroughsLogPlayersSectionTitle = 'Players'; + static const playthroughsLogNotesSectionTitle = 'Notes'; static const playthroughsLogCooperativeResultSectionTitle = 'Result'; static const playthroughsLogPlayersPlayingNowButtonText = 'Select players that are playing'; static const playthroughsLogPlayersPlayedInThePastButtonText = 'Select players that played'; @@ -187,6 +188,7 @@ class AppText { static const editPlaythroughNoScoreResultLossText = 'Loss'; static const editPlaythroughNoScorePlayersHeaderTitle = 'Players'; static const editPlaythroughNotesHeaderTitle = 'Notes'; + static const editPlaythroughNoNotesText = 'Add notes about your game'; static const editPlaythroughPagePlayedOnSectionTitle = 'Played on'; static const editPlaythroughPageDurationSectionTitle = 'Duration'; static const editPlaythroughPageScoreSectionTitle = 'Score'; diff --git a/board_games_companion/lib/extensions/date_time_extensions.dart b/board_games_companion/lib/extensions/date_time_extensions.dart index 8f0e0cc1..c0b3887b 100644 --- a/board_games_companion/lib/extensions/date_time_extensions.dart +++ b/board_games_companion/lib/extensions/date_time_extensions.dart @@ -1,9 +1,12 @@ +import 'package:board_games_companion/extensions/string_extensions.dart'; import 'package:intl/intl.dart'; import 'package:sprintf/sprintf.dart'; import '../common/app_text.dart'; import '../common/constants.dart'; +final DateFormat historicalPlaythroughDateFormat = DateFormat('d MMMM y'); + extension DateTimeExtensions on DateTime? { String toShortMonth([String? fallbackValue]) { if (this == null) { @@ -39,6 +42,26 @@ extension DateTimeExtensions on DateTime? { return sprintf(AppText.daysAgoFormat, [daysAgo]); } + String toHistoricalPlaythroughHeaderFormat() { + if (this == null) { + return ''; + } + + if (isToday) { + return AppText.today.toCapitalized(); + } + + if (isYesterday) { + return AppText.yesteday.toCapitalized(); + } + + if (isDayBeforeYesterday) { + return AppText.dayBeforeYesteday.toCapitalized(); + } + + return historicalPlaythroughDateFormat.format(this!); + } + int get daysAgo { final nowUtc = DateTime.now().toUtc(); return nowUtc.difference(this!).inDays; diff --git a/board_games_companion/lib/injectable.config.dart b/board_games_companion/lib/injectable.config.dart index 2b08ea9e..e0640929 100644 --- a/board_games_companion/lib/injectable.config.dart +++ b/board_games_companion/lib/injectable.config.dart @@ -8,28 +8,28 @@ import 'package:board_games_companion/pages/board_game_details/board_game_details_view_model.dart' as _i45; import 'package:board_games_companion/pages/collections/collection_search_result_view_model.dart' - as _i31; -import 'package:board_games_companion/pages/collections/collections_view_model.dart' as _i32; -import 'package:board_games_companion/pages/create_board_game/create_board_game_view_model.dart' +import 'package:board_games_companion/pages/collections/collections_view_model.dart' as _i33; +import 'package:board_games_companion/pages/create_board_game/create_board_game_view_model.dart' + as _i34; import 'package:board_games_companion/pages/edit_playthrough/edit_playthrough_view_model.dart' as _i46; import 'package:board_games_companion/pages/edit_playthrough/playthrough_note_view_model.dart' - as _i38; + as _i12; import 'package:board_games_companion/pages/home/home_view_model.dart' as _i47; import 'package:board_games_companion/pages/hot_board_games/hot_board_games_view_model.dart' - as _i35; + as _i36; import 'package:board_games_companion/pages/player/player_view_model.dart' - as _i26; + as _i27; import 'package:board_games_companion/pages/players/players_view_model.dart' as _i11; import 'package:board_games_companion/pages/plays/plays_view_model.dart' - as _i36; -import 'package:board_games_companion/pages/playthroughs/playthrough_migration_view_model.dart' as _i37; +import 'package:board_games_companion/pages/playthroughs/playthrough_migration_view_model.dart' + as _i38; import 'package:board_games_companion/pages/playthroughs/playthrough_players_selection_view_model.dart' - as _i12; + as _i13; import 'package:board_games_companion/pages/playthroughs/playthrough_statistics_view_model.dart' as _i39; import 'package:board_games_companion/pages/playthroughs/playthroughs_game_settings_view_model.dart' @@ -42,40 +42,40 @@ import 'package:board_games_companion/pages/playthroughs/playthroughs_view_model as _i43; import 'package:board_games_companion/pages/settings/settings_view_model.dart' as _i44; -import 'package:board_games_companion/services/analytics_service.dart' as _i21; +import 'package:board_games_companion/services/analytics_service.dart' as _i22; import 'package:board_games_companion/services/board_games_filters_service.dart' as _i4; import 'package:board_games_companion/services/board_games_geek_service.dart' - as _i23; -import 'package:board_games_companion/services/board_games_search_service.dart' as _i24; -import 'package:board_games_companion/services/board_games_service.dart' +import 'package:board_games_companion/services/board_games_search_service.dart' as _i25; +import 'package:board_games_companion/services/board_games_service.dart' + as _i26; import 'package:board_games_companion/services/environment_service.dart' as _i6; import 'package:board_games_companion/services/file_service.dart' as _i7; import 'package:board_games_companion/services/player_service.dart' as _i9; import 'package:board_games_companion/services/playthroughs_service.dart' - as _i27; + as _i28; import 'package:board_games_companion/services/preferences_service.dart' - as _i13; -import 'package:board_games_companion/services/rate_and_review_service.dart' as _i14; -import 'package:board_games_companion/services/score_service.dart' as _i15; -import 'package:board_games_companion/services/search_service.dart' as _i17; -import 'package:board_games_companion/services/user_service.dart' as _i19; +import 'package:board_games_companion/services/rate_and_review_service.dart' + as _i15; +import 'package:board_games_companion/services/score_service.dart' as _i16; +import 'package:board_games_companion/services/search_service.dart' as _i18; +import 'package:board_games_companion/services/user_service.dart' as _i20; import 'package:board_games_companion/stores/app_store.dart' as _i3; import 'package:board_games_companion/stores/board_games_filters_store.dart' - as _i22; -import 'package:board_games_companion/stores/board_games_store.dart' as _i30; + as _i23; +import 'package:board_games_companion/stores/board_games_store.dart' as _i31; import 'package:board_games_companion/stores/game_playthroughs_details_store.dart' - as _i34; + as _i35; import 'package:board_games_companion/stores/players_store.dart' as _i10; -import 'package:board_games_companion/stores/playthroughs_store.dart' as _i28; -import 'package:board_games_companion/stores/scores_store.dart' as _i16; -import 'package:board_games_companion/stores/search_store.dart' as _i18; -import 'package:board_games_companion/stores/user_store.dart' as _i20; +import 'package:board_games_companion/stores/playthroughs_store.dart' as _i29; +import 'package:board_games_companion/stores/scores_store.dart' as _i17; +import 'package:board_games_companion/stores/search_store.dart' as _i19; +import 'package:board_games_companion/stores/user_store.dart' as _i21; import 'package:board_games_companion/utilities/analytics_route_observer.dart' - as _i29; + as _i30; import 'package:board_games_companion/utilities/custom_http_client_adapter.dart' as _i5; import 'package:firebase_analytics/firebase_analytics.dart' as _i8; @@ -110,142 +110,142 @@ _i1.GetIt $initGetIt( gh.singleton<_i10.PlayersStore>(_i10.PlayersStore(gh<_i9.PlayerService>())); gh.factory<_i11.PlayersViewModel>( () => _i11.PlayersViewModel(gh<_i10.PlayersStore>())); - gh.factory<_i12.PlaythroughPlayersSelectionViewModel>( - () => _i12.PlaythroughPlayersSelectionViewModel(gh<_i10.PlayersStore>())); - gh.singleton<_i13.PreferencesService>(_i13.PreferencesService()); - gh.singleton<_i14.RateAndReviewService>( - _i14.RateAndReviewService(gh<_i13.PreferencesService>())); - gh.singleton<_i15.ScoreService>(_i15.ScoreService()); - gh.singleton<_i16.ScoresStore>(_i16.ScoresStore(gh<_i15.ScoreService>())); - gh.singleton<_i17.SearchService>(_i17.SearchService()); - gh.singleton<_i18.SearchStore>(_i18.SearchStore(gh<_i17.SearchService>())); - gh.singleton<_i19.UserService>(_i19.UserService()); - gh.singleton<_i20.UserStore>(_i20.UserStore(gh<_i19.UserService>())); - gh.singleton<_i21.AnalyticsService>(_i21.AnalyticsService( + gh.factory<_i12.PlaythroughNoteViewModel>( + () => _i12.PlaythroughNoteViewModel()); + gh.factory<_i13.PlaythroughPlayersSelectionViewModel>( + () => _i13.PlaythroughPlayersSelectionViewModel(gh<_i10.PlayersStore>())); + gh.singleton<_i14.PreferencesService>(_i14.PreferencesService()); + gh.singleton<_i15.RateAndReviewService>( + _i15.RateAndReviewService(gh<_i14.PreferencesService>())); + gh.singleton<_i16.ScoreService>(_i16.ScoreService()); + gh.singleton<_i17.ScoresStore>(_i17.ScoresStore(gh<_i16.ScoreService>())); + gh.singleton<_i18.SearchService>(_i18.SearchService()); + gh.singleton<_i19.SearchStore>(_i19.SearchStore(gh<_i18.SearchService>())); + gh.singleton<_i20.UserService>(_i20.UserService()); + gh.singleton<_i21.UserStore>(_i21.UserStore(gh<_i20.UserService>())); + gh.singleton<_i22.AnalyticsService>(_i22.AnalyticsService( gh<_i8.FirebaseAnalytics>(), - gh<_i14.RateAndReviewService>(), + gh<_i15.RateAndReviewService>(), )); - gh.singleton<_i22.BoardGamesFiltersStore>(_i22.BoardGamesFiltersStore( + gh.singleton<_i23.BoardGamesFiltersStore>(_i23.BoardGamesFiltersStore( gh<_i4.BoardGamesFiltersService>(), - gh<_i21.AnalyticsService>(), + gh<_i22.AnalyticsService>(), )); - gh.singleton<_i23.BoardGamesGeekService>( - _i23.BoardGamesGeekService(gh<_i5.CustomHttpClientAdapter>())); - gh.singleton<_i24.BoardGamesSearchService>( - _i24.BoardGamesSearchService(gh<_i6.EnvironmentService>())); - gh.singleton<_i25.BoardGamesService>( - _i25.BoardGamesService(gh<_i23.BoardGamesGeekService>())); - gh.factory<_i26.PlayerViewModel>( - () => _i26.PlayerViewModel(gh<_i10.PlayersStore>())); - gh.singleton<_i27.PlaythroughService>( - _i27.PlaythroughService(gh<_i15.ScoreService>())); - gh.singleton<_i28.PlaythroughsStore>(_i28.PlaythroughsStore( - gh<_i27.PlaythroughService>(), - gh<_i16.ScoresStore>(), + gh.singleton<_i24.BoardGamesGeekService>( + _i24.BoardGamesGeekService(gh<_i5.CustomHttpClientAdapter>())); + gh.singleton<_i25.BoardGamesSearchService>( + _i25.BoardGamesSearchService(gh<_i6.EnvironmentService>())); + gh.singleton<_i26.BoardGamesService>( + _i26.BoardGamesService(gh<_i24.BoardGamesGeekService>())); + gh.factory<_i27.PlayerViewModel>( + () => _i27.PlayerViewModel(gh<_i10.PlayersStore>())); + gh.singleton<_i28.PlaythroughService>( + _i28.PlaythroughService(gh<_i16.ScoreService>())); + gh.singleton<_i29.PlaythroughsStore>(_i29.PlaythroughsStore( + gh<_i28.PlaythroughService>(), + gh<_i17.ScoresStore>(), )); - gh.factory<_i29.AnalyticsRouteObserver>( - () => _i29.AnalyticsRouteObserver(gh<_i21.AnalyticsService>())); - gh.singleton<_i30.BoardGamesStore>(_i30.BoardGamesStore( - gh<_i25.BoardGamesService>(), - gh<_i27.PlaythroughService>(), + gh.factory<_i30.AnalyticsRouteObserver>( + () => _i30.AnalyticsRouteObserver(gh<_i22.AnalyticsService>())); + gh.singleton<_i31.BoardGamesStore>(_i31.BoardGamesStore( + gh<_i26.BoardGamesService>(), + gh<_i28.PlaythroughService>(), )); - gh.factory<_i31.CollectionSearchResultViewModel>( - () => _i31.CollectionSearchResultViewModel(gh<_i30.BoardGamesStore>())); - gh.factory<_i32.CollectionsViewModel>(() => _i32.CollectionsViewModel( - gh<_i20.UserStore>(), - gh<_i30.BoardGamesStore>(), - gh<_i22.BoardGamesFiltersStore>(), - gh<_i16.ScoresStore>(), - gh<_i28.PlaythroughsStore>(), + gh.factory<_i32.CollectionSearchResultViewModel>( + () => _i32.CollectionSearchResultViewModel(gh<_i31.BoardGamesStore>())); + gh.factory<_i33.CollectionsViewModel>(() => _i33.CollectionsViewModel( + gh<_i21.UserStore>(), + gh<_i31.BoardGamesStore>(), + gh<_i23.BoardGamesFiltersStore>(), + gh<_i17.ScoresStore>(), + gh<_i29.PlaythroughsStore>(), gh<_i10.PlayersStore>(), )); - gh.factory<_i33.CreateBoardGameViewModel>(() => _i33.CreateBoardGameViewModel( - gh<_i30.BoardGamesStore>(), + gh.factory<_i34.CreateBoardGameViewModel>(() => _i34.CreateBoardGameViewModel( + gh<_i31.BoardGamesStore>(), gh<_i7.FileService>(), )); - gh.singleton<_i34.GamePlaythroughsDetailsStore>( - _i34.GamePlaythroughsDetailsStore( - gh<_i28.PlaythroughsStore>(), - gh<_i16.ScoresStore>(), + gh.singleton<_i35.GamePlaythroughsDetailsStore>( + _i35.GamePlaythroughsDetailsStore( + gh<_i29.PlaythroughsStore>(), + gh<_i17.ScoresStore>(), gh<_i10.PlayersStore>(), - gh<_i30.BoardGamesStore>(), + gh<_i31.BoardGamesStore>(), )); - gh.singleton<_i35.HotBoardGamesViewModel>(_i35.HotBoardGamesViewModel( - gh<_i30.BoardGamesStore>(), - gh<_i23.BoardGamesGeekService>(), - gh<_i21.AnalyticsService>(), + gh.singleton<_i36.HotBoardGamesViewModel>(_i36.HotBoardGamesViewModel( + gh<_i31.BoardGamesStore>(), + gh<_i24.BoardGamesGeekService>(), + gh<_i22.AnalyticsService>(), )); - gh.factory<_i36.PlaysViewModel>(() => _i36.PlaysViewModel( - gh<_i28.PlaythroughsStore>(), - gh<_i30.BoardGamesStore>(), + gh.factory<_i37.PlaysViewModel>(() => _i37.PlaysViewModel( + gh<_i29.PlaythroughsStore>(), + gh<_i31.BoardGamesStore>(), gh<_i10.PlayersStore>(), - gh<_i16.ScoresStore>(), - gh<_i21.AnalyticsService>(), + gh<_i17.ScoresStore>(), + gh<_i22.AnalyticsService>(), )); - gh.factory<_i37.PlaythroughMigrationViewModel>(() => - _i37.PlaythroughMigrationViewModel( - gh<_i34.GamePlaythroughsDetailsStore>())); - gh.factory<_i38.PlaythroughNoteViewModel>(() => - _i38.PlaythroughNoteViewModel(gh<_i34.GamePlaythroughsDetailsStore>())); + gh.factory<_i38.PlaythroughMigrationViewModel>(() => + _i38.PlaythroughMigrationViewModel( + gh<_i35.GamePlaythroughsDetailsStore>())); gh.singleton<_i39.PlaythroughStatisticsViewModel>( _i39.PlaythroughStatisticsViewModel( gh<_i9.PlayerService>(), - gh<_i16.ScoresStore>(), - gh<_i34.GamePlaythroughsDetailsStore>(), + gh<_i17.ScoresStore>(), + gh<_i35.GamePlaythroughsDetailsStore>(), )); gh.factory<_i40.PlaythroughsGameSettingsViewModel>( () => _i40.PlaythroughsGameSettingsViewModel( - gh<_i30.BoardGamesStore>(), - gh<_i34.GamePlaythroughsDetailsStore>(), + gh<_i31.BoardGamesStore>(), + gh<_i35.GamePlaythroughsDetailsStore>(), )); gh.factory<_i41.PlaythroughsHistoryViewModel>(() => _i41.PlaythroughsHistoryViewModel( - gh<_i34.GamePlaythroughsDetailsStore>())); + gh<_i35.GamePlaythroughsDetailsStore>())); gh.factory<_i42.PlaythroughsLogGameViewModel>( () => _i42.PlaythroughsLogGameViewModel( gh<_i10.PlayersStore>(), - gh<_i34.GamePlaythroughsDetailsStore>(), - gh<_i21.AnalyticsService>(), + gh<_i35.GamePlaythroughsDetailsStore>(), + gh<_i22.AnalyticsService>(), )); gh.factory<_i43.PlaythroughsViewModel>(() => _i43.PlaythroughsViewModel( - gh<_i34.GamePlaythroughsDetailsStore>(), + gh<_i35.GamePlaythroughsDetailsStore>(), gh<_i10.PlayersStore>(), - gh<_i21.AnalyticsService>(), - gh<_i25.BoardGamesService>(), - gh<_i20.UserStore>(), + gh<_i22.AnalyticsService>(), + gh<_i26.BoardGamesService>(), + gh<_i21.UserStore>(), )); gh.singleton<_i44.SettingsViewModel>(_i44.SettingsViewModel( gh<_i7.FileService>(), - gh<_i25.BoardGamesService>(), + gh<_i26.BoardGamesService>(), gh<_i4.BoardGamesFiltersService>(), gh<_i9.PlayerService>(), - gh<_i19.UserService>(), - gh<_i27.PlaythroughService>(), - gh<_i15.ScoreService>(), - gh<_i13.PreferencesService>(), + gh<_i20.UserService>(), + gh<_i28.PlaythroughService>(), + gh<_i16.ScoreService>(), + gh<_i14.PreferencesService>(), gh<_i3.AppStore>(), - gh<_i20.UserStore>(), - gh<_i30.BoardGamesStore>(), + gh<_i21.UserStore>(), + gh<_i31.BoardGamesStore>(), )); gh.factory<_i45.BoardGameDetailsViewModel>( () => _i45.BoardGameDetailsViewModel( - gh<_i30.BoardGamesStore>(), - gh<_i21.AnalyticsService>(), + gh<_i31.BoardGamesStore>(), + gh<_i22.AnalyticsService>(), )); gh.factory<_i46.EditPlaythoughViewModel>(() => - _i46.EditPlaythoughViewModel(gh<_i34.GamePlaythroughsDetailsStore>())); + _i46.EditPlaythoughViewModel(gh<_i35.GamePlaythroughsDetailsStore>())); gh.factory<_i47.HomeViewModel>(() => _i47.HomeViewModel( - gh<_i21.AnalyticsService>(), - gh<_i14.RateAndReviewService>(), + gh<_i22.AnalyticsService>(), + gh<_i15.RateAndReviewService>(), gh<_i11.PlayersViewModel>(), - gh<_i22.BoardGamesFiltersStore>(), - gh<_i32.CollectionsViewModel>(), - gh<_i35.HotBoardGamesViewModel>(), - gh<_i36.PlaysViewModel>(), + gh<_i23.BoardGamesFiltersStore>(), + gh<_i33.CollectionsViewModel>(), + gh<_i36.HotBoardGamesViewModel>(), + gh<_i37.PlaysViewModel>(), gh<_i3.AppStore>(), - gh<_i18.SearchStore>(), - gh<_i30.BoardGamesStore>(), - gh<_i24.BoardGamesSearchService>(), + gh<_i19.SearchStore>(), + gh<_i31.BoardGamesStore>(), + gh<_i25.BoardGamesSearchService>(), )); return getIt; } diff --git a/board_games_companion/lib/models/navigation/playthough_note_page_arguments.dart b/board_games_companion/lib/models/navigation/playthough_note_page_arguments.dart index 196dd4c9..1240f6fb 100644 --- a/board_games_companion/lib/models/navigation/playthough_note_page_arguments.dart +++ b/board_games_companion/lib/models/navigation/playthough_note_page_arguments.dart @@ -1,8 +1,7 @@ -import '../hive/playthrough.dart'; +import 'package:board_games_companion/models/hive/playthrough_note.dart'; class PlaythroughNotePageArguments { - const PlaythroughNotePageArguments(this.playthrough, {this.noteId}); + const PlaythroughNotePageArguments({this.note}); - final String? noteId; - final Playthrough playthrough; + final PlaythroughNote? note; } 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 0b5fc338..32a19293 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 @@ -260,18 +260,18 @@ We couldn't retrieve any board games. Check your Internet connectivity and try a ), const SizedBox(height: _halfSpacingBetweenSecions), if (!viewModel.isCreatedByUser) ...[ - SectionHeader.title(primaryTitle: AppText.boardGameDetailsPaboutLinksTitle), + SectionHeader.title(title: AppText.boardGameDetailsPaboutLinksTitle), _Links(boardGameDetailsStore: viewModel), const SizedBox(height: _halfSpacingBetweenSecions), - SectionHeader.title(primaryTitle: AppText.boardGameDetailsPaboutCreditsTitle), + SectionHeader.title(title: AppText.boardGameDetailsPaboutCreditsTitle), const SizedBox(height: _sectionTopSpacing), _Credits(boardGameDetails: viewModel.boardGame), const SizedBox(height: _halfSpacingBetweenSecions), SectionHeader.title( - primaryTitle: AppText.boardGameDetailsPaboutCategoriesTitle, + title: AppText.boardGameDetailsPaboutCategoriesTitle, ), _Categories(categories: viewModel.boardGame.categories!), - if (viewModel.isMainGame && viewModel.hasExpansions) + if (viewModel.isMainGame && viewModel.hasExpansions) ...[ Observer( builder: (_) { return BoardGameDetailsExpansions( @@ -283,9 +283,10 @@ We couldn't retrieve any board games. Check your Internet connectivity and try a ); }, ), - const SizedBox(height: _halfSpacingBetweenSecions), + const SizedBox(height: _halfSpacingBetweenSecions), + ], SectionHeader.title( - primaryTitle: AppText.boardGameDetailsPaboutDescriptionTitle, + title: AppText.boardGameDetailsPaboutDescriptionTitle, ), const SizedBox(height: _sectionTopSpacing), Padding( diff --git a/board_games_companion/lib/pages/collections/collections_filter_panel.dart b/board_games_companion/lib/pages/collections/collections_filter_panel.dart index 83d33051..ce588ca2 100644 --- a/board_games_companion/lib/pages/collections/collections_filter_panel.dart +++ b/board_games_companion/lib/pages/collections/collections_filter_panel.dart @@ -1,12 +1,13 @@ import 'dart:math'; +import 'package:board_games_companion/widgets/common/page_container.dart'; +import 'package:board_games_companion/widgets/common/section_header.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import '../../common/app_colors.dart'; import '../../common/app_styles.dart'; import '../../common/app_text.dart'; -import '../../common/app_theme.dart'; import '../../common/dimensions.dart'; import '../../common/enums/order_by.dart'; import '../../models/sort_by.dart'; @@ -33,39 +34,73 @@ class CollectionsFilterPanelState extends State { Widget build(BuildContext context) { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Dimensions.standardSpacing, - vertical: Dimensions.doubleStandardSpacing, + padding: const EdgeInsets.only( + bottom: Dimensions.doubleStandardSpacing, ), - child: Column( - children: [ - _SortBy(gamesViewModel: widget.viewModel), - _Filters(gamesViewModel: widget.viewModel), - const SizedBox(height: Dimensions.standardSpacing), - Align( - alignment: Alignment.bottomRight, - child: Observer( + child: PageContainer( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(AppStyles.defaultBottomSheetCornerRadius), + topRight: Radius.circular(AppStyles.defaultBottomSheetCornerRadius), + ), + child: Column( + children: [ + const SizedBox(height: Dimensions.oneAndHalfStandardSpacing), + const _BottomSheetHandle(), + const SizedBox(height: Dimensions.oneAndHalfStandardSpacing), + _SortBySection(gamesViewModel: widget.viewModel), + const SizedBox(height: Dimensions.standardSpacing), + _FiltersSection(gamesViewModel: widget.viewModel), + const SizedBox(height: Dimensions.standardSpacing), + Observer( builder: (_) { - return ElevatedIconButton( - icon: const Icon(Icons.clear), - title: AppText.filterGamesPanelClearFiltersButtonText, - color: AppColors.accentColor, - onPressed: widget.viewModel.anyFiltersApplied - ? () => widget.viewModel.clearFilters() - : null, + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(right: Dimensions.standardSpacing), + child: ElevatedIconButton( + icon: const Icon(Icons.clear), + title: AppText.filterGamesPanelClearFiltersButtonText, + color: AppColors.accentColor, + onPressed: widget.viewModel.anyFiltersApplied + ? () => widget.viewModel.clearFilters() + : null, + ), + ), + ], ); }, ), - ), - ], + ], + ), ), ), ); } } -class _SortBy extends StatelessWidget { - const _SortBy({ +class _BottomSheetHandle extends StatelessWidget { + const _BottomSheetHandle(); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + color: AppColors.accentColor, + borderRadius: BorderRadius.circular(AppStyles.defaultCornerRadius), + ), + child: const SizedBox( + height: Dimensions.halfStandardSpacing, + width: Dimensions.trippleStandardSpacing, + ), + ), + ); + } +} + +class _SortBySection extends StatelessWidget { + const _SortBySection({ Key? key, required CollectionsViewModel gamesViewModel, }) : _gamesViewModel = gamesViewModel, @@ -78,7 +113,11 @@ class _SortBy extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Sort by', style: AppTheme.titleTextStyle), + SectionHeader.titleWithIcon( + title: 'Sort by', + icon: const Icon(Icons.sort), + ), + const SizedBox(height: Dimensions.standardSpacing), Wrap( spacing: Dimensions.standardSpacing, children: [ @@ -90,7 +129,6 @@ class _SortBy extends StatelessWidget { ) ], ), - const SizedBox(height: Dimensions.doubleStandardSpacing), ], ); } @@ -145,8 +183,8 @@ class _SortByChip extends StatelessWidget { } } -class _Filters extends StatelessWidget { - const _Filters({ +class _FiltersSection extends StatelessWidget { + const _FiltersSection({ required this.gamesViewModel, Key? key, }) : super(key: key); @@ -157,63 +195,69 @@ class _Filters extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - const Text('Filter by', style: AppTheme.titleTextStyle), - const SizedBox(height: Dimensions.standardSpacing), - const Align( - alignment: Alignment.centerLeft, - child: Text('Rating', style: AppTheme.sectionHeaderTextStyle), + SectionHeader.titleWithIcon( + title: 'Rating', + icon: const Icon(Icons.filter_alt_outlined), ), - const SizedBox(height: Dimensions.standardSpacing), - BgcSegmentedButtonsContainer( - height: Dimensions.collectionFilterHexagonSize + Dimensions.doubleStandardSpacing, - backgroundColor: AppColors.primaryColor.withAlpha(AppStyles.opacity80Percent), - child: Observer( - builder: (_) { - return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _FilterRatingValue.any( - isSelected: gamesViewModel.filterByRating == null, - onRatingSelected: (double? rating) => updateFilterRating(rating), - ), - _FilterRatingValue.rating( - rating: 6.5, - onRatingSelected: (double? rating) => updateFilterRating(rating), - isSelected: gamesViewModel.filterByRating == 6.5, - ), - _FilterRatingValue.rating( - rating: 7.5, - onRatingSelected: (double? rating) => updateFilterRating(rating), - isSelected: gamesViewModel.filterByRating == 7.5, - ), - _FilterRatingValue.rating( - rating: 8.0, - onRatingSelected: (double? rating) => updateFilterRating(rating), - isSelected: gamesViewModel.filterByRating == 8.0, - ), - _FilterRatingValue.rating( - rating: 8.5, - onRatingSelected: (double? rating) => updateFilterRating(rating), - isSelected: gamesViewModel.filterByRating == 8.5, - ), - ], - ); - }, + const SizedBox(height: Dimensions.doubleStandardSpacing), + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: BgcSegmentedButtonsContainer( + height: Dimensions.collectionFilterHexagonSize + Dimensions.doubleStandardSpacing, + backgroundColor: AppColors.primaryColor.withAlpha(AppStyles.opacity80Percent), + child: Observer( + builder: (_) { + return Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _FilterRatingValue.any( + isSelected: gamesViewModel.filterByRating == null, + onRatingSelected: (double? rating) => updateFilterRating(rating), + ), + _FilterRatingValue.rating( + rating: 6.5, + onRatingSelected: (double? rating) => updateFilterRating(rating), + isSelected: gamesViewModel.filterByRating == 6.5, + ), + _FilterRatingValue.rating( + rating: 7.5, + onRatingSelected: (double? rating) => updateFilterRating(rating), + isSelected: gamesViewModel.filterByRating == 7.5, + ), + _FilterRatingValue.rating( + rating: 8.0, + onRatingSelected: (double? rating) => updateFilterRating(rating), + isSelected: gamesViewModel.filterByRating == 8.0, + ), + _FilterRatingValue.rating( + rating: 8.5, + onRatingSelected: (double? rating) => updateFilterRating(rating), + isSelected: gamesViewModel.filterByRating == 8.5, + ), + ], + ); + }, + ), ), ), - const SizedBox(height: Dimensions.doubleStandardSpacing * 2), - const Align( - alignment: Alignment.centerLeft, - child: Text('Number of players', style: AppTheme.sectionHeaderTextStyle), + const SizedBox(height: Dimensions.doubleStandardSpacing), + SectionHeader.titleWithIcon( + title: 'Number of players', + icon: const Icon(Icons.filter_alt_outlined), ), - Observer(builder: (_) { - if (gamesViewModel.anyBoardGames) { - return _FilterNumberOfPlayersSlider(gamesViewModel: gamesViewModel); - } + Observer( + builder: (_) { + if (gamesViewModel.anyBoardGames) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: _FilterNumberOfPlayersSlider(gamesViewModel: gamesViewModel), + ); + } - return const SizedBox.shrink(); - }), + return const SizedBox.shrink(); + }, + ), ], ); } diff --git a/board_games_companion/lib/pages/collections/collections_page.dart b/board_games_companion/lib/pages/collections/collections_page.dart index 548dc8c9..8965b606 100644 --- a/board_games_companion/lib/pages/collections/collections_page.dart +++ b/board_games_companion/lib/pages/collections/collections_page.dart @@ -223,6 +223,8 @@ class _AppBar extends StatefulWidget { } class _AppBarState extends State<_AppBar> { + static const double _requiredFiltersBottomSheetHeight = 500; + @override Widget build(BuildContext context) => SliverAppBar( pinned: true, @@ -286,6 +288,7 @@ class _AppBarState extends State<_AppBar> { await showModalBottomSheet( backgroundColor: AppColors.primaryColor, elevation: Dimensions.defaultElevation, + isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(AppStyles.defaultBottomSheetCornerRadius), @@ -293,8 +296,12 @@ class _AppBarState extends State<_AppBar> { ), ), context: context, - builder: (_) { - return CollectionsFilterPanel(viewModel: widget.viewModel); + builder: (context) { + final pageHeight = MediaQuery.of(context).size.height; + return FractionallySizedBox( + heightFactor: _requiredFiltersBottomSheetHeight / pageHeight, + child: CollectionsFilterPanel(viewModel: widget.viewModel), + ); }, ); } diff --git a/board_games_companion/lib/pages/create_board_game/create_board_game_page.dart b/board_games_companion/lib/pages/create_board_game/create_board_game_page.dart index 421239ab..24cc83fa 100644 --- a/board_games_companion/lib/pages/create_board_game/create_board_game_page.dart +++ b/board_games_companion/lib/pages/create_board_game/create_board_game_page.dart @@ -245,7 +245,7 @@ class _Form extends StatelessWidget { return SliverList( delegate: SliverChildListDelegate.fixed( [ - SectionHeader.title(primaryTitle: AppText.createNewGameBoardGameImage), + SectionHeader.title(title: AppText.createNewGameBoardGameImage), Observer( builder: (_) { return _ImageSection( @@ -266,7 +266,7 @@ class _Form extends StatelessWidget { ); }, ), - SectionHeader.title(primaryTitle: AppText.createNewGameBoardGameRating), + SectionHeader.title(title: AppText.createNewGameBoardGameRating), Observer( builder: (_) { return _RatingSection( @@ -275,7 +275,7 @@ class _Form extends StatelessWidget { ); }, ), - SectionHeader.title(primaryTitle: AppText.createNewGameBoardGamePlayers), + SectionHeader.title(title: AppText.createNewGameBoardGamePlayers), Observer( builder: (_) { return _PlayersSection( @@ -286,7 +286,7 @@ class _Form extends StatelessWidget { ); }, ), - SectionHeader.title(primaryTitle: AppText.createNewGameBoardGamePlaytime), + SectionHeader.title(title: AppText.createNewGameBoardGamePlaytime), Observer( builder: (_) { return _PlaytimeSection( @@ -297,7 +297,7 @@ class _Form extends StatelessWidget { ); }, ), - SectionHeader.title(primaryTitle: AppText.createNewGameBoardGameMinAge), + SectionHeader.title(title: AppText.createNewGameBoardGameMinAge), Observer( builder: (_) { return _AgeSection( diff --git a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_page.dart b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_page.dart index af17535b..dda5c187 100644 --- a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_page.dart +++ b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_page.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:numberpicker/numberpicker.dart'; import 'package:sliver_tools/sliver_tools.dart'; @@ -27,6 +26,7 @@ import '../../widgets/common/slivers/bgc_sliver_title_header_delegate.dart'; import '../../widgets/player/player_avatar.dart'; import '../../widgets/playthrough/calendar_card.dart'; import '../../widgets/playthrough/cooperative_game_result_segmented_button.dart'; +import '../../widgets/playthrough/playthrough_note_list_item.dart'; import '../enter_score/enter_score_view_model.dart'; import 'edit_playthrough_view_model.dart'; import 'playthrough_note_page.dart'; @@ -69,7 +69,7 @@ class EditPlaythroughPageState extends State with EnterScor if (widget.viewModel.gameClassification == GameClassification.Score) _ScoresSection( playerScores: widget.viewModel.playerScores, - playthroughDetailsId: widget.viewModel.playthroughDetails.id, + playthroughDetailsId: widget.viewModel.playthroughDetails?.id, onItemTapped: (PlayerScore playerScore) async => _editPlayerScore(playerScore, context), ), @@ -84,7 +84,7 @@ class EditPlaythroughPageState extends State with EnterScor if (widget.viewModel.hasNotes) _NotesSection( notes: widget.viewModel.notes!, - onTap: (note) => _editNote(note.id), + onTap: (note) => _editNote(note), onDelete: (note) => _deleteNote(note), ), ], @@ -235,19 +235,29 @@ class EditPlaythroughPageState extends State with EnterScor } Future _addNote() async { - await Navigator.of(context).pushNamed( + final PlaythroughNote? addedNote = await Navigator.of(context).pushNamed( PlaythroughNotePage.pageRoute, - arguments: PlaythroughNotePageArguments(widget.viewModel.playthrough), + arguments: const PlaythroughNotePageArguments(), ); - widget.viewModel.refreshNotes(); + + if (addedNote == null) { + return; + } + + widget.viewModel.addPlaythroughNote(addedNote); } - Future _editNote(String noteId) async { - await Navigator.of(context).pushNamed( + Future _editNote(PlaythroughNote note) async { + final PlaythroughNote? editedNote = await Navigator.of(context).pushNamed( PlaythroughNotePage.pageRoute, - arguments: PlaythroughNotePageArguments(widget.viewModel.playthrough, noteId: noteId), + arguments: PlaythroughNotePageArguments(note: note), ); - widget.viewModel.refreshNotes(); + + if (editedNote == null) { + return; + } + + widget.viewModel.editPlaythroughNote(editedNote); } Future _deleteNote(PlaythroughNote note) async { @@ -264,7 +274,7 @@ class _ScoresSection extends StatelessWidget { }) : super(key: key); final List playerScores; - final String playthroughDetailsId; + final String? playthroughDetailsId; final Future Function(PlayerScore) onItemTapped; @override @@ -287,7 +297,7 @@ class _ScoresSection extends StatelessWidget { if (index.isEven) { return _PlayerScoreTile( playerScore: playerScores[itemIndex], - playthroughId: playthroughDetailsId, + playthroughDetailsId: playthroughDetailsId, onItemTapped: onItemTapped, ); } @@ -400,45 +410,19 @@ class _NotesSection extends StatelessWidget { SliverPersistentHeader( pinned: true, delegate: BgcSliverTitleHeaderDelegate.title( - primaryTitle: AppText.editPlaythroughNotesHeaderTitle), + primaryTitle: AppText.editPlaythroughNotesHeaderTitle, + ), ), SliverList( delegate: SliverChildBuilderDelegate( (_, index) { - final int itemIndex = index ~/ 2; - final note = notes[itemIndex]; - if (index.isEven) { - return InkWell( - onTap: () => onTap(note), - child: Slidable( - endActionPane: ActionPane( - extentRatio: 0.25, - motion: const ScrollMotion(), - children: [ - SlidableAction( - icon: Icons.delete, - onPressed: (_) => onDelete(note), - backgroundColor: AppColors.redColor, - ), - ], - ), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(Dimensions.standardSpacing), - child: Text(note.text, textAlign: TextAlign.justify), - ), - ) - ], - ), - ), - ); - } - - return const SizedBox(height: Dimensions.standardSpacing); + return PlaythroughNoteListItem( + onTap: onTap, + note: notes[index], + onDelete: onDelete, + ); }, - childCount: max(0, notes.length * 2 - 1), + childCount: notes.length, ), ), // MK Adding padding to the bottom of the list to avoid overlap of the FOB with the notes @@ -456,12 +440,12 @@ class _PlayerScoreTile extends StatefulWidget { const _PlayerScoreTile({ Key? key, required this.playerScore, - required this.playthroughId, + required this.playthroughDetailsId, required this.onItemTapped, }) : super(key: key); final PlayerScore playerScore; - final String playthroughId; + final String? playthroughDetailsId; final Future Function(PlayerScore) onItemTapped; @override @@ -499,7 +483,7 @@ class _PlayerScoreTileState extends State<_PlayerScoreTile> { child: PlayerAvatar( player: widget.playerScore.player, avatarImageSize: Dimensions.smallPlayerAvatarSize, - playerHeroIdSuffix: widget.playthroughId, + playerHeroIdSuffix: widget.playthroughDetailsId ?? '', ), ), const SizedBox(width: Dimensions.standardSpacing), 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 4327fc9b..e850507d 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 @@ -1,5 +1,6 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:injectable/injectable.dart'; @@ -36,8 +37,8 @@ abstract class _EditPlaythoughViewModel with Store { PlaythroughDetails get playthroughDetailsWorkingCopy => _playthroughDetailsWorkingCopy!; @computed - PlaythroughDetails get playthroughDetails => - _gamePlaythroughsDetailsStore.playthroughsDetails.firstWhere((pd) => pd.id == _playthroughId); + PlaythroughDetails? get playthroughDetails => _gamePlaythroughsDetailsStore.playthroughsDetails + .firstWhereOrNull((pd) => pd.id == _playthroughId); @computed Playthrough get playthrough => playthroughDetailsWorkingCopy.playthrough; @@ -92,7 +93,7 @@ abstract class _EditPlaythoughViewModel with Store { _playthroughId = playthroughId; _playthroughDetailsWorkingCopy = - playthroughDetails.copyWith(playerScores: playthroughDetails.playerScores); + playthroughDetails?.copyWith(playerScores: playthroughDetails!.playerScores); } @action @@ -134,7 +135,7 @@ abstract class _EditPlaythoughViewModel with Store { void updateDuration(int hoursPlayed, int minutesPlyed) { final updatedPlaythrough = playthrough.copyWith( endDate: - playthroughDetails.startDate.add(Duration(hours: hoursPlayed, minutes: minutesPlyed))); + playthroughDetails?.startDate.add(Duration(hours: hoursPlayed, minutes: minutesPlyed))); _playthroughDetailsWorkingCopy = _playthroughDetailsWorkingCopy?.copyWith(playthrough: updatedPlaythrough); @@ -173,25 +174,45 @@ abstract class _EditPlaythoughViewModel with Store { playthroughDetailsWorkingCopy.copyWith(playerScores: updatedPlayerScores); } - /// After adding/editing a note to the [PlaythroughDetails] refresh the working copy with the latest data @action - void refreshNotes() { - _playthroughDetailsWorkingCopy = _playthroughDetailsWorkingCopy!.copyWith( - playthrough: - _playthroughDetailsWorkingCopy!.playthrough.copyWith(notes: playthroughDetails.notes)); + Future deletePlaythrough() async { + if (playthroughDetails == null) { + return; + } + + await _gamePlaythroughsDetailsStore.deletePlaythrough(playthroughDetails!.id); } @action - Future deletePlaythrough() async { - await _gamePlaythroughsDetailsStore.deletePlaythrough(playthroughDetails.id); + void addPlaythroughNote(PlaythroughNote note) { + final existingNotes = _playthroughDetailsWorkingCopy!.notes ?? []; + + _updatePlaythroughDetailsNotes([...existingNotes, note]); + } + + @action + void editPlaythroughNote(PlaythroughNote note) { + final noteToUpdateIndex = + _playthroughDetailsWorkingCopy!.notes?.indexWhere((n) => n.id == note.id); + if (noteToUpdateIndex == null) { + return; + } + + final updatedPlaythroughNotes = _playthroughDetailsWorkingCopy!.notes!; + updatedPlaythroughNotes[noteToUpdateIndex] = note; + + _updatePlaythroughDetailsNotes(updatedPlaythroughNotes); } @action void deletePlaythroughNote(PlaythroughNote note) { final updatedPlaythroughNotes = List.from(_playthroughDetailsWorkingCopy!.notes!)..remove(note); - _playthroughDetailsWorkingCopy = _playthroughDetailsWorkingCopy!.copyWith( - playthrough: - _playthroughDetailsWorkingCopy!.playthrough.copyWith(notes: updatedPlaythroughNotes)); + _updatePlaythroughDetailsNotes(updatedPlaythroughNotes); + } + + void _updatePlaythroughDetailsNotes(List notes) { + _playthroughDetailsWorkingCopy = _playthroughDetailsWorkingCopy! + .copyWith(playthrough: _playthroughDetailsWorkingCopy!.playthrough.copyWith(notes: notes)); } } diff --git a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.g.dart b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.g.dart index fe36d5b1..1f54b61d 100644 --- a/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.g.dart +++ b/board_games_companion/lib/pages/edit_playthrough/edit_playthrough_view_model.g.dart @@ -9,13 +9,14 @@ part of 'edit_playthrough_view_model.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 _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { - Computed? _$playthroughDetailsComputed; + Computed? _$playthroughDetailsComputed; @override - PlaythroughDetails get playthroughDetails => (_$playthroughDetailsComputed ??= - Computed(() => super.playthroughDetails, + PlaythroughDetails? get playthroughDetails => + (_$playthroughDetailsComputed ??= Computed( + () => super.playthroughDetails, name: '_EditPlaythoughViewModel.playthroughDetails')) - .value; + .value; Computed? _$playthroughComputed; @override @@ -209,11 +210,22 @@ mixin _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { } @override - void refreshNotes() { + void addPlaythroughNote(PlaythroughNote note) { + final _$actionInfo = _$_EditPlaythoughViewModelActionController.startAction( + name: '_EditPlaythoughViewModel.addPlaythroughNote'); + try { + return super.addPlaythroughNote(note); + } finally { + _$_EditPlaythoughViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void editPlaythroughNote(PlaythroughNote note) { final _$actionInfo = _$_EditPlaythoughViewModelActionController.startAction( - name: '_EditPlaythoughViewModel.refreshNotes'); + name: '_EditPlaythoughViewModel.editPlaythroughNote'); try { - return super.refreshNotes(); + return super.editPlaythroughNote(note); } finally { _$_EditPlaythoughViewModelActionController.endAction(_$actionInfo); } diff --git a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_page.dart b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_page.dart index a91a3fc6..12e6a957 100644 --- a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_page.dart +++ b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_page.dart @@ -103,8 +103,8 @@ class _PlaythroughNotePageState extends State { Future _saveNote() async { final navigatorState = Navigator.of(context); - await widget.viewModel.saveNote(_controller.text); - navigatorState.pop(); + widget.viewModel.updateNote(_controller.text); + navigatorState.pop(widget.viewModel.note); } void _updateNote(String note) => widget.viewModel.updateNote(note); 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 0d672e83..581b68d7 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 @@ -5,10 +5,7 @@ import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:uuid/uuid.dart'; -import '../../models/hive/playthrough.dart'; import '../../models/hive/playthrough_note.dart'; -import '../../models/playthroughs/playthrough_details.dart'; -import '../../stores/game_playthroughs_details_store.dart'; import 'playthrough_note_page_visual_states.dart'; part 'playthrough_note_view_model.g.dart'; @@ -17,24 +14,11 @@ part 'playthrough_note_view_model.g.dart'; class PlaythroughNoteViewModel = _PlaythroughNoteViewModel with _$PlaythroughNoteViewModel; abstract class _PlaythroughNoteViewModel with Store { - _PlaythroughNoteViewModel(this._gamePlaythroughsDetailsStore); - - final GamePlaythroughsDetailsStore _gamePlaythroughsDetailsStore; - - PlaythroughDetails get _playthroughDetails => _gamePlaythroughsDetailsStore.playthroughsDetails - .firstWhere((playthroughDetails) => playthroughDetails.id == _playthrough!.id); - - String? _noteId; + late bool isNewNote; @observable PlaythroughNote? note; - @observable - Playthrough? _playthrough; - - @computed - bool get isNewNote => _noteId == null; - @computed bool get isNoteEmpty => note?.text.isNullOrBlank ?? false; @@ -44,46 +28,19 @@ abstract class _PlaythroughNoteViewModel with Store { : PlaythroughNotePageVisualState.edit(note!); @action - void setNoteId(String? value) { - _noteId = value; - if (_noteId == null) { - note = PlaythroughNote( - id: const Uuid().v4(), - text: '', - createdAt: DateTime.now().toUtc(), - ); - - return; - } + void setNote(PlaythroughNote? playthroughNote) { + isNewNote = playthroughNote == null; + playthroughNote ??= PlaythroughNote( + id: const Uuid().v4(), + text: '', + createdAt: DateTime.now().toUtc(), + ); - note = _playthrough!.notes!.firstWhere((note) => note.id == _noteId); + note = playthroughNote; } - @action - void setPlaythrough(Playthrough value) => _playthrough = value; - @action void updateNote(String text) { note = note!.copyWith(text: text); } - - @action - Future saveNote(String text) async { - updateNote(text); - - final playthroughNotes = List.from(_playthrough!.notes ?? []); - visualState.maybeWhen( - add: () { - playthroughNotes.add(note!); - }, - orElse: () { - final noteToUpdateIndex = playthroughNotes.indexWhere((n) => n.id == _noteId); - playthroughNotes[noteToUpdateIndex] = note!.copyWith(modifiedAt: DateTime.now().toUtc()); - }, - ); - - _playthrough = _playthrough!.copyWith(notes: playthroughNotes); - final updatedPlaythroughDetails = _playthroughDetails.copyWith(playthrough: _playthrough!); - await _gamePlaythroughsDetailsStore.updatePlaythrough(updatedPlaythroughDetails); - } } diff --git a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.g.dart b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.g.dart index 8d415fea..0ce0a54c 100644 --- a/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.g.dart +++ b/board_games_companion/lib/pages/edit_playthrough/playthrough_note_view_model.g.dart @@ -9,13 +9,6 @@ part of 'playthrough_note_view_model.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 _$PlaythroughNoteViewModel on _PlaythroughNoteViewModel, Store { - Computed? _$isNewNoteComputed; - - @override - bool get isNewNote => - (_$isNewNoteComputed ??= Computed(() => super.isNewNote, - name: '_PlaythroughNoteViewModel.isNewNote')) - .value; Computed? _$isNoteEmptyComputed; @override @@ -47,50 +40,15 @@ mixin _$PlaythroughNoteViewModel on _PlaythroughNoteViewModel, Store { }); } - late final _$_playthroughAtom = - Atom(name: '_PlaythroughNoteViewModel._playthrough', context: context); - - @override - Playthrough? get _playthrough { - _$_playthroughAtom.reportRead(); - return super._playthrough; - } - - @override - set _playthrough(Playthrough? value) { - _$_playthroughAtom.reportWrite(value, super._playthrough, () { - super._playthrough = value; - }); - } - - late final _$saveNoteAsyncAction = - AsyncAction('_PlaythroughNoteViewModel.saveNote', context: context); - - @override - Future saveNote(String text) { - return _$saveNoteAsyncAction.run(() => super.saveNote(text)); - } - late final _$_PlaythroughNoteViewModelActionController = ActionController(name: '_PlaythroughNoteViewModel', context: context); @override - void setNoteId(String? value) { - final _$actionInfo = _$_PlaythroughNoteViewModelActionController - .startAction(name: '_PlaythroughNoteViewModel.setNoteId'); - try { - return super.setNoteId(value); - } finally { - _$_PlaythroughNoteViewModelActionController.endAction(_$actionInfo); - } - } - - @override - void setPlaythrough(Playthrough value) { + void setNote(PlaythroughNote? playthroughNote) { final _$actionInfo = _$_PlaythroughNoteViewModelActionController - .startAction(name: '_PlaythroughNoteViewModel.setPlaythrough'); + .startAction(name: '_PlaythroughNoteViewModel.setNote'); try { - return super.setPlaythrough(value); + return super.setNote(playthroughNote); } finally { _$_PlaythroughNoteViewModelActionController.endAction(_$actionInfo); } @@ -111,7 +69,6 @@ mixin _$PlaythroughNoteViewModel on _PlaythroughNoteViewModel, Store { String toString() { return ''' note: ${note}, -isNewNote: ${isNewNote}, isNoteEmpty: ${isNoteEmpty}, visualState: ${visualState} '''; diff --git a/board_games_companion/lib/pages/plays/grouped_board_game_playthroughs.dart b/board_games_companion/lib/pages/plays/grouped_board_game_playthroughs.dart deleted file mode 100644 index 7f01d350..00000000 --- a/board_games_companion/lib/pages/plays/grouped_board_game_playthroughs.dart +++ /dev/null @@ -1,37 +0,0 @@ -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/plays/grouped_board_game_playthroughs.freezed.dart b/board_games_companion/lib/pages/plays/grouped_board_game_playthroughs.freezed.dart deleted file mode 100644 index 8ca87e87..00000000 --- a/board_games_companion/lib/pages/plays/grouped_board_game_playthroughs.freezed.dart +++ /dev/null @@ -1,173 +0,0 @@ -// 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, unnecessary_question_mark - -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, - GroupedBoardGamePlaythroughs>; - @useResult - $Res call({DateTime date, List boardGamePlaythroughs}); -} - -/// @nodoc -class _$GroupedBoardGamePlaythroughsCopyWithImpl<$Res, - $Val extends GroupedBoardGamePlaythroughs> - implements $GroupedBoardGamePlaythroughsCopyWith<$Res> { - _$GroupedBoardGamePlaythroughsCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - Object? boardGamePlaythroughs = null, - }) { - return _then(_value.copyWith( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - boardGamePlaythroughs: null == boardGamePlaythroughs - ? _value.boardGamePlaythroughs - : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$_GroupedBoardGamePlaythroughsCopyWith<$Res> - implements $GroupedBoardGamePlaythroughsCopyWith<$Res> { - factory _$$_GroupedBoardGamePlaythroughsCopyWith( - _$_GroupedBoardGamePlaythroughs value, - $Res Function(_$_GroupedBoardGamePlaythroughs) then) = - __$$_GroupedBoardGamePlaythroughsCopyWithImpl<$Res>; - @override - @useResult - $Res call({DateTime date, List boardGamePlaythroughs}); -} - -/// @nodoc -class __$$_GroupedBoardGamePlaythroughsCopyWithImpl<$Res> - extends _$GroupedBoardGamePlaythroughsCopyWithImpl<$Res, - _$_GroupedBoardGamePlaythroughs> - implements _$$_GroupedBoardGamePlaythroughsCopyWith<$Res> { - __$$_GroupedBoardGamePlaythroughsCopyWithImpl( - _$_GroupedBoardGamePlaythroughs _value, - $Res Function(_$_GroupedBoardGamePlaythroughs) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - Object? boardGamePlaythroughs = null, - }) { - return _then(_$_GroupedBoardGamePlaythroughs( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - boardGamePlaythroughs: null == boardGamePlaythroughs - ? _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 { - if (_boardGamePlaythroughs is EqualUnmodifiableListView) - return _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 && - (identical(other.date, date) || other.date == date) && - const DeepCollectionEquality() - .equals(other._boardGamePlaythroughs, _boardGamePlaythroughs)); - } - - @override - int get hashCode => Object.hash(runtimeType, date, - const DeepCollectionEquality().hash(_boardGamePlaythroughs)); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$_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/plays/historical_playthrough.dart b/board_games_companion/lib/pages/plays/historical_playthrough.dart new file mode 100644 index 00000000..33f1c81d --- /dev/null +++ b/board_games_companion/lib/pages/plays/historical_playthrough.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'board_game_playthrough.dart'; + +part 'historical_playthrough.freezed.dart'; + +@freezed +class HistoricalPlaythrough with _$HistoricalPlaythrough { + const factory HistoricalPlaythrough.withDateHeader({ + required DateTime playedOn, + required BoardGamePlaythrough boardGamePlaythroughs, + }) = _withDateHeader; + const factory HistoricalPlaythrough.withoutDateHeader({ + required BoardGamePlaythrough boardGamePlaythroughs, + }) = _withoutDateHeader; +} diff --git a/board_games_companion/lib/pages/plays/historical_playthrough.freezed.dart b/board_games_companion/lib/pages/plays/historical_playthrough.freezed.dart new file mode 100644 index 00000000..a5758213 --- /dev/null +++ b/board_games_companion/lib/pages/plays/historical_playthrough.freezed.dart @@ -0,0 +1,434 @@ +// 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, unnecessary_question_mark + +part of 'historical_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 _$HistoricalPlaythrough { + BoardGamePlaythrough get boardGamePlaythroughs => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs) + withDateHeader, + required TResult Function(BoardGamePlaythrough boardGamePlaythroughs) + withoutDateHeader, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult? Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_withDateHeader value) withDateHeader, + required TResult Function(_withoutDateHeader value) withoutDateHeader, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_withDateHeader value)? withDateHeader, + TResult? Function(_withoutDateHeader value)? withoutDateHeader, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_withDateHeader value)? withDateHeader, + TResult Function(_withoutDateHeader value)? withoutDateHeader, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HistoricalPlaythroughCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HistoricalPlaythroughCopyWith<$Res> { + factory $HistoricalPlaythroughCopyWith(HistoricalPlaythrough value, + $Res Function(HistoricalPlaythrough) then) = + _$HistoricalPlaythroughCopyWithImpl<$Res, HistoricalPlaythrough>; + @useResult + $Res call({BoardGamePlaythrough boardGamePlaythroughs}); + + $BoardGamePlaythroughCopyWith<$Res> get boardGamePlaythroughs; +} + +/// @nodoc +class _$HistoricalPlaythroughCopyWithImpl<$Res, + $Val extends HistoricalPlaythrough> + implements $HistoricalPlaythroughCopyWith<$Res> { + _$HistoricalPlaythroughCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? boardGamePlaythroughs = null, + }) { + return _then(_value.copyWith( + boardGamePlaythroughs: null == boardGamePlaythroughs + ? _value.boardGamePlaythroughs + : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable + as BoardGamePlaythrough, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $BoardGamePlaythroughCopyWith<$Res> get boardGamePlaythroughs { + return $BoardGamePlaythroughCopyWith<$Res>(_value.boardGamePlaythroughs, + (value) { + return _then(_value.copyWith(boardGamePlaythroughs: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_withDateHeaderCopyWith<$Res> + implements $HistoricalPlaythroughCopyWith<$Res> { + factory _$$_withDateHeaderCopyWith( + _$_withDateHeader value, $Res Function(_$_withDateHeader) then) = + __$$_withDateHeaderCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs}); + + @override + $BoardGamePlaythroughCopyWith<$Res> get boardGamePlaythroughs; +} + +/// @nodoc +class __$$_withDateHeaderCopyWithImpl<$Res> + extends _$HistoricalPlaythroughCopyWithImpl<$Res, _$_withDateHeader> + implements _$$_withDateHeaderCopyWith<$Res> { + __$$_withDateHeaderCopyWithImpl( + _$_withDateHeader _value, $Res Function(_$_withDateHeader) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? playedOn = null, + Object? boardGamePlaythroughs = null, + }) { + return _then(_$_withDateHeader( + playedOn: null == playedOn + ? _value.playedOn + : playedOn // ignore: cast_nullable_to_non_nullable + as DateTime, + boardGamePlaythroughs: null == boardGamePlaythroughs + ? _value.boardGamePlaythroughs + : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable + as BoardGamePlaythrough, + )); + } +} + +/// @nodoc + +class _$_withDateHeader implements _withDateHeader { + const _$_withDateHeader( + {required this.playedOn, required this.boardGamePlaythroughs}); + + @override + final DateTime playedOn; + @override + final BoardGamePlaythrough boardGamePlaythroughs; + + @override + String toString() { + return 'HistoricalPlaythrough.withDateHeader(playedOn: $playedOn, boardGamePlaythroughs: $boardGamePlaythroughs)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_withDateHeader && + (identical(other.playedOn, playedOn) || + other.playedOn == playedOn) && + (identical(other.boardGamePlaythroughs, boardGamePlaythroughs) || + other.boardGamePlaythroughs == boardGamePlaythroughs)); + } + + @override + int get hashCode => Object.hash(runtimeType, playedOn, boardGamePlaythroughs); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_withDateHeaderCopyWith<_$_withDateHeader> get copyWith => + __$$_withDateHeaderCopyWithImpl<_$_withDateHeader>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs) + withDateHeader, + required TResult Function(BoardGamePlaythrough boardGamePlaythroughs) + withoutDateHeader, + }) { + return withDateHeader(playedOn, boardGamePlaythroughs); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult? Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + }) { + return withDateHeader?.call(playedOn, boardGamePlaythroughs); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + required TResult orElse(), + }) { + if (withDateHeader != null) { + return withDateHeader(playedOn, boardGamePlaythroughs); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_withDateHeader value) withDateHeader, + required TResult Function(_withoutDateHeader value) withoutDateHeader, + }) { + return withDateHeader(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_withDateHeader value)? withDateHeader, + TResult? Function(_withoutDateHeader value)? withoutDateHeader, + }) { + return withDateHeader?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_withDateHeader value)? withDateHeader, + TResult Function(_withoutDateHeader value)? withoutDateHeader, + required TResult orElse(), + }) { + if (withDateHeader != null) { + return withDateHeader(this); + } + return orElse(); + } +} + +abstract class _withDateHeader implements HistoricalPlaythrough { + const factory _withDateHeader( + {required final DateTime playedOn, + required final BoardGamePlaythrough boardGamePlaythroughs}) = + _$_withDateHeader; + + DateTime get playedOn; + @override + BoardGamePlaythrough get boardGamePlaythroughs; + @override + @JsonKey(ignore: true) + _$$_withDateHeaderCopyWith<_$_withDateHeader> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_withoutDateHeaderCopyWith<$Res> + implements $HistoricalPlaythroughCopyWith<$Res> { + factory _$$_withoutDateHeaderCopyWith(_$_withoutDateHeader value, + $Res Function(_$_withoutDateHeader) then) = + __$$_withoutDateHeaderCopyWithImpl<$Res>; + @override + @useResult + $Res call({BoardGamePlaythrough boardGamePlaythroughs}); + + @override + $BoardGamePlaythroughCopyWith<$Res> get boardGamePlaythroughs; +} + +/// @nodoc +class __$$_withoutDateHeaderCopyWithImpl<$Res> + extends _$HistoricalPlaythroughCopyWithImpl<$Res, _$_withoutDateHeader> + implements _$$_withoutDateHeaderCopyWith<$Res> { + __$$_withoutDateHeaderCopyWithImpl( + _$_withoutDateHeader _value, $Res Function(_$_withoutDateHeader) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? boardGamePlaythroughs = null, + }) { + return _then(_$_withoutDateHeader( + boardGamePlaythroughs: null == boardGamePlaythroughs + ? _value.boardGamePlaythroughs + : boardGamePlaythroughs // ignore: cast_nullable_to_non_nullable + as BoardGamePlaythrough, + )); + } +} + +/// @nodoc + +class _$_withoutDateHeader implements _withoutDateHeader { + const _$_withoutDateHeader({required this.boardGamePlaythroughs}); + + @override + final BoardGamePlaythrough boardGamePlaythroughs; + + @override + String toString() { + return 'HistoricalPlaythrough.withoutDateHeader(boardGamePlaythroughs: $boardGamePlaythroughs)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_withoutDateHeader && + (identical(other.boardGamePlaythroughs, boardGamePlaythroughs) || + other.boardGamePlaythroughs == boardGamePlaythroughs)); + } + + @override + int get hashCode => Object.hash(runtimeType, boardGamePlaythroughs); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_withoutDateHeaderCopyWith<_$_withoutDateHeader> get copyWith => + __$$_withoutDateHeaderCopyWithImpl<_$_withoutDateHeader>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs) + withDateHeader, + required TResult Function(BoardGamePlaythrough boardGamePlaythroughs) + withoutDateHeader, + }) { + return withoutDateHeader(boardGamePlaythroughs); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult? Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + }) { + return withoutDateHeader?.call(boardGamePlaythroughs); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + DateTime playedOn, BoardGamePlaythrough boardGamePlaythroughs)? + withDateHeader, + TResult Function(BoardGamePlaythrough boardGamePlaythroughs)? + withoutDateHeader, + required TResult orElse(), + }) { + if (withoutDateHeader != null) { + return withoutDateHeader(boardGamePlaythroughs); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_withDateHeader value) withDateHeader, + required TResult Function(_withoutDateHeader value) withoutDateHeader, + }) { + return withoutDateHeader(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_withDateHeader value)? withDateHeader, + TResult? Function(_withoutDateHeader value)? withoutDateHeader, + }) { + return withoutDateHeader?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_withDateHeader value)? withDateHeader, + TResult Function(_withoutDateHeader value)? withoutDateHeader, + required TResult orElse(), + }) { + if (withoutDateHeader != null) { + return withoutDateHeader(this); + } + return orElse(); + } +} + +abstract class _withoutDateHeader implements HistoricalPlaythrough { + const factory _withoutDateHeader( + {required final BoardGamePlaythrough boardGamePlaythroughs}) = + _$_withoutDateHeader; + + @override + BoardGamePlaythrough get boardGamePlaythroughs; + @override + @JsonKey(ignore: true) + _$$_withoutDateHeaderCopyWith<_$_withoutDateHeader> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/pages/plays/plays_page.dart b/board_games_companion/lib/pages/plays/plays_page.dart index a8fc5c39..c6d1e7b8 100644 --- a/board_games_companion/lib/pages/plays/plays_page.dart +++ b/board_games_companion/lib/pages/plays/plays_page.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:basics/basics.dart'; +import 'package:board_games_companion/extensions/date_time_extensions.dart'; +import 'package:board_games_companion/pages/plays/historical_playthrough.dart'; +import 'package:board_games_companion/widgets/common/section_header.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -41,7 +44,6 @@ import '../playthroughs/playthroughs_page.dart'; import 'board_game_playthrough.dart'; import 'game_spinner_filters.dart'; import 'game_spinner_game_selected_dialog.dart'; -import 'grouped_board_game_playthroughs.dart'; import 'plays_page_visual_states.dart'; import 'plays_view_model.dart'; @@ -122,32 +124,12 @@ class _PlaysPageState extends State with SingleTickerProviderStateMix Observer( builder: (_) { return widget.viewModel.visualState?.when( - history: (tab, finishedPlaythroughs) { - if (!widget.viewModel.hasAnyFinishedPlaythroughs) { + history: (tab, historicalPlaythroughs) { + if (historicalPlaythroughs.isEmpty) { return const _NoPlaythroughsSliver(); } else { - return MultiSliver( - children: [ - if (widget.viewModel.hasAnyFinishedPlaythroughs) ...[ - for (final groupedBoardGamePlaythroughs - in widget.viewModel.finishedBoardGamePlaythroughs) ...[ - _PlaythroughGroupHeaderSliver( - widget: widget, - groupedBoardGamePlaythroughs: - groupedBoardGamePlaythroughs, - ), - _PlaythroughGroupListSliver( - groupedBoardGamePlaythroughs: - groupedBoardGamePlaythroughs, - ), - if (groupedBoardGamePlaythroughs == - widget.viewModel.finishedBoardGamePlaythroughs.last) - const SliverToBoxAdapter( - child: SizedBox(height: Dimensions.bottomTabTopHeight), - ), - ] - ] - ], + return _HistoricalPlaythroughSliverList( + historicalPlaythroughs: historicalPlaythroughs, ); } }, @@ -226,6 +208,47 @@ class _PlaysPageState extends State with SingleTickerProviderStateMix } } +class _HistoricalPlaythroughSliverList extends StatelessWidget { + const _HistoricalPlaythroughSliverList({ + required this.historicalPlaythroughs, + }); + + final List historicalPlaythroughs; + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final isLast = index == historicalPlaythroughs.length - 1; + final historicalPlaythrough = historicalPlaythroughs[index].when( + withDateHeader: (playedOn, boardGamePlaythroughs) => + _HistoricalPlaythroughItem.withDateHeader( + boardGamePlaythrough: boardGamePlaythroughs, + playedOn: playedOn, + ), + withoutDateHeader: (boardGamePlaythroughs) => + _HistoricalPlaythroughItem.withoutDateHeader( + boardGamePlaythrough: boardGamePlaythroughs, + ), + ); + + if (isLast) { + return Column( + children: [ + historicalPlaythrough, + const SizedBox(height: Dimensions.bottomTabTopHeight) + ], + ); + } + + return historicalPlaythrough; + }, + childCount: historicalPlaythroughs.length, + )); + } +} + class _GameSpinnerFilters extends StatelessWidget { const _GameSpinnerFilters({ required this.gameSpinnerFilters, @@ -672,81 +695,100 @@ class _GameSpinnerItemWebImage extends StatelessWidget { } } -class _PlaythroughGroupListSliver extends StatelessWidget { - const _PlaythroughGroupListSliver({ - Key? key, - required this.groupedBoardGamePlaythroughs, - }) : super(key: key); +class _HistoricalPlaythroughItem extends StatelessWidget { + const _HistoricalPlaythroughItem._({ + required this.boardGamePlaythrough, + this.playedOn, + }) : super(); + + factory _HistoricalPlaythroughItem.withoutDateHeader({ + required BoardGamePlaythrough boardGamePlaythrough, + }) => + _HistoricalPlaythroughItem._(boardGamePlaythrough: boardGamePlaythrough); + + factory _HistoricalPlaythroughItem.withDateHeader({ + required BoardGamePlaythrough boardGamePlaythrough, + required DateTime playedOn, + }) => + _HistoricalPlaythroughItem._( + boardGamePlaythrough: boardGamePlaythrough, + playedOn: playedOn, + ); - final GroupedBoardGamePlaythroughs groupedBoardGamePlaythroughs; + static const double _playthroughContainerHeight = 110; + + final BoardGamePlaythrough boardGamePlaythrough; + final DateTime? playedOn; @override Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (_, index) { - final boardGamePlaythrough = groupedBoardGamePlaythroughs.boardGamePlaythroughs[index]; - final isFirst = index == 0; - return Padding( - padding: EdgeInsets.only( - top: isFirst ? Dimensions.standardSpacing : 0, - bottom: Dimensions.standardSpacing, - left: Dimensions.standardSpacing, - right: Dimensions.standardSpacing, + final playthroughTile = Padding( + padding: const EdgeInsets.only( + bottom: Dimensions.standardSpacing, + left: Dimensions.standardSpacing, + right: Dimensions.standardSpacing, + ), + child: PanelContainer( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppStyles.panelContainerCornerRadius), + onTap: () => _navigateToEditPlaythrough( + context, + boardGamePlaythrough.boardGameDetails.id, + boardGamePlaythrough.playthrough.id, ), - child: PanelContainer( - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(AppStyles.panelContainerCornerRadius), - onTap: () => _navigateToEditPlaythrough( - context, - boardGamePlaythrough.boardGameDetails.id, - boardGamePlaythrough.playthrough.id, - ), - child: Padding( - padding: const EdgeInsets.all(Dimensions.standardSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Padding( + padding: const EdgeInsets.all(Dimensions.standardSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: _playthroughContainerHeight, + child: Row( 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), - ), - ], + 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, + ), + ), ), ); + + if (playedOn == null) { + return playthroughTile; + } + + return Column( + children: [ + SectionHeader.title(title: playedOn!.toHistoricalPlaythroughHeaderFormat()), + const SizedBox(height: Dimensions.standardSpacing), + playthroughTile, + ], + ); } Future _navigateToPlaythrough( @@ -794,6 +836,128 @@ class _PlaythroughGroupListSliver extends StatelessWidget { } } +// 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]; +// final isFirst = index == 0; +// return Padding( +// padding: EdgeInsets.only( +// top: isFirst ? Dimensions.standardSpacing : 0, +// bottom: Dimensions.standardSpacing, +// left: Dimensions.standardSpacing, +// right: Dimensions.standardSpacing, +// ), +// child: PanelContainer( +// child: Material( +// color: Colors.transparent, +// child: InkWell( +// borderRadius: BorderRadius.circular(AppStyles.panelContainerCornerRadius), +// onTap: () => _navigateToEditPlaythrough( +// context, +// boardGamePlaythrough.boardGameDetails.id, +// boardGamePlaythrough.playthrough.id, +// ), +// 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, +// boardGameImageHeroId: boardGamePlaythrough.id, +// navigatingFromType: PlaysPage, +// ), +// ); +// } + +// void _navigateToEditPlaythrough( +// BuildContext context, +// String boardGameId, +// String playthroughId, +// ) { +// Navigator.pushNamed( +// context, +// EditPlaythroughPage.pageRoute, +// arguments: EditPlaythroughPageArguments( +// boardGameId: boardGameId, +// playthroughId: playthroughId, +// goBackPageRoute: HomePage.pageRoute, +// ), +// ); +// } +// } + class _PlaythroughDetails extends StatelessWidget { const _PlaythroughDetails({ Key? key, @@ -854,7 +1018,7 @@ class _PlaythroughGeneralStats extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(width: _uniformedIconSize, child: Center(child: icon)), + SizedBox(width: _uniformedIconSize, child: icon), const SizedBox(width: Dimensions.standardSpacing), Expanded( child: Text( @@ -895,25 +1059,6 @@ class _PlaythroughActions extends StatelessWidget { } } -class _PlaythroughGroupHeaderSliver extends StatelessWidget { - const _PlaythroughGroupHeaderSliver({ - Key? key, - required this.widget, - required this.groupedBoardGamePlaythroughs, - }) : super(key: key); - - final PlaysPage widget; - final GroupedBoardGamePlaythroughs groupedBoardGamePlaythroughs; - - @override - Widget build(BuildContext context) { - return SliverPersistentHeader( - delegate: BgcSliverTitleHeaderDelegate.title( - primaryTitle: groupedBoardGamePlaythroughs.dateFormtted), - ); - } -} - class _NoPlaythroughsSliver extends StatelessWidget { const _NoPlaythroughsSliver({Key? key}) : super(key: key); diff --git a/board_games_companion/lib/pages/plays/plays_page_visual_states.dart b/board_games_companion/lib/pages/plays/plays_page_visual_states.dart index f80c8424..5ba595fc 100644 --- a/board_games_companion/lib/pages/plays/plays_page_visual_states.dart +++ b/board_games_companion/lib/pages/plays/plays_page_visual_states.dart @@ -1,8 +1,8 @@ +import 'package:board_games_companion/pages/plays/historical_playthrough.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../common/enums/plays_tab.dart'; import '../../models/hive/board_game_details.dart'; -import 'grouped_board_game_playthroughs.dart'; part 'plays_page_visual_states.freezed.dart'; @@ -10,7 +10,7 @@ part 'plays_page_visual_states.freezed.dart'; class PlaysPageVisualState with _$PlaysPageVisualState { const factory PlaysPageVisualState.history( PlaysTab playsTab, - List finishedBoardGamePlaythroughs, + List historicalPlaythroughs, ) = _History; const factory PlaysPageVisualState.statistics(PlaysTab playsTab) = _Statistics; const factory PlaysPageVisualState.selectGame( diff --git a/board_games_companion/lib/pages/plays/plays_page_visual_states.freezed.dart b/board_games_companion/lib/pages/plays/plays_page_visual_states.freezed.dart index 7c6b6520..8d1afa3e 100644 --- a/board_games_companion/lib/pages/plays/plays_page_visual_states.freezed.dart +++ b/board_games_companion/lib/pages/plays/plays_page_visual_states.freezed.dart @@ -20,7 +20,7 @@ mixin _$PlaysPageVisualState { @optionalTypeArgs TResult when({ required TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs) + List historicalPlaythroughs) history, required TResult Function(PlaysTab playsTab) statistics, required TResult Function( @@ -31,7 +31,7 @@ mixin _$PlaysPageVisualState { @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult? Function(PlaysTab playsTab)? statistics, TResult? Function( @@ -42,7 +42,7 @@ mixin _$PlaysPageVisualState { @optionalTypeArgs TResult maybeWhen({ TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult Function(PlaysTab playsTab)? statistics, TResult Function( @@ -122,8 +122,7 @@ abstract class _$$_HistoryCopyWith<$Res> @override @useResult $Res call( - {PlaysTab playsTab, - List finishedBoardGamePlaythroughs}); + {PlaysTab playsTab, List historicalPlaythroughs}); } /// @nodoc @@ -137,17 +136,17 @@ class __$$_HistoryCopyWithImpl<$Res> @override $Res call({ Object? playsTab = null, - Object? finishedBoardGamePlaythroughs = null, + Object? historicalPlaythroughs = null, }) { return _then(_$_History( null == playsTab ? _value.playsTab : playsTab // ignore: cast_nullable_to_non_nullable as PlaysTab, - null == finishedBoardGamePlaythroughs - ? _value._finishedBoardGamePlaythroughs - : finishedBoardGamePlaythroughs // ignore: cast_nullable_to_non_nullable - as List, + null == historicalPlaythroughs + ? _value._historicalPlaythroughs + : historicalPlaythroughs // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -155,24 +154,24 @@ class __$$_HistoryCopyWithImpl<$Res> /// @nodoc class _$_History implements _History { - const _$_History(this.playsTab, - final List finishedBoardGamePlaythroughs) - : _finishedBoardGamePlaythroughs = finishedBoardGamePlaythroughs; + const _$_History( + this.playsTab, final List historicalPlaythroughs) + : _historicalPlaythroughs = historicalPlaythroughs; @override final PlaysTab playsTab; - final List _finishedBoardGamePlaythroughs; + final List _historicalPlaythroughs; @override - List get finishedBoardGamePlaythroughs { - if (_finishedBoardGamePlaythroughs is EqualUnmodifiableListView) - return _finishedBoardGamePlaythroughs; + List get historicalPlaythroughs { + if (_historicalPlaythroughs is EqualUnmodifiableListView) + return _historicalPlaythroughs; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_finishedBoardGamePlaythroughs); + return EqualUnmodifiableListView(_historicalPlaythroughs); } @override String toString() { - return 'PlaysPageVisualState.history(playsTab: $playsTab, finishedBoardGamePlaythroughs: $finishedBoardGamePlaythroughs)'; + return 'PlaysPageVisualState.history(playsTab: $playsTab, historicalPlaythroughs: $historicalPlaythroughs)'; } @override @@ -183,13 +182,12 @@ class _$_History implements _History { (identical(other.playsTab, playsTab) || other.playsTab == playsTab) && const DeepCollectionEquality().equals( - other._finishedBoardGamePlaythroughs, - _finishedBoardGamePlaythroughs)); + other._historicalPlaythroughs, _historicalPlaythroughs)); } @override int get hashCode => Object.hash(runtimeType, playsTab, - const DeepCollectionEquality().hash(_finishedBoardGamePlaythroughs)); + const DeepCollectionEquality().hash(_historicalPlaythroughs)); @JsonKey(ignore: true) @override @@ -201,35 +199,35 @@ class _$_History implements _History { @optionalTypeArgs TResult when({ required TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs) + List historicalPlaythroughs) history, required TResult Function(PlaysTab playsTab) statistics, required TResult Function( PlaysTab playsTab, List shuffledBoardGames) selectGame, }) { - return history(playsTab, finishedBoardGamePlaythroughs); + return history(playsTab, historicalPlaythroughs); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult? Function(PlaysTab playsTab)? statistics, TResult? Function( PlaysTab playsTab, List shuffledBoardGames)? selectGame, }) { - return history?.call(playsTab, finishedBoardGamePlaythroughs); + return history?.call(playsTab, historicalPlaythroughs); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult Function(PlaysTab playsTab)? statistics, TResult Function( @@ -238,7 +236,7 @@ class _$_History implements _History { required TResult orElse(), }) { if (history != null) { - return history(playsTab, finishedBoardGamePlaythroughs); + return history(playsTab, historicalPlaythroughs); } return orElse(); } @@ -279,14 +277,12 @@ class _$_History implements _History { } abstract class _History implements PlaysPageVisualState { - const factory _History( - final PlaysTab playsTab, - final List - finishedBoardGamePlaythroughs) = _$_History; + const factory _History(final PlaysTab playsTab, + final List historicalPlaythroughs) = _$_History; @override PlaysTab get playsTab; - List get finishedBoardGamePlaythroughs; + List get historicalPlaythroughs; @override @JsonKey(ignore: true) _$$_HistoryCopyWith<_$_History> get copyWith => @@ -361,7 +357,7 @@ class _$_Statistics implements _Statistics { @optionalTypeArgs TResult when({ required TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs) + List historicalPlaythroughs) history, required TResult Function(PlaysTab playsTab) statistics, required TResult Function( @@ -375,7 +371,7 @@ class _$_Statistics implements _Statistics { @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult? Function(PlaysTab playsTab)? statistics, TResult? Function( @@ -389,7 +385,7 @@ class _$_Statistics implements _Statistics { @optionalTypeArgs TResult maybeWhen({ TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult Function(PlaysTab playsTab)? statistics, TResult Function( @@ -535,7 +531,7 @@ class _$_SelectGame implements _SelectGame { @optionalTypeArgs TResult when({ required TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs) + List historicalPlaythroughs) history, required TResult Function(PlaysTab playsTab) statistics, required TResult Function( @@ -549,7 +545,7 @@ class _$_SelectGame implements _SelectGame { @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult? Function(PlaysTab playsTab)? statistics, TResult? Function( @@ -563,7 +559,7 @@ class _$_SelectGame implements _SelectGame { @optionalTypeArgs TResult maybeWhen({ TResult Function(PlaysTab playsTab, - List finishedBoardGamePlaythroughs)? + List historicalPlaythroughs)? history, TResult Function(PlaysTab playsTab)? statistics, TResult Function( diff --git a/board_games_companion/lib/pages/plays/plays_view_model.dart b/board_games_companion/lib/pages/plays/plays_view_model.dart index 29474470..394ae6ef 100644 --- a/board_games_companion/lib/pages/plays/plays_view_model.dart +++ b/board_games_companion/lib/pages/plays/plays_view_model.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:basics/basics.dart'; +import 'package:board_games_companion/pages/plays/historical_playthrough.dart'; import 'package:collection/collection.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; @@ -11,6 +12,7 @@ import 'package:tuple/tuple.dart'; import '../../common/analytics.dart'; import '../../common/enums/collection_type.dart'; import '../../common/enums/plays_tab.dart'; +import '../../extensions/date_time_extensions.dart'; import '../../models/hive/board_game_details.dart'; import '../../models/hive/playthrough.dart'; import '../../models/hive/score.dart'; @@ -23,7 +25,6 @@ import '../../stores/playthroughs_store.dart'; import '../../stores/scores_store.dart'; import 'board_game_playthrough.dart'; import 'game_spinner_filters.dart'; -import 'grouped_board_game_playthroughs.dart'; import 'plays_page_visual_states.dart'; part 'plays_view_model.g.dart'; @@ -82,41 +83,31 @@ abstract class _PlaysViewModel with Store { .toList(); @computed - List get finishedBoardGamePlaythroughs { - final result = []; - final finishedPlaythroughsGrouped = groupBy( + List get historicalPlaythroughs { + final result = []; + final playthroughsGrouped = groupBy( finishedPlaythroughs ..sort((playthroughA, playthroughB) => playthroughB.endDate!.compareTo(playthroughA.endDate!)), - (Playthrough playthrough) => playthroughGroupingDateFormat.format(playthrough.endDate!)); + (Playthrough playthrough) => historicalPlaythroughDateFormat.format(playthrough.endDate!)); - for (final playthroughsEntry in finishedPlaythroughsGrouped.entries) { + for (final playthroughsEntry in playthroughsGrouped.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(), + HistoricalPlaythrough.withDateHeader( + playedOn: historicalPlaythroughDateFormat.parse(playthroughsEntry.key), + boardGamePlaythroughs: _mapToBoardGamePlaythrough(playthroughsEntry.value.first), ), ); + + if (playthroughsEntry.value.length <= 1) { + continue; + } + + for (final playthrough in playthroughsEntry.value.skip(1)) { + result.add(HistoricalPlaythrough.withoutDateHeader( + boardGamePlaythroughs: _mapToBoardGamePlaythrough(playthrough), + )); + } } return result; @@ -186,7 +177,7 @@ abstract class _PlaysViewModel with Store { void setSelectTab(PlaysTab selectedTab) { switch (selectedTab) { case PlaysTab.history: - visualState = PlaysPageVisualState.history(PlaysTab.history, finishedBoardGamePlaythroughs); + visualState = PlaysPageVisualState.history(PlaysTab.history, historicalPlaythroughs); break; case PlaysTab.statistics: @@ -263,7 +254,8 @@ abstract class _PlaysViewModel with Store { _setupGameSpinnerFilters(); visualState = PlaysPageVisualState.history( PlaysTab.history, - finishedBoardGamePlaythroughs, + historicalPlaythroughs, + // finishedBoardGamePlaythroughs.take(2).toList(), ); } @@ -279,4 +271,25 @@ abstract class _PlaysViewModel with Store { collections: filterCollections, ); } + + BoardGamePlaythrough _mapToBoardGamePlaythrough(Playthrough playthrough) { + return 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]!, + ); + } } diff --git a/board_games_companion/lib/pages/plays/plays_view_model.g.dart b/board_games_companion/lib/pages/plays/plays_view_model.g.dart index 5a044a84..b7fbdd19 100644 --- a/board_games_companion/lib/pages/plays/plays_view_model.g.dart +++ b/board_games_companion/lib/pages/plays/plays_view_model.g.dart @@ -24,15 +24,14 @@ mixin _$PlaysViewModel on _PlaysViewModel, Store { () => super.finishedPlaythroughs, name: '_PlaysViewModel.finishedPlaythroughs')) .value; - Computed>? - _$finishedBoardGamePlaythroughsComputed; + Computed>? _$historicalPlaythroughsComputed; @override - List get finishedBoardGamePlaythroughs => - (_$finishedBoardGamePlaythroughsComputed ??= - Computed>( - () => super.finishedBoardGamePlaythroughs, - name: '_PlaysViewModel.finishedBoardGamePlaythroughs')) + List get historicalPlaythroughs => + (_$historicalPlaythroughsComputed ??= + Computed>( + () => super.historicalPlaythroughs, + name: '_PlaysViewModel.historicalPlaythroughs')) .value; Computed? _$hasAnyFinishedPlaythroughsComputed; @@ -221,7 +220,7 @@ futureLoadGamesPlaythroughs: ${futureLoadGamesPlaythroughs}, visualState: ${visualState}, gameSpinnerFilters: ${gameSpinnerFilters}, finishedPlaythroughs: ${finishedPlaythroughs}, -finishedBoardGamePlaythroughs: ${finishedBoardGamePlaythroughs}, +historicalPlaythroughs: ${historicalPlaythroughs}, hasAnyFinishedPlaythroughs: ${hasAnyFinishedPlaythroughs}, hasAnyBoardGames: ${hasAnyBoardGames}, hasAnyBoardGamesToShuffle: ${hasAnyBoardGamesToShuffle}, diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.dart b/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.dart new file mode 100644 index 00000000..c2340e67 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../models/hive/playthrough_note.dart'; + +part 'playthrough_notes_state.freezed.dart'; + +@freezed +class PlaythroughNotesState with _$PlaythroughNotesState { + const factory PlaythroughNotesState.empty() = _empty; + const factory PlaythroughNotesState.notes({ + required List playthroughNotes, + }) = _notes; +} diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.freezed.dart b/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.freezed.dart new file mode 100644 index 00000000..1b109ac2 --- /dev/null +++ b/board_games_companion/lib/pages/playthroughs/playthrough_notes_state.freezed.dart @@ -0,0 +1,317 @@ +// 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, unnecessary_question_mark + +part of 'playthrough_notes_state.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 _$PlaythroughNotesState { + @optionalTypeArgs + TResult when({ + required TResult Function() empty, + required TResult Function(List playthroughNotes) notes, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? empty, + TResult? Function(List playthroughNotes)? notes, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? empty, + TResult Function(List playthroughNotes)? notes, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_empty value) empty, + required TResult Function(_notes value) notes, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_empty value)? empty, + TResult? Function(_notes value)? notes, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_empty value)? empty, + TResult Function(_notes value)? notes, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaythroughNotesStateCopyWith<$Res> { + factory $PlaythroughNotesStateCopyWith(PlaythroughNotesState value, + $Res Function(PlaythroughNotesState) then) = + _$PlaythroughNotesStateCopyWithImpl<$Res, PlaythroughNotesState>; +} + +/// @nodoc +class _$PlaythroughNotesStateCopyWithImpl<$Res, + $Val extends PlaythroughNotesState> + implements $PlaythroughNotesStateCopyWith<$Res> { + _$PlaythroughNotesStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_emptyCopyWith<$Res> { + factory _$$_emptyCopyWith(_$_empty value, $Res Function(_$_empty) then) = + __$$_emptyCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_emptyCopyWithImpl<$Res> + extends _$PlaythroughNotesStateCopyWithImpl<$Res, _$_empty> + implements _$$_emptyCopyWith<$Res> { + __$$_emptyCopyWithImpl(_$_empty _value, $Res Function(_$_empty) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_empty implements _empty { + const _$_empty(); + + @override + String toString() { + return 'PlaythroughNotesState.empty()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_empty); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() empty, + required TResult Function(List playthroughNotes) notes, + }) { + return empty(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? empty, + TResult? Function(List playthroughNotes)? notes, + }) { + return empty?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? empty, + TResult Function(List playthroughNotes)? notes, + required TResult orElse(), + }) { + if (empty != null) { + return empty(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_empty value) empty, + required TResult Function(_notes value) notes, + }) { + return empty(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_empty value)? empty, + TResult? Function(_notes value)? notes, + }) { + return empty?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_empty value)? empty, + TResult Function(_notes value)? notes, + required TResult orElse(), + }) { + if (empty != null) { + return empty(this); + } + return orElse(); + } +} + +abstract class _empty implements PlaythroughNotesState { + const factory _empty() = _$_empty; +} + +/// @nodoc +abstract class _$$_notesCopyWith<$Res> { + factory _$$_notesCopyWith(_$_notes value, $Res Function(_$_notes) then) = + __$$_notesCopyWithImpl<$Res>; + @useResult + $Res call({List playthroughNotes}); +} + +/// @nodoc +class __$$_notesCopyWithImpl<$Res> + extends _$PlaythroughNotesStateCopyWithImpl<$Res, _$_notes> + implements _$$_notesCopyWith<$Res> { + __$$_notesCopyWithImpl(_$_notes _value, $Res Function(_$_notes) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? playthroughNotes = null, + }) { + return _then(_$_notes( + playthroughNotes: null == playthroughNotes + ? _value._playthroughNotes + : playthroughNotes // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_notes implements _notes { + const _$_notes({required final List playthroughNotes}) + : _playthroughNotes = playthroughNotes; + + final List _playthroughNotes; + @override + List get playthroughNotes { + if (_playthroughNotes is EqualUnmodifiableListView) + return _playthroughNotes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_playthroughNotes); + } + + @override + String toString() { + return 'PlaythroughNotesState.notes(playthroughNotes: $playthroughNotes)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_notes && + const DeepCollectionEquality() + .equals(other._playthroughNotes, _playthroughNotes)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_playthroughNotes)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_notesCopyWith<_$_notes> get copyWith => + __$$_notesCopyWithImpl<_$_notes>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() empty, + required TResult Function(List playthroughNotes) notes, + }) { + return notes(playthroughNotes); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? empty, + TResult? Function(List playthroughNotes)? notes, + }) { + return notes?.call(playthroughNotes); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? empty, + TResult Function(List playthroughNotes)? notes, + required TResult orElse(), + }) { + if (notes != null) { + return notes(playthroughNotes); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_empty value) empty, + required TResult Function(_notes value) notes, + }) { + return notes(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_empty value)? empty, + TResult? Function(_notes value)? notes, + }) { + return notes?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_empty value)? empty, + TResult Function(_notes value)? notes, + required TResult orElse(), + }) { + if (notes != null) { + return notes(this); + } + return orElse(); + } +} + +abstract class _notes implements PlaythroughNotesState { + const factory _notes( + {required final List playthroughNotes}) = _$_notes; + + List get playthroughNotes; + @JsonKey(ignore: true) + _$$_notesCopyWith<_$_notes> get copyWith => + throw _privateConstructorUsedError; +} 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 cda8d155..a5f1b229 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 @@ -1,6 +1,8 @@ import 'dart:math' as math; import 'dart:math'; +import 'package:board_games_companion/models/hive/playthrough_note.dart'; +import 'package:board_games_companion/pages/playthroughs/playthrough_notes_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -21,6 +23,7 @@ import '../../models/hive/no_score_game_result.dart'; import '../../models/hive/player.dart'; import '../../models/navigation/edit_playthrough_page_arguments.dart'; import '../../models/navigation/player_page_arguments.dart'; +import '../../models/navigation/playthough_note_page_arguments.dart'; import '../../models/player_score.dart'; import '../../models/playthroughs/playthrough_players_selection_result.dart'; import '../../widgets/common/default_icon.dart'; @@ -34,7 +37,9 @@ import '../../widgets/common/text/item_property_value_widget.dart'; import '../../widgets/player/player_avatar.dart'; import '../../widgets/playthrough/calendar_card.dart'; import '../../widgets/playthrough/cooperative_game_result_segmented_button.dart'; +import '../../widgets/playthrough/playthrough_note_list_item.dart'; import '../edit_playthrough/edit_playthrough_page.dart'; +import '../edit_playthrough/playthrough_note_page.dart'; import '../enter_score/enter_score_view_model.dart'; import '../player/player_page.dart'; import 'playthrough_players_selection_page.dart'; @@ -88,13 +93,23 @@ class PlaythroughsLogGamePageState extends State { Observer( builder: (_) { return viewModel.playthroughTimeline.maybeWhen( - inThePast: () => _DateAndDurationSection( - playthroughDate: viewModel.playthroughDate, - playthroughDuration: viewModel.playthroughDuration, - onPlaythroughTimeChanged: (playthoughDate) => - viewModel.playthroughDate = playthoughDate, - onPlaythroughDurationChanged: (playthoughDuration) => - viewModel.playthroughDuration = playthoughDuration, + inThePast: () => MultiSliver( + children: [ + _DateAndDurationSection( + playthroughDate: viewModel.playthroughDate, + playthroughDuration: viewModel.playthroughDuration, + onPlaythroughTimeChanged: (playthoughDate) => + viewModel.playthroughDate = playthoughDate, + onPlaythroughDurationChanged: (playthoughDuration) => + viewModel.playthroughDuration = playthoughDuration, + ), + _NotesSection( + noteState: viewModel.notesState, + onCreateNote: () => _createNote(), + onDeleteNote: (note) => viewModel.deleteNote(note), + onEditNote: (note) => _editNote(note), + ), + ], ), orElse: () => const SliverToBoxAdapter(child: SizedBox.shrink()), ); @@ -220,6 +235,32 @@ class PlaythroughsLogGamePageState extends State { ), ); } + + Future _createNote() async { + final PlaythroughNote? addedNote = await Navigator.of(context).pushNamed( + PlaythroughNotePage.pageRoute, + arguments: const PlaythroughNotePageArguments(), + ); + + if (addedNote == null) { + return; + } + + viewModel.addNote(addedNote); + } + + Future _editNote(PlaythroughNote note) async { + final PlaythroughNote? editedNote = await Navigator.of(context).pushNamed( + PlaythroughNotePage.pageRoute, + arguments: PlaythroughNotePageArguments(note: note), + ); + + if (editedNote == null) { + return; + } + + viewModel.editNote(editedNote); + } } class _SubmitSection extends StatelessWidget { @@ -355,6 +396,62 @@ class _DateAndDurationSection extends StatelessWidget { } } +class _NotesSection extends StatelessWidget { + const _NotesSection({ + required this.noteState, + required this.onCreateNote, + required this.onEditNote, + required this.onDeleteNote, + }); + + final PlaythroughNotesState noteState; + final VoidCallback onCreateNote; + final void Function(PlaythroughNote) onEditNote; + final void Function(PlaythroughNote) onDeleteNote; + + @override + Widget build(BuildContext context) { + return MultiSliver( + children: [ + SliverPersistentHeader( + delegate: BgcSliverTitleHeaderDelegate.action( + primaryTitle: AppText.playthroughsLogNotesSectionTitle, + action: IconButton( + icon: const Icon(Icons.add), + onPressed: () => onCreateNote(), + ), + ), + ), + noteState.when( + notes: (notes) => SliverPadding( + padding: const EdgeInsets.symmetric(vertical: Dimensions.standardSpacing), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (_, index) { + final note = notes[index]; + return PlaythroughNoteListItem( + note: note, + onTap: onEditNote, + onDelete: onDeleteNote, + ); + }, + childCount: notes.length, + ), + ), + ), + empty: () => const Padding( + padding: EdgeInsets.all(Dimensions.standardSpacing), + child: Text( + AppText.editPlaythroughNoNotesText, + style: AppTheme.defaultTextFieldStyle, + ), + ), + ), + ], + ); + } +} + class _SetPlaythroughDuration extends StatelessWidget { const _SetPlaythroughDuration({ required this.playthroughDuration, 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 4baee358..0236b8a2 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,5 +1,6 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/models/hive/playthrough_note.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import 'package:uuid/uuid.dart'; @@ -15,6 +16,7 @@ import '../../models/playthroughs/playthrough_details.dart'; import '../../services/analytics_service.dart'; import '../../stores/game_playthroughs_details_store.dart'; import '../../stores/players_store.dart'; +import 'playthrough_notes_state.dart'; import 'playthrough_timeline.dart'; import 'playthroughs_log_game_players.dart'; @@ -41,6 +43,9 @@ abstract class _PlaythroughsLogGameViewModel with Store { @observable Duration playthroughDuration = const Duration(); + @observable + PlaythroughNotesState notesState = const PlaythroughNotesState.empty(); + @observable ObservableFuture? futureLoadPlayers; @@ -78,6 +83,10 @@ abstract class _PlaythroughsLogGameViewModel with Store { selectedPlayerScores, playthroughTimeline.when(now: () => DateTime.now(), inThePast: () => playthroughDate), playthroughTimeline.when(now: () => null, inThePast: () => playthroughDuration), + notes: notesState.maybeWhen( + notes: (playthroughNotes) => playthroughNotes, + orElse: () => null, + ), ); await _analyticsService.logEvent( @@ -97,6 +106,7 @@ abstract class _PlaythroughsLogGameViewModel with Store { playthroughDate = DateTime.now(); playthroughDuration = const Duration(); cooperativeGameResult = null; + notesState = const PlaythroughNotesState.empty(); if (_playersStore.players.isEmpty) { playersState = const PlaythroughsLogGamePlayers.noPlayers(); @@ -193,6 +203,56 @@ abstract class _PlaythroughsLogGameViewModel with Store { ); } + @action + void addNote(PlaythroughNote note) { + notesState.when( + notes: (playthroughNotes) { + notesState = PlaythroughNotesState.notes( + playthroughNotes: [...playthroughNotes, note], + ); + }, + empty: () => notesState = PlaythroughNotesState.notes(playthroughNotes: [note]), + ); + } + + @action + void editNote(PlaythroughNote note) { + notesState.maybeWhen( + notes: (playthroughNotes) { + final notes = playthroughNotes.toList(); + final noteToUpdateIndex = notes.indexWhere((n) => n.id == note.id); + if (noteToUpdateIndex == null) { + return; + } + + notes[noteToUpdateIndex] = note; + + notesState = PlaythroughNotesState.notes( + playthroughNotes: notes, + ); + }, + orElse: () {}, + ); + } + + @action + void deleteNote(PlaythroughNote note) { + notesState.maybeWhen( + notes: (playthroughNotes) { + final notes = playthroughNotes.toList(); + notes.remove(note); + if (notes.isEmpty) { + notesState = const PlaythroughNotesState.empty(); + } else { + notesState = PlaythroughNotesState.notes( + playthroughNotes: notes, + ); + } + }, + orElse: () {}, + ); + } + @action void setPlaythroughTimeline(PlaythroughTimeline playthroughTimeline) => this.playthroughTimeline = playthroughTimeline; diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.g.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.g.dart index 6ea71d2c..34b22602 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.g.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_log_game_view_model.g.dart @@ -71,6 +71,22 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { }); } + late final _$notesStateAtom = + Atom(name: '_PlaythroughsLogGameViewModel.notesState', context: context); + + @override + PlaythroughNotesState get notesState { + _$notesStateAtom.reportRead(); + return super.notesState; + } + + @override + set notesState(PlaythroughNotesState value) { + _$notesStateAtom.reportWrite(value, super.notesState, () { + super.notesState = value; + }); + } + late final _$futureLoadPlayersAtom = Atom( name: '_PlaythroughsLogGameViewModel.futureLoadPlayers', context: context); @@ -197,6 +213,39 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { } } + @override + void addNote(PlaythroughNote note) { + final _$actionInfo = _$_PlaythroughsLogGameViewModelActionController + .startAction(name: '_PlaythroughsLogGameViewModel.addNote'); + try { + return super.addNote(note); + } finally { + _$_PlaythroughsLogGameViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void editNote(PlaythroughNote note) { + final _$actionInfo = _$_PlaythroughsLogGameViewModelActionController + .startAction(name: '_PlaythroughsLogGameViewModel.editNote'); + try { + return super.editNote(note); + } finally { + _$_PlaythroughsLogGameViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void deleteNote(PlaythroughNote note) { + final _$actionInfo = _$_PlaythroughsLogGameViewModelActionController + .startAction(name: '_PlaythroughsLogGameViewModel.deleteNote'); + try { + return super.deleteNote(note); + } finally { + _$_PlaythroughsLogGameViewModelActionController.endAction(_$actionInfo); + } + } + @override void setPlaythroughTimeline(PlaythroughTimeline playthroughTimeline) { final _$actionInfo = @@ -214,6 +263,7 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { return ''' playthroughDate: ${playthroughDate}, playthroughDuration: ${playthroughDuration}, +notesState: ${notesState}, futureLoadPlayers: ${futureLoadPlayers}, cooperativeGameResult: ${cooperativeGameResult}, playthroughTimeline: ${playthroughTimeline}, diff --git a/board_games_companion/lib/services/playthroughs_service.dart b/board_games_companion/lib/services/playthroughs_service.dart index 6ca3761d..7d5d418b 100644 --- a/board_games_companion/lib/services/playthroughs_service.dart +++ b/board_games_companion/lib/services/playthroughs_service.dart @@ -3,6 +3,7 @@ import 'package:injectable/injectable.dart'; import '../common/enums/playthrough_status.dart'; import '../models/hive/playthrough.dart'; +import '../models/hive/playthrough_note.dart'; import '../models/hive/score.dart'; import '../models/player_score.dart'; import 'hive_base_service.dart'; @@ -54,6 +55,7 @@ class PlaythroughService extends BaseHiveService? notes, }) async { if ((boardGameId.isEmpty) || (playerIds.isEmpty)) { return null; @@ -70,6 +72,7 @@ class PlaythroughService extends BaseHiveService[], startDate: startDate, bggPlayId: bggPlayId, + notes: notes, ); if (duration == null) { diff --git a/board_games_companion/lib/stores/game_playthroughs_details_store.dart b/board_games_companion/lib/stores/game_playthroughs_details_store.dart index 865feb79..7420d1e6 100644 --- a/board_games_companion/lib/stores/game_playthroughs_details_store.dart +++ b/board_games_companion/lib/stores/game_playthroughs_details_store.dart @@ -10,6 +10,7 @@ import '../common/enums/game_family.dart'; import '../models/hive/board_game_details.dart'; import '../models/hive/player.dart'; import '../models/hive/playthrough.dart'; +import '../models/hive/playthrough_note.dart'; import '../models/hive/score.dart'; import '../models/player_score.dart'; import '../models/playthroughs/playthrough_details.dart'; @@ -105,6 +106,7 @@ abstract class _GamePlaythroughsDetailsStore with Store { DateTime startDate, Duration? duration, { int? bggPlayId, + List? notes, }) async { final newPlaythrough = await _playthroughsStore.createPlaythrough( boardGameId, @@ -113,6 +115,7 @@ abstract class _GamePlaythroughsDetailsStore with Store { startDate, duration, bggPlayId: bggPlayId, + notes: notes, ); if (newPlaythrough == null) { diff --git a/board_games_companion/lib/stores/playthroughs_store.dart b/board_games_companion/lib/stores/playthroughs_store.dart index 57ac61d4..3ae28426 100644 --- a/board_games_companion/lib/stores/playthroughs_store.dart +++ b/board_games_companion/lib/stores/playthroughs_store.dart @@ -1,5 +1,6 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/models/hive/playthrough_note.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; @@ -48,6 +49,7 @@ abstract class _PlaythroughsStore with Store { DateTime startDate, Duration? duration, { int? bggPlayId, + List? notes, }) async { final newPlaythrough = await _playthroughService.createPlaythrough( boardGameId, @@ -56,6 +58,7 @@ abstract class _PlaythroughsStore with Store { startDate, duration, bggPlayId: bggPlayId, + notes: notes, ); if (newPlaythrough == null) { diff --git a/board_games_companion/lib/widgets/common/page_container.dart b/board_games_companion/lib/widgets/common/page_container.dart index 7d7e6d24..51c029f1 100644 --- a/board_games_companion/lib/widgets/common/page_container.dart +++ b/board_games_companion/lib/widgets/common/page_container.dart @@ -5,16 +5,18 @@ import '../../common/app_colors.dart'; class PageContainer extends StatelessWidget { const PageContainer({ required this.child, + this.borderRadius, Key? key, }) : super(key: key); final Widget child; + final BorderRadius? borderRadius; @override Widget build(BuildContext context) { return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( + decoration: BoxDecoration( + gradient: const LinearGradient( begin: Alignment(0.5, 0), end: Alignment(0.5, 1), colors: [ @@ -23,6 +25,7 @@ class PageContainer extends StatelessWidget { ], tileMode: TileMode.clamp, ), + borderRadius: borderRadius, ), child: child, ); diff --git a/board_games_companion/lib/widgets/common/section_header.dart b/board_games_companion/lib/widgets/common/section_header.dart index 6199df9d..d0870ece 100644 --- a/board_games_companion/lib/widgets/common/section_header.dart +++ b/board_games_companion/lib/widgets/common/section_header.dart @@ -5,21 +5,22 @@ import '../../common/app_theme.dart'; import '../../common/dimensions.dart'; class SectionHeader extends StatelessWidget { - const SectionHeader({ + const SectionHeader._({ Key? key, required this.primaryWidget, this.secondaryWidget, this.height = Dimensions.sectionHeaderHeight, this.padding = const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + this.borderRadius, }) : super(key: key); factory SectionHeader.title({ - required String primaryTitle, + required String title, double height = Dimensions.sectionHeaderHeight, }) => - SectionHeader( + SectionHeader._( primaryWidget: Text( - primaryTitle, + title, style: AppTheme.titleTextStyle, overflow: TextOverflow.ellipsis, ), @@ -31,7 +32,7 @@ class SectionHeader extends StatelessWidget { required String secondaryTitle, double height = Dimensions.sectionHeaderHeight, }) => - SectionHeader( + SectionHeader._( primaryWidget: Text( primaryTitle, style: AppTheme.titleTextStyle, @@ -45,14 +46,14 @@ class SectionHeader extends StatelessWidget { height: height, ); - factory SectionHeader.customAction({ - required String primaryTitle, + factory SectionHeader.titleWithAction({ + required String title, required Widget action, double height = Dimensions.sectionHeaderHeight, }) => - SectionHeader( + SectionHeader._( primaryWidget: Text( - primaryTitle, + title, style: AppTheme.titleTextStyle, overflow: TextOverflow.ellipsis, ), @@ -61,16 +62,41 @@ class SectionHeader extends StatelessWidget { padding: const EdgeInsets.only(left: Dimensions.standardSpacing), ); + factory SectionHeader.titleWithIcon({ + required String title, + required Widget icon, + BorderRadius? borderRadius, + double height = Dimensions.sectionHeaderHeight, + }) => + SectionHeader._( + primaryWidget: Row( + children: [ + icon, + const SizedBox(width: Dimensions.standardSpacing), + Text( + title, + style: AppTheme.titleTextStyle, + overflow: TextOverflow.ellipsis, + ), + ], + ), + height: height, + padding: const EdgeInsets.only(left: Dimensions.standardSpacing), + borderRadius: borderRadius, + ); + final Widget primaryWidget; final Widget? secondaryWidget; final double height; final EdgeInsets padding; + final BorderRadius? borderRadius; @override Widget build(BuildContext context) { return Material( elevation: Dimensions.defaultElevation, color: AppColors.primaryColor, + borderRadius: borderRadius, child: SizedBox( height: height, child: Padding( diff --git a/board_games_companion/lib/widgets/common/slivers/bgc_sliver_title_header_delegate.dart b/board_games_companion/lib/widgets/common/slivers/bgc_sliver_title_header_delegate.dart index c75570b8..f24e78e7 100644 --- a/board_games_companion/lib/widgets/common/slivers/bgc_sliver_title_header_delegate.dart +++ b/board_games_companion/lib/widgets/common/slivers/bgc_sliver_title_header_delegate.dart @@ -37,11 +37,11 @@ class BgcSliverTitleHeaderDelegate extends SliverPersistentHeaderDelegate { @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { if (action != null) { - return SectionHeader.customAction(primaryTitle: primaryTitle, action: action!); + return SectionHeader.titleWithAction(title: primaryTitle, action: action!); } if (secondaryTitle.isNullOrBlank) { - return SectionHeader.title(height: _size, primaryTitle: primaryTitle); + return SectionHeader.title(height: _size, title: primaryTitle); } return SectionHeader.titles( diff --git a/board_games_companion/lib/widgets/playthrough/playthrough_note_list_item.dart b/board_games_companion/lib/widgets/playthrough/playthrough_note_list_item.dart new file mode 100644 index 00000000..4e26a3fb --- /dev/null +++ b/board_games_companion/lib/widgets/playthrough/playthrough_note_list_item.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +import '../../common/app_colors.dart'; +import '../../common/dimensions.dart'; +import '../../models/hive/playthrough_note.dart'; + +class PlaythroughNoteListItem extends StatelessWidget { + const PlaythroughNoteListItem({ + super.key, + required this.note, + required this.onTap, + required this.onDelete, + }); + + final PlaythroughNote note; + final void Function(PlaythroughNote note) onTap; + final void Function(PlaythroughNote note) onDelete; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onTap(note), + child: Slidable( + endActionPane: ActionPane( + extentRatio: 0.25, + motion: const ScrollMotion(), + children: [ + SlidableAction( + icon: Icons.delete, + onPressed: (_) => onDelete(note), + backgroundColor: AppColors.redColor, + ), + ], + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(Dimensions.standardSpacing), + child: Text(note.text, textAlign: TextAlign.justify), + ) + ], + ), + ), + ), + ); + } +}