diff --git a/board_games_companion/lib/app.dart b/board_games_companion/lib/app.dart index e02459c3..6c529dba 100644 --- a/board_games_companion/lib/app.dart +++ b/board_games_companion/lib/app.dart @@ -1,3 +1,4 @@ +import 'package:board_games_companion/pages/games/games_view_model.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,7 @@ import 'services/analytics_service.dart'; import 'services/board_games_geek_service.dart'; import 'services/preferences_service.dart'; import 'services/rate_and_review_service.dart'; +import 'stores/board_games_filters_store.dart'; import 'stores/board_games_store.dart'; import 'stores/playthroughs_store.dart'; import 'utilities/analytics_route_observer.dart'; @@ -59,10 +61,18 @@ class _BoardGamesCompanionAppState extends State { final analyticsService = getIt(); final rateAndReviewService = getIt(); final playersViewModel = getIt(); + final _boardGamesFiltersStore = getIt(); + + final _boardGamesStore = Provider.of( + context, + listen: false, + ); + final gamesViewModel = GamesViewModel(_boardGamesStore, _boardGamesFiltersStore); return HomePage( analyticsService: analyticsService, rateAndReviewService: rateAndReviewService, + gamesViewModel: gamesViewModel, playersViewModel: playersViewModel, ); }, diff --git a/board_games_companion/lib/common/app_text.dart b/board_games_companion/lib/common/app_text.dart index 610c8662..a1f09225 100644 --- a/board_games_companion/lib/common/app_text.dart +++ b/board_games_companion/lib/common/app_text.dart @@ -38,6 +38,10 @@ class AppText { static const searchBoardGamesPageHotBoardGamesErrorPartOne = '''Sorry, we couldn't retrieve hot board games at this time. Please check your Internet connectivity and try again.'''; static const searchBoardGamesPageHotBoardGamesErrorRetryButtonText = 'Retry'; + static const hotBoardGamesSliverSectionTitle = 'Hot Board Games'; + + static const gamesPageMainGamesSliverSectionTitleFormat = 'Main Games (%s)'; + static const gamesPageExpansionsSliverSectionTitleFormat = '%s Expansions (%s)'; static const playtimeDurationHoursFormat = '%ih %imin'; static const playtimeDurationDaysFormat = '%i day%s %ih'; diff --git a/board_games_companion/lib/injectable.config.dart b/board_games_companion/lib/injectable.config.dart index c7269a86..c1d8daf0 100644 --- a/board_games_companion/lib/injectable.config.dart +++ b/board_games_companion/lib/injectable.config.dart @@ -13,16 +13,17 @@ import 'pages/players/players_view_model.dart' as _i18; import 'pages/playthroughs/playthroughs_view_model.dart' as _i4; import 'services/analytics_service.dart' as _i8; import 'services/board_games_filters_service.dart' as _i14; -import 'services/board_games_geek_service.dart' as _i22; +import 'services/board_games_geek_service.dart' as _i23; import 'services/board_games_service.dart' as _i9; import 'services/file_service.dart' as _i15; -import 'services/injectable_register_module.dart' as _i24; +import 'services/injectable_register_module.dart' as _i25; import 'services/player_service.dart' as _i12; -import 'services/playthroughs_service.dart' as _i23; +import 'services/playthroughs_service.dart' as _i24; import 'services/preferences_service.dart' as _i19; import 'services/rate_and_review_service.dart' as _i20; import 'services/score_service.dart' as _i13; import 'services/user_service.dart' as _i21; +import 'stores/board_games_filters_store.dart' as _i22; import 'stores/players_store.dart' as _i7; import 'stores/playthrough_statistics_store.dart' as _i6; import 'stores/playthrough_store.dart' as _i11; @@ -66,22 +67,24 @@ _i1.GetIt $initGetIt(_i1.GetIt get, gh.singleton<_i21.UserService>(_i21.UserService()); gh.singleton<_i8.AnalyticsService>(_i8.AnalyticsService( get<_i16.FirebaseAnalytics>(), get<_i20.RateAndReviewService>())); - gh.singleton<_i22.BoardGamesGeekService>( - _i22.BoardGamesGeekService(get<_i3.CustomHttpClientAdapter>())); + gh.singleton<_i22.BoardGamesFiltersStore>(_i22.BoardGamesFiltersStore( + get<_i14.BoardGamesFiltersService>(), get<_i8.AnalyticsService>())); + gh.singleton<_i23.BoardGamesGeekService>( + _i23.BoardGamesGeekService(get<_i3.CustomHttpClientAdapter>())); gh.singleton<_i9.BoardGamesService>(_i9.BoardGamesService( - get<_i22.BoardGamesGeekService>(), get<_i19.PreferencesService>())); - gh.singleton<_i23.PlaythroughService>( - _i23.PlaythroughService(get<_i13.ScoreService>())); + get<_i23.BoardGamesGeekService>(), get<_i19.PreferencesService>())); + gh.singleton<_i24.PlaythroughService>( + _i24.PlaythroughService(get<_i13.ScoreService>())); gh.singleton<_i6.PlaythroughStatisticsStore>(_i6.PlaythroughStatisticsStore( get<_i12.PlayerService>(), get<_i13.ScoreService>(), - get<_i23.PlaythroughService>())); + get<_i24.PlaythroughService>())); gh.singleton<_i5.PlaythroughsStore>( - _i5.PlaythroughsStore(get<_i23.PlaythroughService>())); + _i5.PlaythroughsStore(get<_i24.PlaythroughService>())); return get; } -class _$RegisterModule extends _i24.RegisterModule { +class _$RegisterModule extends _i25.RegisterModule { @override _i16.FirebaseAnalytics get firebaseAnalytics => _i16.FirebaseAnalytics(); } diff --git a/board_games_companion/lib/main.dart b/board_games_companion/lib/main.dart index 70821c0a..e8a9ca3c 100644 --- a/board_games_companion/lib/main.dart +++ b/board_games_companion/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:board_games_companion/pages/games/games_view_model.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; @@ -29,7 +30,6 @@ import 'models/hive/user.dart'; import 'models/sort_by.dart'; import 'pages/players/players_view_model.dart'; import 'services/analytics_service.dart'; -import 'services/board_games_filters_service.dart'; import 'services/board_games_geek_service.dart'; import 'services/board_games_service.dart'; import 'services/player_service.dart'; @@ -148,33 +148,20 @@ class App extends StatelessWidget { create: (context) => getIt(), ), ChangeNotifierProvider( - create: (context) { - final BoardGamesFiltersService boardGamesFiltersService = - getIt(); - final AnalyticsService analyticsService = getIt(); - return BoardGamesFiltersStore(boardGamesFiltersService, analyticsService); - }, + create: (context) => getIt(), ), - ChangeNotifierProxyProvider( + ChangeNotifierProvider( create: (context) { final BoardGamesService boardGamesService = getIt(); final PlaythroughService playthroughService = getIt(); final ScoreService scoreService = getIt(); final PlayerService playerService = getIt(); - final boardGamesStore = BoardGamesStore( + return BoardGamesStore( boardGamesService, playthroughService, scoreService, playerService, - Provider.of(context, listen: false), ); - - boardGamesStore.loadBoardGames(); - return boardGamesStore; - }, - update: (_, filtersStore, boardGamesStore) { - boardGamesStore!.applyFilters(); - return boardGamesStore; }, ), ChangeNotifierProxyProvider2( + create: (context) { + final boardGamesFiltersStore = getIt(); + final boardGamesStore = Provider.of( + context, + listen: false, + ); + final gamesViewModel = GamesViewModel(boardGamesStore, boardGamesFiltersStore); + gamesViewModel.loadBoardGames(); + + return gamesViewModel; + }, + update: (_, filtersStore, boardGamesStore, gamesViewModel) { + gamesViewModel!.applyFilters(); + return gamesViewModel; + }, + ), ], child: const BoardGamesCompanionApp(), ); diff --git a/board_games_companion/lib/mixins/import_collection.dart b/board_games_companion/lib/mixins/import_collection.dart index a3efcb07..3a50af4c 100644 --- a/board_games_companion/lib/mixins/import_collection.dart +++ b/board_games_companion/lib/mixins/import_collection.dart @@ -35,6 +35,8 @@ mixin ImportCollection { } void _showSuccessSnackBar(BuildContext context) { + // TODO MK Consider using a "global context" to show a snackbar in case a user switches between pages + // https://stackoverflow.com/a/65607336/510627 ScaffoldMessenger.of(context).showSnackBar( const SnackBar( margin: Dimensions.snackbarMargin, 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 b54b3f88..81ef73dc 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 @@ -10,7 +10,6 @@ import '../../common/enums/collection_type.dart'; import '../../common/styles.dart'; import '../../models/hive/board_game_details.dart'; import '../../services/preferences_service.dart'; -import '../../stores/board_game_details_in_collection_store.dart'; import '../../stores/board_games_store.dart'; import '../../utilities/launcher_helper.dart'; import '../../widgets/board_games/board_game_image.dart'; @@ -66,7 +65,7 @@ class _BoardGamesDetailsPageState extends BasePageState { child: CustomScrollView( slivers: [ _Header( - boardGameDetailsStore: widget.boardGameDetailsStore, + viewModel: widget.boardGameDetailsStore, boardGameName: widget.boardGameName, ), SliverPadding( @@ -93,12 +92,8 @@ class _BoardGamesDetailsPageState extends BasePageState { context, listen: false, ); - final boardGameDetailsInCollectionStore = BoardGameDetailsInCollectionStore( - boardGamesStore, - widget.boardGameDetailsStore.boardGameDetails, - ); - if (!boardGameDetailsInCollectionStore.isInCollection && + if (!boardGamesStore.isInAnyCollection(widget.boardGameDetailsStore.boardGameDetails?.id) && widget.navigatingFromType == PlaythroughsPage) { Navigator.popUntil(context, ModalRoute.withName(HomePage.pageRoute)); return false; @@ -111,14 +106,14 @@ class _BoardGamesDetailsPageState extends BasePageState { class _Header extends StatelessWidget { const _Header({ Key? key, - required BoardGameDetailsViewModel boardGameDetailsStore, + required BoardGameDetailsViewModel viewModel, required String boardGameName, - }) : _boardGameDetailsStore = boardGameDetailsStore, + }) : _viewModel = viewModel, _boardGameName = boardGameName, super(key: key); final String _boardGameName; - final BoardGameDetailsViewModel _boardGameDetailsStore; + final BoardGameDetailsViewModel _viewModel; @override Widget build(BuildContext context) { @@ -152,12 +147,12 @@ class _Header extends StatelessWidget { ), ), background: ChangeNotifierProvider.value( - value: _boardGameDetailsStore, + value: _viewModel, child: Consumer( builder: (_, store, __) { // TODO Add shadow to the image return BoardGameImage( - _boardGameDetailsStore.boardGameDetails, + _viewModel.boardGameDetails, minImageHeight: Constants.BoardGameDetailsImageHeight, ); }, diff --git a/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart b/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart index 78ea17b0..3de1e5dc 100644 --- a/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart +++ b/board_games_companion/lib/pages/board_game_details/board_game_details_view_model.dart @@ -33,7 +33,7 @@ class BoardGameDetailsViewModel with ChangeNotifier { return _boardGameDetails!; } for (final boardGameExpansion in boardGameDetails.expansions) { - final boardGameExpansionDetails = _boardGamesStore.allboardGames.firstWhereOrNull( + final boardGameExpansionDetails = _boardGamesStore.allBoardGames.firstWhereOrNull( (boardGame) => boardGame.id == boardGameExpansion.id, ); 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 2b19c151..4143d19f 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 @@ -479,21 +479,15 @@ class _ActionButtons extends StatelessWidget { if (!viewModel.playthoughEnded) ...[ ElevatedIconButton( title: AppText.stop, - icon: const DefaultIcon( - Icons.stop, - ), + icon: const DefaultIcon(Icons.stop), color: AppTheme.blueColor, onPressed: onStop, ), - const SizedBox( - width: Dimensions.standardSpacing, - ), + const SizedBox(width: Dimensions.standardSpacing), ], ElevatedIconButton( title: 'Save', - icon: const DefaultIcon( - Icons.save, - ), + icon: const DefaultIcon(Icons.save), color: AppTheme.accentColor, onPressed: onSave, ), diff --git a/board_games_companion/lib/pages/games/games_filter_panel.dart b/board_games_companion/lib/pages/games/games_filter_panel.dart index bcf96741..828e48de 100644 --- a/board_games_companion/lib/pages/games/games_filter_panel.dart +++ b/board_games_companion/lib/pages/games/games_filter_panel.dart @@ -210,7 +210,7 @@ class _Filters extends StatelessWidget { alignment: Alignment.centerLeft, child: Text('Number of players', style: AppTheme.sectionHeaderTextStyle), ), - if (boardGamesStore.allboardGames.isNotEmpty) + if (boardGamesStore.allBoardGames.isNotEmpty) _FilterNumberOfPlayersSlider( boardGamesFiltersStore: boardGamesFiltersStore, boardGamesStore: boardGamesStore, @@ -270,13 +270,13 @@ class _FilterNumberOfPlayersSlider extends StatelessWidget { @override Widget build(BuildContext context) { final minNumberOfPlayers = max( - boardGamesStore.allboardGames + boardGamesStore.allBoardGames .where((boardGameDetails) => boardGameDetails.minPlayers != null) .map((boardGameDetails) => boardGameDetails.minPlayers!) .reduce(min), Constants.minNumberOfPlayers); final maxNumberOfPlayers = min( - boardGamesStore.allboardGames + boardGamesStore.allBoardGames .where((boardGameDetails) => boardGameDetails.maxPlayers != null) .map((boardGameDetails) => boardGameDetails.maxPlayers!) .reduce(max), diff --git a/board_games_companion/lib/pages/games/games_page.dart b/board_games_companion/lib/pages/games/games_page.dart index e91438f9..95ae68b1 100644 --- a/board_games_companion/lib/pages/games/games_page.dart +++ b/board_games_companion/lib/pages/games/games_page.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'package:board_games_companion/common/app_text.dart'; +import 'package:board_games_companion/pages/games/games_view_model.dart'; +import 'package:board_games_companion/widgets/common/slivers/bgc_sliver_header_delegate.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:sprintf/sprintf.dart'; import '../../common/analytics.dart'; import '../../common/animation_tags.dart'; @@ -17,7 +20,6 @@ import '../../models/navigation/playthroughs_page_arguments.dart'; import '../../services/analytics_service.dart'; import '../../services/rate_and_review_service.dart'; import '../../stores/board_games_filters_store.dart'; -import '../../stores/board_games_store.dart'; import '../../stores/user_store.dart'; import '../../widgets/board_games/board_game_tile.dart'; import '../../widgets/common/bgg_community_member_text_widget.dart'; @@ -32,14 +34,14 @@ import 'games_filter_panel.dart'; class GamesPage extends StatefulWidget { const GamesPage( - this.boardGamesStore, + this.viewModel, this.userStore, this.analyticsService, this.rateAndReviewService, { Key? key, }) : super(key: key); - final BoardGamesStore boardGamesStore; + final GamesViewModel viewModel; final UserStore userStore; final AnalyticsService analyticsService; final RateAndReviewService rateAndReviewService; @@ -56,26 +58,26 @@ class _GamesPageState extends State with SingleTickerProviderStateMix _topTabController = TabController( length: 3, vsync: this, - initialIndex: widget.boardGamesStore.selectedTab.index, + initialIndex: widget.viewModel.selectedTab.index, ); super.initState(); } @override Widget build(BuildContext context) { - if (widget.boardGamesStore.loadDataState == LoadDataState.loaded) { - if (!widget.boardGamesStore.anyBoardGamesInCollections && + if (widget.viewModel.loadDataState == LoadDataState.loaded) { + if (!widget.viewModel.anyBoardGamesInCollections && (widget.userStore.user?.name.isEmpty ?? true)) { return const _Empty(); } return _Collection( - boardGamesStore: widget.boardGamesStore, + viewModel: widget.viewModel, topTabController: _topTabController, analyticsService: widget.analyticsService, rateAndReviewService: widget.rateAndReviewService, ); - } else if (widget.boardGamesStore.loadDataState == LoadDataState.error) { + } else if (widget.viewModel.loadDataState == LoadDataState.error) { return const Center( child: GenericErrorMessage(), ); @@ -93,14 +95,14 @@ class _GamesPageState extends State with SingleTickerProviderStateMix class _Collection extends StatelessWidget { const _Collection({ - required this.boardGamesStore, + required this.viewModel, required this.topTabController, required this.analyticsService, required this.rateAndReviewService, Key? key, }) : super(key: key); - final BoardGamesStore boardGamesStore; + final GamesViewModel viewModel; final TabController topTabController; final AnalyticsService analyticsService; final RateAndReviewService rateAndReviewService; @@ -112,45 +114,53 @@ class _Collection extends StatelessWidget { child: CustomScrollView( slivers: [ _AppBar( - boardGamesStore: boardGamesStore, + viewModel: viewModel, topTabController: topTabController, analyticsService: analyticsService, rateAndReviewService: rateAndReviewService, updateSearchResults: (String searchPhrase) => _updateSearchResults(searchPhrase), ), - Builder( - builder: (_) { - final List boardGames = []; - switch (boardGamesStore.selectedTab) { - case GamesTab.Owned: - boardGames.addAll(boardGamesStore.filteredBoardGamesOwned); - break; - case GamesTab.Friends: - boardGames.addAll(boardGamesStore.filteredBoardGamesFriends); - break; - case GamesTab.Wishlist: - boardGames.addAll(boardGamesStore.filteredBoardGamesOnWishlist); - break; - } - - if (boardGames.isEmpty) { - if (boardGamesStore.searchPhrase?.isNotEmpty ?? false) { - return _EmptySearchResult( - boardGamesStore: boardGamesStore, - onClearSearch: () => _updateSearchResults(''), - ); - } - - return _EmptyCollection(boardGamesStore: boardGamesStore); - } - - return _Grid( - boardGames: boardGames, - collectionType: boardGamesStore.selectedTab.toCollectionType(), + if (viewModel.collectionSate == CollectionState.emptySearchResult) + _EmptySearchResult( + viewModel: viewModel, + onClearSearch: () => _updateSearchResults(''), + ), + if (viewModel.collectionSate == CollectionState.emptyCollection) + _EmptyCollection(gamesViewModel: viewModel), + if (viewModel.collectionSate == CollectionState.collection) ...[ + if (viewModel.hasAnyMainGameInSelectedCollection) ...[ + SliverPersistentHeader( + delegate: BgcSliverHeaderDelegate( + title: sprintf( + AppText.gamesPageMainGamesSliverSectionTitleFormat, + [viewModel.totalMainGamesInCollections], + ), + ), + ), + _Grid( + boardGames: viewModel.mainGamesInCollections, + collectionType: viewModel.selectedTab.toCollectionType(), analyticsService: analyticsService, - ); - }, - ), + ), + ], + if (viewModel.hasAnyExpansionsInSelectedCollection) ...[ + for (var expansionsMapEntry in viewModel.expansionGroupedByMainGame.entries) ...[ + SliverPersistentHeader( + delegate: BgcSliverHeaderDelegate( + title: sprintf( + AppText.gamesPageExpansionsSliverSectionTitleFormat, + [expansionsMapEntry.key.name, expansionsMapEntry.value.length], + ), + ), + ), + _Grid( + boardGames: expansionsMapEntry.value, + collectionType: viewModel.selectedTab.toCollectionType(), + analyticsService: analyticsService, + ), + ] + ] + ], const SliverPadding(padding: EdgeInsets.all(8.0)), ], ), @@ -158,7 +168,7 @@ class _Collection extends StatelessWidget { } Future _updateSearchResults(String searchPhrase) async { - boardGamesStore.updateSearchResults(searchPhrase); + viewModel.updateSearchResults(searchPhrase); await rateAndReviewService.increaseNumberOfSignificantActions(); } @@ -166,7 +176,7 @@ class _Collection extends StatelessWidget { class _AppBar extends StatefulWidget { const _AppBar({ - required this.boardGamesStore, + required this.viewModel, required this.topTabController, required this.analyticsService, required this.rateAndReviewService, @@ -174,7 +184,7 @@ class _AppBar extends StatefulWidget { Key? key, }) : super(key: key); - final BoardGamesStore boardGamesStore; + final GamesViewModel viewModel; final TabController topTabController; final AnalyticsService analyticsService; final RateAndReviewService rateAndReviewService; @@ -202,13 +212,14 @@ class _AppBarState extends State<_AppBar> { @override Widget build(BuildContext context) { - if (widget.boardGamesStore.searchPhrase?.isEmpty ?? true) { + if (widget.viewModel.searchPhrase?.isEmpty ?? true) { _searchController.text = ''; } return SliverAppBar( - pinned: true, + pinned: false, floating: true, + elevation: 0, titleSpacing: Dimensions.standardSpacing, foregroundColor: AppTheme.accentColor, title: TextField( @@ -220,7 +231,7 @@ class _AppBarState extends State<_AppBar> { style: AppTheme.defaultTextFieldStyle, decoration: InputDecoration( hintText: 'Search for a game...', - suffixIcon: (widget.boardGamesStore.searchPhrase?.isNotEmpty ?? false) + suffixIcon: (widget.viewModel.searchPhrase?.isNotEmpty ?? false) ? IconButton( icon: const Icon( Icons.clear, @@ -238,7 +249,7 @@ class _AppBarState extends State<_AppBar> { ), onSubmitted: (searchPhrase) async { _debounce?.cancel(); - if (widget.boardGamesStore.searchPhrase != _searchController.text) { + if (widget.viewModel.searchPhrase != _searchController.text) { await widget.updateSearchResults(_searchController.text); } _searchFocusNode.unfocus(); @@ -251,7 +262,7 @@ class _AppBarState extends State<_AppBar> { icon: boardGamesFiltersStore.anyFiltersApplied ? const Icon(Icons.filter_alt_rounded, color: AppTheme.accentColor) : const Icon(Icons.filter_alt_outlined, color: AppTheme.accentColor), - onPressed: widget.boardGamesStore.allboardGames.isNotEmpty + onPressed: widget.viewModel.anyBoardGames ? () async { await _openFiltersPanel(context); await widget.analyticsService.logEvent(name: Analytics.FilterCollection); @@ -266,24 +277,24 @@ class _AppBarState extends State<_AppBar> { preferredSize: const Size.fromHeight(74), child: TabBar( onTap: (int index) { - widget.boardGamesStore.selectedTab = index.toGamesTab(); + widget.viewModel.selectedTab = index.toGamesTab(); }, controller: widget.topTabController, tabs: [ _TopTab( 'Owned', Icons.grid_on, - isSelected: widget.boardGamesStore.selectedTab == GamesTab.Owned, + isSelected: widget.viewModel.selectedTab == GamesTab.Owned, ), _TopTab( 'Friends', Icons.group, - isSelected: widget.boardGamesStore.selectedTab == GamesTab.Friends, + isSelected: widget.viewModel.selectedTab == GamesTab.Friends, ), _TopTab( 'Wishlist', Icons.card_giftcard, - isSelected: widget.boardGamesStore.selectedTab == GamesTab.Wishlist, + isSelected: widget.viewModel.selectedTab == GamesTab.Wishlist, ), ], indicatorColor: AppTheme.accentColor, @@ -352,24 +363,20 @@ class _Grid extends StatelessWidget { crossAxisSpacing: Dimensions.standardSpacing, mainAxisSpacing: Dimensions.standardSpacing, maxCrossAxisExtent: Dimensions.boardGameItemCollectionImageWidth, - children: List.generate( - boardGames.length, - (int index) { - final boardGame = boardGames[index]; - - return BoardGameTile( + children: [ + for (var boardGame in boardGames) + BoardGameTile( boardGame: boardGame, onTap: () async { await Navigator.pushNamed( context, PlaythroughsPage.pageRoute, - arguments: PlaythroughsPageArguments(boardGames[index], collectionType), + arguments: PlaythroughsPageArguments(boardGame, collectionType), ); }, heroTag: '${AnimationTags.boardGamePlaythroughImageHeroTag}_$collectionType', - ); - }, - ), + ) + ], ), ); } @@ -485,12 +492,12 @@ class _ImportDataFromBggSectionState extends State<_ImportDataFromBggSection> { class _EmptySearchResult extends StatelessWidget { const _EmptySearchResult({ - required this.boardGamesStore, + required this.viewModel, required this.onClearSearch, Key? key, }) : super(key: key); - final BoardGamesStore boardGamesStore; + final GamesViewModel viewModel; final VoidCallback onClearSearch; @override @@ -511,7 +518,7 @@ class _EmptySearchResult extends StatelessWidget { '''It looks like you don't have any board games in your collection that match the search phrase ''', ), TextSpan( - text: boardGamesStore.searchPhrase, + text: viewModel.searchPhrase, style: const TextStyle( fontWeight: FontWeight.bold, ), @@ -540,10 +547,10 @@ class _EmptySearchResult extends StatelessWidget { class _EmptyCollection extends StatelessWidget { const _EmptyCollection({ Key? key, - required this.boardGamesStore, + required this.gamesViewModel, }) : super(key: key); - final BoardGamesStore boardGamesStore; + final GamesViewModel gamesViewModel; @override Widget build(BuildContext context) { @@ -567,13 +574,13 @@ class _EmptyCollection extends StatelessWidget { text: "It looks like you don't have any board games in your ", ), TextSpan( - text: boardGamesStore.selectedTab + text: gamesViewModel.selectedTab .toCollectionType() .toHumandReadableText(), style: const TextStyle(fontWeight: FontWeight.bold), ), const TextSpan(text: ' collection yet.'), - if (boardGamesStore.selectedTab == GamesTab.Wishlist && + if (gamesViewModel.selectedTab == GamesTab.Wishlist && (userStore.user?.name.isNotEmpty ?? false)) ...[ const TextSpan(text: "\n\nIf you want to see board games from BGG's "), const TextSpan( @@ -596,7 +603,7 @@ class _EmptyCollection extends StatelessWidget { ), textAlign: TextAlign.justify, ), - if (boardGamesStore.selectedTab == GamesTab.Wishlist && + if (gamesViewModel.selectedTab == GamesTab.Wishlist && (userStore.user?.name.isNotEmpty ?? false)) ...[ const SizedBox(height: Dimensions.doubleStandardSpacing), Align( diff --git a/board_games_companion/lib/pages/games/games_view_model.dart b/board_games_companion/lib/pages/games/games_view_model.dart new file mode 100644 index 00000000..345e0d24 --- /dev/null +++ b/board_games_companion/lib/pages/games/games_view_model.dart @@ -0,0 +1,218 @@ +import 'package:board_games_companion/common/enums/order_by.dart'; +import 'package:board_games_companion/common/enums/sort_by_option.dart'; +import 'package:board_games_companion/stores/board_games_filters_store.dart'; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/material.dart'; + +import '../../common/enums/enums.dart'; +import '../../common/enums/games_tab.dart'; +import '../../extensions/date_time_extensions.dart'; +import '../../extensions/double_extensions.dart'; +import '../../extensions/int_extensions.dart'; +import '../../extensions/string_extensions.dart'; +import '../../models/hive/board_game_details.dart'; +import '../../stores/board_games_store.dart'; + +enum CollectionState { + emptySearchResult, + emptyCollection, + collection, +} + +class GamesViewModel with ChangeNotifier { + GamesViewModel( + this._boardGamesStore, + this._boardGamesFiltersStore, + ); + + final BoardGamesStore _boardGamesStore; + final BoardGamesFiltersStore _boardGamesFiltersStore; + + String? _searchPhrase; + + final Map _mainBoardGameByExpansionId = {}; + Map _filteredBoardGames = {}; + + bool get anyBoardGamesInCollections => _boardGamesStore.allBoardGames + .any((boardGame) => boardGame.isOwned! || boardGame.isOnWishlist! || boardGame.isFriends!); + + bool get anyBoardGames => _boardGamesStore.allBoardGames.isNotEmpty; + + LoadDataState _loadDataState = LoadDataState.none; + LoadDataState get loadDataState => _loadDataState; + + CollectionState get collectionSate { + if (!anyBoardGamesInSelectedCollection) { + if (!isSearchPhraseEmpty) { + return CollectionState.emptySearchResult; + } + + return CollectionState.emptyCollection; + } + + return CollectionState.collection; + } + + GamesTab _selectedTab = GamesTab.Owned; + + String? get searchPhrase => _searchPhrase; + + bool get isSearchPhraseEmpty => _searchPhrase?.isEmpty ?? true; + + List get boardGamesInSelectedCollection { + switch (selectedTab) { + case GamesTab.Owned: + return _filteredBoardGames.values.where((boardGame) => boardGame.isOwned!).toList(); + case GamesTab.Friends: + return _filteredBoardGames.values.where((boardGame) => boardGame.isFriends!).toList(); + case GamesTab.Wishlist: + return _filteredBoardGames.values.where((boardGame) => boardGame.isOnWishlist!).toList(); + } + } + + bool get anyBoardGamesInSelectedCollection => boardGamesInSelectedCollection.isNotEmpty; + + List get mainGamesInCollections => boardGamesInSelectedCollection + .where((boardGame) => !(boardGame.isExpansion ?? false)) + .toList(); + + bool get hasAnyMainGameInSelectedCollection => mainGamesInCollections.isNotEmpty; + + int get totalMainGamesInCollections => mainGamesInCollections.length; + + List get _expansionsInSelectedCollection => + boardGamesInSelectedCollection.where((boardGame) => boardGame.isExpansion ?? false).toList(); + + int get totalExpansionsInCollections => _expansionsInSelectedCollection.length; + + bool get hasAnyExpansionsInSelectedCollection => _expansionsInSelectedCollection.isNotEmpty; + + Map> get expansionGroupedByMainGame { + final Map> expansionsGrouped = {}; + for (final expansion in _expansionsInSelectedCollection) { + final mainGame = _mainBoardGameByExpansionId[expansion.id]; + if (mainGame == null) { + continue; + } + + if (!expansionsGrouped.containsKey(mainGame)) { + expansionsGrouped[mainGame] = []; + } + + expansionsGrouped[mainGame]!.add(expansion); + } + + return expansionsGrouped; + } + + GamesTab get selectedTab => _selectedTab; + set selectedTab(GamesTab value) { + if (_selectedTab != value) { + _selectedTab = value; + notifyListeners(); + } + } + + Future loadBoardGames() async { + notifyListeners(); + _loadDataState = LoadDataState.loading; + try { + await _boardGamesStore.loadBoardGames(); + for (final boardGameDetails in _boardGamesStore.allBoardGames) { + _filteredBoardGames[boardGameDetails.id] = boardGameDetails; + for (final boardGameExpansion in boardGameDetails.expansions) { + _mainBoardGameByExpansionId[boardGameExpansion.id] = boardGameDetails; + } + } + + await _boardGamesFiltersStore.loadFilterPreferences(); + } catch (e, stack) { + FirebaseCrashlytics.instance.recordError(e, stack); + _loadDataState = LoadDataState.error; + } + + _loadDataState = LoadDataState.loaded; + notifyListeners(); + } + + void updateSearchResults(String searchPhrase) { + if (searchPhrase.isEmpty == true && _searchPhrase?.isEmpty == true) { + return; + } + + _searchPhrase = searchPhrase; + + if (searchPhrase.isEmpty) { + applyFilters(); + return; + } + + final searchPhraseLowerCase = searchPhrase.toLowerCase(); + + _filteredBoardGames = { + for (var boardGameDetails in _boardGamesStore.allBoardGames.where((boardGameDetails) => + boardGameDetails.name.toLowerCase().contains(searchPhraseLowerCase))) + boardGameDetails.id: boardGameDetails + }; + + notifyListeners(); + } + + void applyFilters() { + final selectedSortBy = _boardGamesFiltersStore.sortBy.firstWhereOrNull( + (sb) => sb.selected, + ); + + _filteredBoardGames = { + for (var boardGameDetails in _boardGamesStore.allBoardGames.where((boardGame) => + (_boardGamesFiltersStore.filterByRating == null || + boardGame.rating! >= _boardGamesFiltersStore.filterByRating!) && + (_boardGamesFiltersStore.numberOfPlayers == null || + (boardGame.maxPlayers != null && + boardGame.minPlayers != null && + (boardGame.maxPlayers! >= _boardGamesFiltersStore.numberOfPlayers! && + boardGame.minPlayers! <= _boardGamesFiltersStore.numberOfPlayers!))))) + boardGameDetails.id: boardGameDetails + }; + + if (selectedSortBy != null) { + final sortedFilteredBoardGames = List.of(_filteredBoardGames.values); + sortedFilteredBoardGames.sort((a, b) { + if (selectedSortBy.orderBy == OrderBy.Descending) { + final buffer = a; + a = b; + b = buffer; + } + + switch (selectedSortBy.sortByOption) { + case SortByOption.Name: + return a.name.safeCompareTo(b.name); + case SortByOption.YearPublished: + return a.yearPublished.safeCompareTo(b.yearPublished); + case SortByOption.LastUpdated: + return a.lastModified.safeCompareTo(b.lastModified); + case SortByOption.Rank: + return a.rank.safeCompareTo(b.rank); + case SortByOption.NumberOfPlayers: + if (selectedSortBy.orderBy == OrderBy.Descending) { + return b.maxPlayers.safeCompareTo(a.maxPlayers); + } + + return a.minPlayers.safeCompareTo(b.maxPlayers); + case SortByOption.Playtime: + return a.maxPlaytime.safeCompareTo(b.maxPlaytime); + case SortByOption.Rating: + return b.rating.safeCompareTo(a.rating); + default: + return a.lastModified.safeCompareTo(b.lastModified); + } + }); + _filteredBoardGames = { + for (var boardGameDetails in sortedFilteredBoardGames) boardGameDetails.id: boardGameDetails + }; + } + + notifyListeners(); + } +} diff --git a/board_games_companion/lib/pages/home/home_page.dart b/board_games_companion/lib/pages/home/home_page.dart index e5f0168d..e1593271 100644 --- a/board_games_companion/lib/pages/home/home_page.dart +++ b/board_games_companion/lib/pages/home/home_page.dart @@ -1,3 +1,4 @@ +import 'package:board_games_companion/pages/games/games_view_model.dart'; import 'package:board_games_companion/pages/players/players_view_model.dart'; import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:flutter/material.dart'; @@ -7,7 +8,6 @@ import '../../common/app_theme.dart'; import '../../common/dimensions.dart'; import '../../services/analytics_service.dart'; import '../../services/rate_and_review_service.dart'; -import '../../stores/board_games_store.dart'; import '../../stores/user_store.dart'; import '../../widgets/bottom_tab_icon.dart'; import '../../widgets/common/page_container_widget.dart'; @@ -21,6 +21,7 @@ class HomePage extends StatefulWidget { const HomePage({ required this.analyticsService, required this.rateAndReviewService, + required this.gamesViewModel, required this.playersViewModel, Key? key, }) : super(key: key); @@ -29,6 +30,7 @@ class HomePage extends StatefulWidget { final AnalyticsService analyticsService; final RateAndReviewService rateAndReviewService; + final GamesViewModel gamesViewModel; final PlayersViewModel playersViewModel; static final GlobalKey homePageGlobalKey = @@ -66,10 +68,10 @@ class _HomePageState extends BasePageState with SingleTickerProviderSt child: TabBarView( controller: tabController, children: [ - Consumer2( - builder: (_, boardGamesStore, userStore, __) { + Consumer2( + builder: (_, viewModel, userStore, __) { return GamesPage( - boardGamesStore, + viewModel, userStore, widget.analyticsService, widget.rateAndReviewService, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart index 9d2230e6..490d8145 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart @@ -156,7 +156,7 @@ class _PlaythroughsPageState extends BasePageState }); await widget.viewModel.importPlays(username, boardGameId); if (widget.viewModel.bggPlaysImportRaport!.playsToImportTotal > 0) { - await showImportPlaysReportDialog( + await _showImportPlaysReportDialog( context, username, boardGameId, @@ -190,7 +190,7 @@ class _PlaythroughsPageState extends BasePageState ); } - Future showImportPlaysReportDialog( + Future _showImportPlaysReportDialog( BuildContext context, String username, String boardGameId, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart index c08d0402..800fba17 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart @@ -164,6 +164,10 @@ class PlaythroughsViewModel with ChangeNotifier, BoardGameAware { continue; } + // TODO MK Players should be loaded by the time this method is called. + // This is to fix a bug in production that casues duplicate players to be created because players are not populated. + await _playersStore.loadPlayers(); + final List playthroughPlayers = []; final Map playerScores = {}; for (final bggPlayer in bggPlay.players) { diff --git a/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart b/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart index ae7b1521..d7d2e2d7 100644 --- a/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart +++ b/board_games_companion/lib/pages/search_board_games/search_board_games_page.dart @@ -23,7 +23,7 @@ import '../../widgets/common/elevated_icon_button.dart'; import '../../widgets/common/generic_error_message_widget.dart'; import '../../widgets/common/loading_indicator_widget.dart'; import '../../widgets/common/page_container_widget.dart'; -import '../../widgets/common/ripple_effect.dart'; +import '../../widgets/common/slivers/bgc_sliver_header_delegate.dart'; import '../board_game_details/board_game_details_page.dart'; class SearchBoardGamesPage extends StatefulWidget { @@ -64,7 +64,7 @@ class _SearchBoardGamesPageState extends State { ), SliverPersistentHeader( pinned: true, - delegate: _HotBoardGamesHeader(), + delegate: BgcSliverHeaderDelegate(title: AppText.hotBoardGamesSliverSectionTitle), ), _HotBoardGames( analyticsService: widget.analyticsService, @@ -213,13 +213,14 @@ class _SearchResultsState extends State<_SearchResults> { (_, index) { final int itemIndex = index ~/ 2; if (index.isEven) { - return Container( - decoration: BoxDecoration( - color: AppTheme.primaryColor, + return Material( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.circular(Styles.defaultCornerRadius), + elevation: 4, + child: InkWell( borderRadius: BorderRadius.circular(Styles.defaultCornerRadius), - boxShadow: const [AppTheme.defaultBoxShadow], - ), - child: RippleEffect( + onTap: () => + _navigateToBoardGameDetails(searchResults!, itemIndex, context), child: Padding( padding: const EdgeInsets.all(Dimensions.standardSpacing), child: Column( @@ -235,14 +236,10 @@ class _SearchResultsState extends State<_SearchResults> { searchResults[itemIndex].yearPublished.toString(), style: AppTheme.subTitleTextStyle, ), - const SizedBox( - height: Dimensions.halfStandardSpacing, - ), + const SizedBox(height: Dimensions.halfStandardSpacing), ], ), ), - onTap: () async => - _navigateToBoardGameDetails(searchResults, itemIndex, context), ), ); } @@ -513,33 +510,3 @@ class _HotBoardGames extends StatelessWidget { onBoardGameTapped(boardGame); } } - -class _HotBoardGamesHeader extends SliverPersistentHeaderDelegate { - @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - return Container( - color: AppTheme.primaryColor, - padding: const EdgeInsets.all( - Dimensions.standardSpacing, - ), - child: const Align( - alignment: Alignment.centerLeft, - child: Text( - 'Hot Board Games', - style: AppTheme.titleTextStyle, - ), - ), - ); - } - - @override - double get maxExtent => 50; - - @override - double get minExtent => 50; - - @override - bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { - return true; - } -} diff --git a/board_games_companion/lib/stores/board_game_details_in_collection_store.dart b/board_games_companion/lib/stores/board_game_details_in_collection_store.dart deleted file mode 100644 index 1129545c..00000000 --- a/board_games_companion/lib/stores/board_game_details_in_collection_store.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/cupertino.dart'; - -import '../models/hive/board_game_details.dart'; -import 'board_games_store.dart'; - -class BoardGameDetailsInCollectionStore extends ChangeNotifier { - BoardGameDetailsInCollectionStore( - this._boardGamesStore, - this._boardGameDetails, - ); - - final BoardGamesStore _boardGamesStore; - BoardGameDetails? _boardGameDetails; - - bool get isInCollection { - if ((_boardGamesStore.filteredBoardGames?.isEmpty ?? true) || _boardGameDetails == null) { - return false; - } - - final boardGameInCollection = _boardGamesStore.filteredBoardGames!.firstWhereOrNull( - (boardGameDetails) => boardGameDetails.id == _boardGameDetails!.id, - ); - return boardGameInCollection != null; - } - - void updateIsInCollectionStatus([BoardGameDetails? boardGameDetails]) { - if (boardGameDetails != null) { - _boardGameDetails = boardGameDetails; - } - notifyListeners(); - } -} diff --git a/board_games_companion/lib/stores/board_games_filters_store.dart b/board_games_companion/lib/stores/board_games_filters_store.dart index 42398059..d3991aa1 100644 --- a/board_games_companion/lib/stores/board_games_filters_store.dart +++ b/board_games_companion/lib/stores/board_games_filters_store.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; import '../common/analytics.dart'; import '../common/constants.dart'; @@ -11,6 +12,7 @@ import '../models/sort_by.dart'; import '../services/analytics_service.dart'; import '../services/board_games_filters_service.dart'; +@singleton class BoardGamesFiltersStore with ChangeNotifier { BoardGamesFiltersStore( this._boardGamesFiltersService, diff --git a/board_games_companion/lib/stores/board_games_store.dart b/board_games_companion/lib/stores/board_games_store.dart index 86e9bf2b..e22882eb 100644 --- a/board_games_companion/lib/stores/board_games_store.dart +++ b/board_games_companion/lib/stores/board_games_store.dart @@ -1,26 +1,17 @@ -import 'package:board_games_companion/models/import_result.dart'; +import 'package:basics/basics.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; -import '../common/enums/enums.dart'; -import '../common/enums/games_tab.dart'; -import '../common/enums/order_by.dart'; -import '../common/enums/sort_by_option.dart'; import '../common/hive_boxes.dart'; -import '../extensions/date_time_extensions.dart'; -import '../extensions/double_extensions.dart'; -import '../extensions/int_extensions.dart'; -import '../extensions/string_extensions.dart'; import '../models/collection_import_result.dart'; -import '../models/hive/board_game_category.dart'; import '../models/hive/board_game_details.dart'; import '../models/hive/board_game_expansion.dart'; +import '../models/import_result.dart'; import '../services/board_games_service.dart'; import '../services/player_service.dart'; import '../services/playthroughs_service.dart'; import '../services/score_service.dart'; -import 'board_games_filters_store.dart'; class BoardGamesStore with ChangeNotifier { BoardGamesStore( @@ -28,61 +19,30 @@ class BoardGamesStore with ChangeNotifier { this._playthroughService, this._scoreService, this._playerService, - this._boardGamesFiltersStore, ); final BoardGamesService _boardGamesService; final PlaythroughService _playthroughService; final ScoreService _scoreService; final PlayerService _playerService; - final BoardGamesFiltersStore _boardGamesFiltersStore; - String? _searchPhrase; List _allBoardGames = []; - List _filteredBoardGames = []; - LoadDataState _loadDataState = LoadDataState.none; - GamesTab _selectedTab = GamesTab.Owned; + List get allBoardGames => _allBoardGames; - LoadDataState get loadDataState => _loadDataState; - List? get filteredBoardGames => _filteredBoardGames; - List get filteredBoardGamesOwned => - _filteredBoardGames.where((boardGame) => boardGame.isOwned!).toList(); - List get filteredBoardGamesOnWishlist => - _filteredBoardGames.where((boardGame) => boardGame.isOnWishlist!).toList(); - List get filteredBoardGamesFriends => - _filteredBoardGames.where((boardGame) => boardGame.isFriends!).toList(); - - // MK All board games in collection - List get allboardGames => _allBoardGames; - bool get anyBoardGamesInCollections => _allBoardGames - .any((boardGame) => boardGame.isOwned! || boardGame.isOnWishlist! || boardGame.isFriends!); - String? get searchPhrase => _searchPhrase; + bool isInAnyCollection(String? boardGameId) { + if (boardGameId?.isBlank ?? true) { + return false; + } - List get filteredBoardGamesCategories { - final allBoardGameCategories = filteredBoardGames! - .map((boardGame) => boardGame.categories) - .expand((categories) => categories!) - .toList(); - final uniqueBoardGameCategories = allBoardGameCategories.map((category) => category.id).toSet(); - allBoardGameCategories.retainWhere((category) => uniqueBoardGameCategories.remove(category.id)); - return allBoardGameCategories; + return _allBoardGames.any((boardGameDetails) => + boardGameDetails.id == boardGameId && + (boardGameDetails.isFriends! || + boardGameDetails.isOnWishlist! || + boardGameDetails.isOwned!)); } Future loadBoardGames() async { - _loadDataState = LoadDataState.loading; - notifyListeners(); - - try { - _allBoardGames = await _boardGamesService.retrieveBoardGames(); - _filteredBoardGames = List.of(_allBoardGames); - await _boardGamesFiltersStore.loadFilterPreferences(); - } catch (e, stack) { - FirebaseCrashlytics.instance.recordError(e, stack); - _loadDataState = LoadDataState.error; - } - - _loadDataState = LoadDataState.loaded; - notifyListeners(); + _allBoardGames = await _boardGamesService.retrieveBoardGames(); } Future addOrUpdateBoardGame(BoardGameDetails boardGameDetails) async { @@ -96,7 +56,6 @@ class BoardGamesStore with ChangeNotifier { final existingBoardGameDetails = retrieveBoardGame(boardGameDetails.id); if (existingBoardGameDetails == null) { _allBoardGames.add(boardGameDetails); - _filteredBoardGames.add(boardGameDetails); } else { existingBoardGameDetails.imageUrl = boardGameDetails.imageUrl; existingBoardGameDetails.name = boardGameDetails.name; @@ -136,7 +95,7 @@ class BoardGamesStore with ChangeNotifier { // MK If updating an expansion, update IsInCollection flag for the parent board game if (boardGameDetails.isExpansion!) { - final BoardGamesExpansion? parentBoardGameExpansion = allboardGames + final BoardGamesExpansion? parentBoardGameExpansion = allBoardGames .expand((BoardGameDetails boardGameDetails) => boardGameDetails.expansions) .firstWhereOrNull( (BoardGamesExpansion boardGameExpansion) => @@ -159,7 +118,7 @@ class BoardGamesStore with ChangeNotifier { } BoardGameDetails? retrieveBoardGame(String boardGameId) { - return allboardGames.firstWhereOrNull( + return allBoardGames.firstWhereOrNull( (BoardGameDetails boardGameDetails) => boardGameDetails.id == boardGameId, ); } @@ -181,7 +140,6 @@ class BoardGamesStore with ChangeNotifier { } _allBoardGames.remove(boardGameToRemove); - _filteredBoardGames.remove(boardGameToRemove); notifyListeners(); } @@ -195,7 +153,6 @@ class BoardGamesStore with ChangeNotifier { await _boardGamesService.removeBoardGames(bggSyncedBoardGames); await _playthroughService.deletePlaythroughsForGames(bggSyncedBoardGames); - _filteredBoardGames.removeWhere((boardGame) => boardGame.isBggSynced!); _allBoardGames.removeWhere((boardGame) => boardGame.isBggSynced!); notifyListeners(); @@ -212,91 +169,15 @@ class BoardGamesStore with ChangeNotifier { importResult = await _boardGamesService.importCollections(username); if (importResult.isSuccess) { _allBoardGames = await _boardGamesService.retrieveBoardGames(); - _filteredBoardGames = List.of(_allBoardGames); } } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); importResult = CollectionImportResult.failure([ImportError.exception(e, stack)]); } - _loadDataState = LoadDataState.loaded; - applyFilters(); - - return importResult; - } - - void updateSearchResults(String searchPhrase) { - if (searchPhrase.isEmpty == true && _searchPhrase?.isEmpty == true) { - return; - } - - _searchPhrase = searchPhrase; - - if (searchPhrase.isEmpty) { - applyFilters(); - return; - } - - final searchPhraseLowerCase = searchPhrase.toLowerCase(); - - _filteredBoardGames = List.of(_allBoardGames - .where((boardGameDetails) => - boardGameDetails.name.toLowerCase().contains(searchPhraseLowerCase)) - .toList()); - notifyListeners(); - } - - void applyFilters() { - final selectedSortBy = _boardGamesFiltersStore.sortBy.firstWhereOrNull( - (sb) => sb.selected, - ); - - _filteredBoardGames = _allBoardGames - .where((boardGame) => - (_boardGamesFiltersStore.filterByRating == null || - boardGame.rating! >= _boardGamesFiltersStore.filterByRating!) && - (_boardGamesFiltersStore.numberOfPlayers == null || - (boardGame.maxPlayers != null && - boardGame.minPlayers != null && - (boardGame.maxPlayers! >= _boardGamesFiltersStore.numberOfPlayers! && - boardGame.minPlayers! <= _boardGamesFiltersStore.numberOfPlayers!)))) - .toList(); - - if (selectedSortBy != null) { - filteredBoardGames?.sort((a, b) { - if (selectedSortBy.orderBy == OrderBy.Descending) { - final buffer = a; - a = b; - b = buffer; - } - switch (selectedSortBy.sortByOption) { - case SortByOption.Name: - return a.name.safeCompareTo(b.name); - case SortByOption.YearPublished: - return a.yearPublished.safeCompareTo(b.yearPublished); - case SortByOption.LastUpdated: - return a.lastModified.safeCompareTo(b.lastModified); - case SortByOption.Rank: - return a.rank.safeCompareTo(b.rank); - case SortByOption.NumberOfPlayers: - if (selectedSortBy.orderBy == OrderBy.Descending) { - return b.maxPlayers.safeCompareTo(a.maxPlayers); - } - - return a.minPlayers.safeCompareTo(b.maxPlayers); - case SortByOption.Playtime: - return a.maxPlaytime.safeCompareTo(b.maxPlaytime); - case SortByOption.Rating: - return b.rating.safeCompareTo(a.rating); - default: - return a.lastModified.safeCompareTo(b.lastModified); - } - }); - } - - notifyListeners(); + return importResult; } @override @@ -308,12 +189,4 @@ class BoardGamesStore with ChangeNotifier { super.dispose(); } - - GamesTab get selectedTab => _selectedTab; - set selectedTab(GamesTab value) { - if (_selectedTab != value) { - _selectedTab = value; - notifyListeners(); - } - } } diff --git a/board_games_companion/lib/widgets/common/ripple_effect.dart b/board_games_companion/lib/widgets/common/ripple_effect.dart index 3646b7dd..4793610c 100644 --- a/board_games_companion/lib/widgets/common/ripple_effect.dart +++ b/board_games_companion/lib/widgets/common/ripple_effect.dart @@ -11,6 +11,7 @@ class RippleEffect extends StatelessWidget { this.splashColor, this.highlightColor = Colors.transparent, this.borderRadius, + this.elevation, Key? key, }) : super(key: key); @@ -20,12 +21,14 @@ class RippleEffect extends StatelessWidget { final Color? splashColor; final Color highlightColor; final BorderRadius? borderRadius; + final double? elevation; @override Widget build(BuildContext context) { return Material( color: backgroundColor, borderRadius: borderRadius, + elevation: elevation ?? 0, child: InkWell( highlightColor: highlightColor, splashColor: splashColor ?? AppTheme.accentColor.withAlpha(Styles.opacity70Percent), diff --git a/board_games_companion/lib/widgets/common/slivers/bgc_sliver_header_delegate.dart b/board_games_companion/lib/widgets/common/slivers/bgc_sliver_header_delegate.dart new file mode 100644 index 00000000..cd6e1553 --- /dev/null +++ b/board_games_companion/lib/widgets/common/slivers/bgc_sliver_header_delegate.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../../../common/app_theme.dart'; +import '../../../common/dimensions.dart'; + +class BgcSliverHeaderDelegate extends SliverPersistentHeaderDelegate { + BgcSliverHeaderDelegate({ + required this.title, + }) : super(); + + String title; + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + return Material( + elevation: 4, + child: Container( + color: AppTheme.primaryColor, + padding: const EdgeInsets.all(Dimensions.standardSpacing), + child: Align( + alignment: Alignment.centerLeft, + child: Text(title, style: AppTheme.titleTextStyle, overflow: TextOverflow.ellipsis), + ), + ), + ); + } + + @override + double get maxExtent => 50; + + @override + double get minExtent => 50; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { + return true; + } +}