diff --git a/board_games_companion/android/app/src/main/AndroidManifest.xml b/board_games_companion/android/app/src/main/AndroidManifest.xml index b68f154f..c3397fa4 100644 --- a/board_games_companion/android/app/src/main/AndroidManifest.xml +++ b/board_games_companion/android/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ - + diff --git a/board_games_companion/ios/Podfile.lock b/board_games_companion/ios/Podfile.lock index 29c85b39..afbda5b5 100644 --- a/board_games_companion/ios/Podfile.lock +++ b/board_games_companion/ios/Podfile.lock @@ -1,4 +1,38 @@ PODS: + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Firebase/Analytics (9.2.0): - Firebase/Core - Firebase/Core (9.2.0): @@ -123,13 +157,20 @@ PODS: - path_provider_ios (0.0.1): - Flutter - PromisesObjC (2.1.1) + - SDWebImage (5.13.2): + - SDWebImage/Core (= 5.13.2) + - SDWebImage/Core (5.13.2) + - share_plus (0.0.1): + - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - SwiftyGif (5.4.3) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: + - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -138,11 +179,14 @@ DEPENDENCIES: - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: + - DKImagePickerController + - DKPhotoGallery - Firebase - FirebaseAnalytics - FirebaseCore @@ -156,8 +200,12 @@ SPEC REPOS: - GoogleUtilities - nanopb - PromisesObjC + - SDWebImage + - SwiftyGif EXTERNAL SOURCES: + file_picker: + :path: ".symlinks/plugins/file_picker/ios" firebase_analytics: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: @@ -174,12 +222,17 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 Firebase: 4ba896cb8e5105d4b9e247e1c1b6222b548df55a firebase_analytics: 4e55c4e99e2b914449eb890c13897194a8c0b5f0 firebase_core: ada8be870601fe3c2684dae2356f634189bd598f @@ -201,7 +254,10 @@ SPEC CHECKSUMS: package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb + SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de PODFILE CHECKSUM: b259ee2cff80e9e247a66f9310d20a4efd32c397 diff --git a/board_games_companion/ios/Runner.xcodeproj/project.pbxproj b/board_games_companion/ios/Runner.xcodeproj/project.pbxproj index 93c360e1..3c1d0db7 100644 --- a/board_games_companion/ios/Runner.xcodeproj/project.pbxproj +++ b/board_games_companion/ios/Runner.xcodeproj/project.pbxproj @@ -221,6 +221,8 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", + "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCoreDiagnostics/FirebaseCoreDiagnostics.framework", @@ -230,16 +232,22 @@ "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", + "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", "${BUILT_PRODUCTS_DIR}/in_app_review/in_app_review.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", + "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreDiagnostics.framework", @@ -249,11 +257,15 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_review.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", ); diff --git a/board_games_companion/lib/app.dart b/board_games_companion/lib/app.dart index 1335ecdb..a0dd38df 100644 --- a/board_games_companion/lib/app.dart +++ b/board_games_companion/lib/app.dart @@ -1,4 +1,5 @@ import 'package:board_games_companion/pages/games/games_view_model.dart'; +import 'package:board_games_companion/pages/settings/settings_view_model.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:flutter/material.dart'; @@ -24,7 +25,6 @@ import 'services/analytics_service.dart'; import 'services/preferences_service.dart'; import 'services/rate_and_review_service.dart'; import 'stores/board_games_filters_store.dart'; -import 'stores/playthroughs_store.dart'; import 'utilities/analytics_route_observer.dart'; class BoardGamesCompanionApp extends StatefulWidget { @@ -116,25 +116,26 @@ class BoardGamesCompanionAppState extends State { builder: (BuildContext context) => PlaythroughsPage(viewModel: viewModel), ); - case EditPlaythoughPage.pageRoute: + case EditPlaythroughPage.pageRoute: final arguments = routeSettings.arguments as EditPlaythroughPageArguments; - - // MK Need to create view model manually (i.e. without DI) because the passed in playthroughViewModel - // needs to be exactly the same as the one on the history page to ensure it's updated once navigated back - final viewModel = - EditPlaythoughViewModel(arguments.playthroughViewModel, getIt()); + final viewModel = getIt(); + viewModel.setPlaythroughId(arguments.playthroughId); return MaterialPageRoute( settings: routeSettings, - builder: (BuildContext context) => EditPlaythoughPage(viewModel: viewModel)); + builder: (BuildContext context) => EditPlaythroughPage(viewModel: viewModel)); case AboutPage.pageRoute: return MaterialPageRoute( settings: routeSettings, builder: (BuildContext context) => const AboutPage()); case SettingsPage.pageRoute: + final viewModel = getIt(); + return MaterialPageRoute( - settings: routeSettings, builder: (BuildContext context) => const SettingsPage()); + settings: routeSettings, + builder: (BuildContext context) => SettingsPage(viewModel: viewModel), + ); default: return null; diff --git a/board_games_companion/lib/common/app_text.dart b/board_games_companion/lib/common/app_text.dart index da91f509..eae37620 100644 --- a/board_games_companion/lib/common/app_text.dart +++ b/board_games_companion/lib/common/app_text.dart @@ -125,6 +125,13 @@ class AppText { 'Games imported from the BGG collections are missing some details (e.g. complexity or expansions). Refresh the data to get the latest information.'; static const gamesPageSearchResultExpansionsSectionTitleFormat = 'Expansions (%i)'; + static const settingsPageBackupAndRestoreSectionBody = + "Backup app's data to a *.zip archive in case you want to migrate your data to a new/other device."; + static const settingsPageBackupAndRestoreSectionTitle = 'Backup & Restore'; + static const settingsPageBackupButtonText = 'Backup'; + static const settingsPageRestireButtonText = 'Restore'; + static const settingsPageBackupsListTitle = 'Backups'; + static const gamePlaytimeFormat = '%s min'; static const gamePlayersSingularFormat = '%i players'; static const gamePlayersPluralFormat = '%i players'; diff --git a/board_games_companion/lib/common/constants.dart b/board_games_companion/lib/common/constants.dart index 98401290..8315fcfc 100644 --- a/board_games_companion/lib/common/constants.dart +++ b/board_games_companion/lib/common/constants.dart @@ -4,6 +4,8 @@ class Constants { static const shortMonthDateFormat = 'MMM'; static const shortWeekDayDateFormat = 'E'; + static const appDataBackupDateFormat = 'y-MM-dd HH:mm:ss'; + static const top100 = 100; static const double boardGameDetailsImageHeight = 300; diff --git a/board_games_companion/lib/common/hive_boxes.dart b/board_games_companion/lib/common/hive_boxes.dart index 8ae372d3..98b72eb8 100644 --- a/board_games_companion/lib/common/hive_boxes.dart +++ b/board_games_companion/lib/common/hive_boxes.dart @@ -1,10 +1,30 @@ +// ignore_for_file: avoid_classes_with_only_static_members + +import 'package:board_games_companion/services/board_games_service.dart'; +import 'package:board_games_companion/services/player_service.dart'; +import 'package:board_games_companion/services/score_service.dart'; +import 'package:board_games_companion/services/user_service.dart'; + +import '../services/board_games_filters_service.dart'; +import '../services/playthroughs_service.dart'; +import '../services/preferences_service.dart'; + class HiveBoxes { + static Map boxesNamesMap = { + BoardGamesService: boardGames, + PlayerService: players, + UserService: user, + PlaythroughService: playthroughs, + ScoreService: scores, + BoardGamesFiltersService: collectionFilters, + PreferencesService: preferences, + }; + static const boardGames = 'boardGames'; static const players = 'players'; static const user = 'user'; static const playthroughs = 'playthroughs'; static const scores = 'scores'; - static const sortBy = 'sortBy'; static const collectionFilters = 'collectionFilters'; static const preferences = 'preferences'; static const dioCache = 'dioCache'; diff --git a/board_games_companion/lib/injectable.config.dart b/board_games_companion/lib/injectable.config.dart index 39f2a18b..12e6d714 100644 --- a/board_games_companion/lib/injectable.config.dart +++ b/board_games_companion/lib/injectable.config.dart @@ -4,41 +4,44 @@ // InjectableConfigGenerator // ************************************************************************** -import 'package:firebase_analytics/firebase_analytics.dart' as _i6; -import 'package:firebase_analytics/observer.dart' as _i7; +import 'package:firebase_analytics/firebase_analytics.dart' as _i7; +import 'package:firebase_analytics/observer.dart' as _i8; import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; -import 'pages/board_game_details/board_game_details_view_model.dart' as _i32; -import 'pages/games/collection_search_result_view_model.dart' as _i24; -import 'pages/games/games_view_model.dart' as _i25; -import 'pages/players/players_view_model.dart' as _i10; -import 'pages/playthroughs/playthrough_statistics_view_model.dart' as _i26; -import 'pages/playthroughs/playthrough_view_model.dart' as _i27; -import 'pages/playthroughs/playthroughs_game_settings_view_model.dart' as _i28; -import 'pages/playthroughs/playthroughs_history_view_model.dart' as _i29; -import 'pages/playthroughs/playthroughs_log_game_view_model.dart' as _i30; -import 'pages/playthroughs/playthroughs_view_model.dart' as _i21; -import 'pages/search_board_games/search_board_games_view_model.dart' as _i31; -import 'services/analytics_service.dart' as _i15; -import 'services/board_games_filters_service.dart' as _i3; -import 'services/board_games_geek_service.dart' as _i17; -import 'services/board_games_service.dart' as _i18; -import 'services/file_service.dart' as _i5; -import 'services/injectable_register_module.dart' as _i33; -import 'services/player_service.dart' as _i8; -import 'services/playthroughs_service.dart' as _i19; -import 'services/preferences_service.dart' as _i11; -import 'services/rate_and_review_service.dart' as _i12; -import 'services/score_service.dart' as _i13; -import 'services/user_service.dart' as _i14; -import 'stores/board_games_filters_store.dart' as _i16; -import 'stores/board_games_store.dart' as _i23; -import 'stores/players_store.dart' as _i9; -import 'stores/playthroughs_store.dart' as _i20; -import 'utilities/analytics_route_observer.dart' as _i22; +import 'pages/board_game_details/board_game_details_view_model.dart' as _i35; +import 'pages/edit_playthrough/edit_playthrough_view_model.dart' as _i27; +import 'pages/games/collection_search_result_view_model.dart' as _i26; +import 'pages/games/games_view_model.dart' as _i28; +import 'pages/players/players_view_model.dart' as _i11; +import 'pages/playthroughs/playthrough_statistics_view_model.dart' as _i29; +import 'pages/playthroughs/playthroughs_game_settings_view_model.dart' as _i30; +import 'pages/playthroughs/playthroughs_history_view_model.dart' as _i31; +import 'pages/playthroughs/playthroughs_log_game_view_model.dart' as _i32; +import 'pages/playthroughs/playthroughs_view_model.dart' as _i23; +import 'pages/search_board_games/search_board_games_view_model.dart' as _i33; +import 'pages/settings/settings_view_model.dart' as _i34; +import 'services/analytics_service.dart' as _i17; +import 'services/board_games_filters_service.dart' as _i4; +import 'services/board_games_geek_service.dart' as _i19; +import 'services/board_games_service.dart' as _i20; +import 'services/file_service.dart' as _i6; +import 'services/injectable_register_module.dart' as _i36; +import 'services/player_service.dart' as _i9; +import 'services/playthroughs_service.dart' as _i21; +import 'services/preferences_service.dart' as _i12; +import 'services/rate_and_review_service.dart' as _i13; +import 'services/score_service.dart' as _i14; +import 'services/user_service.dart' as _i15; +import 'stores/app_store.dart' as _i3; +import 'stores/board_games_filters_store.dart' as _i18; +import 'stores/board_games_store.dart' as _i25; +import 'stores/players_store.dart' as _i10; +import 'stores/playthroughs_store.dart' as _i22; +import 'stores/user_store.dart' as _i16; +import 'utilities/analytics_route_observer.dart' as _i24; import 'utilities/custom_http_client_adapter.dart' - as _i4; // ignore_for_file: unnecessary_lambdas + as _i5; // ignore_for_file: unnecessary_lambdas // ignore_for_file: lines_longer_than_80_chars /// initializes the registration of provided dependencies inside of [GetIt] @@ -46,75 +49,96 @@ _i1.GetIt $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) { final gh = _i2.GetItHelper(get, environment, environmentFilter); final registerModule = _$RegisterModule(); - gh.singleton<_i3.BoardGamesFiltersService>(_i3.BoardGamesFiltersService()); - gh.factory<_i4.CustomHttpClientAdapter>(() => _i4.CustomHttpClientAdapter()); - gh.singleton<_i5.FileService>(_i5.FileService()); - gh.singleton<_i6.FirebaseAnalytics>(registerModule.firebaseAnalytics); - gh.singleton<_i7.FirebaseAnalyticsObserver>( + gh.singleton<_i3.AppStore>(_i3.AppStore()); + gh.singleton<_i4.BoardGamesFiltersService>(_i4.BoardGamesFiltersService()); + gh.factory<_i5.CustomHttpClientAdapter>(() => _i5.CustomHttpClientAdapter()); + gh.singleton<_i6.FileService>(_i6.FileService()); + gh.singleton<_i7.FirebaseAnalytics>(registerModule.firebaseAnalytics); + gh.singleton<_i8.FirebaseAnalyticsObserver>( registerModule.firebaseAnalyticsObserver); - gh.singleton<_i8.PlayerService>(_i8.PlayerService(get<_i5.FileService>())); - gh.singleton<_i9.PlayersStore>(_i9.PlayersStore(get<_i8.PlayerService>())); - gh.singleton<_i10.PlayersViewModel>( - _i10.PlayersViewModel(get<_i9.PlayersStore>())); - gh.singleton<_i11.PreferencesService>(_i11.PreferencesService()); - gh.singleton<_i12.RateAndReviewService>( - _i12.RateAndReviewService(get<_i11.PreferencesService>())); - gh.singleton<_i13.ScoreService>(_i13.ScoreService()); - gh.singleton<_i14.UserService>(_i14.UserService()); - gh.singleton<_i15.AnalyticsService>(_i15.AnalyticsService( - get<_i6.FirebaseAnalytics>(), get<_i12.RateAndReviewService>())); - gh.singleton<_i16.BoardGamesFiltersStore>(_i16.BoardGamesFiltersStore( - get<_i3.BoardGamesFiltersService>(), get<_i15.AnalyticsService>())); - gh.singleton<_i17.BoardGamesGeekService>( - _i17.BoardGamesGeekService(get<_i4.CustomHttpClientAdapter>())); - gh.singleton<_i18.BoardGamesService>(_i18.BoardGamesService( - get<_i17.BoardGamesGeekService>(), get<_i11.PreferencesService>())); - gh.singleton<_i19.PlaythroughService>( - _i19.PlaythroughService(get<_i13.ScoreService>())); - gh.singleton<_i20.PlaythroughsStore>( - _i20.PlaythroughsStore(get<_i19.PlaythroughService>())); - gh.factory<_i21.PlaythroughsViewModel>(() => _i21.PlaythroughsViewModel( - get<_i20.PlaythroughsStore>(), - get<_i9.PlayersStore>(), - get<_i15.AnalyticsService>(), - get<_i18.BoardGamesService>())); - gh.factory<_i22.AnalyticsRouteObserver>( - () => _i22.AnalyticsRouteObserver(get<_i15.AnalyticsService>())); - gh.singleton<_i23.BoardGamesStore>(_i23.BoardGamesStore( - get<_i18.BoardGamesService>(), get<_i19.PlaythroughService>())); - gh.factory<_i24.CollectionSearchResultViewModel>( - () => _i24.CollectionSearchResultViewModel(get<_i23.BoardGamesStore>())); - gh.factory<_i25.GamesViewModel>(() => _i25.GamesViewModel( - get<_i23.BoardGamesStore>(), get<_i16.BoardGamesFiltersStore>())); - gh.singleton<_i26.PlaythroughStatisticsViewModel>( - _i26.PlaythroughStatisticsViewModel( - get<_i8.PlayerService>(), - get<_i13.ScoreService>(), - get<_i19.PlaythroughService>(), - get<_i20.PlaythroughsStore>())); - gh.factory<_i27.PlaythroughViewModel>(() => _i27.PlaythroughViewModel( - get<_i8.PlayerService>(), - get<_i13.ScoreService>(), - get<_i20.PlaythroughsStore>())); - gh.factory<_i28.PlaythroughsGameSettingsViewModel>(() => - _i28.PlaythroughsGameSettingsViewModel( - get<_i23.BoardGamesStore>(), get<_i20.PlaythroughsStore>())); - gh.factory<_i29.PlaythroughsHistoryViewModel>( - () => _i29.PlaythroughsHistoryViewModel(get<_i20.PlaythroughsStore>())); - gh.factory<_i30.PlaythroughsLogGameViewModel>(() => - _i30.PlaythroughsLogGameViewModel(get<_i9.PlayersStore>(), - get<_i20.PlaythroughsStore>(), get<_i15.AnalyticsService>())); - gh.singleton<_i31.SearchBoardGamesViewModel>(_i31.SearchBoardGamesViewModel( - get<_i23.BoardGamesStore>(), - get<_i17.BoardGamesGeekService>(), - get<_i15.AnalyticsService>())); - gh.factory<_i32.BoardGameDetailsViewModel>(() => - _i32.BoardGameDetailsViewModel( - get<_i23.BoardGamesStore>(), get<_i15.AnalyticsService>())); + gh.singleton<_i9.PlayerService>(_i9.PlayerService(get<_i6.FileService>())); + gh.singleton<_i10.PlayersStore>( + _i10.PlayersStore(get<_i9.PlayerService>(), get<_i3.AppStore>())); + gh.singleton<_i11.PlayersViewModel>( + _i11.PlayersViewModel(get<_i10.PlayersStore>())); + gh.singleton<_i12.PreferencesService>(_i12.PreferencesService()); + gh.singleton<_i13.RateAndReviewService>( + _i13.RateAndReviewService(get<_i12.PreferencesService>())); + gh.singleton<_i14.ScoreService>(_i14.ScoreService()); + gh.singleton<_i15.UserService>(_i15.UserService()); + gh.singleton<_i16.UserStore>( + _i16.UserStore(get<_i15.UserService>(), get<_i3.AppStore>())); + gh.singleton<_i17.AnalyticsService>(_i17.AnalyticsService( + get<_i7.FirebaseAnalytics>(), get<_i13.RateAndReviewService>())); + gh.singleton<_i18.BoardGamesFiltersStore>(_i18.BoardGamesFiltersStore( + get<_i4.BoardGamesFiltersService>(), get<_i17.AnalyticsService>())); + gh.singleton<_i19.BoardGamesGeekService>( + _i19.BoardGamesGeekService(get<_i5.CustomHttpClientAdapter>())); + gh.singleton<_i20.BoardGamesService>( + _i20.BoardGamesService(get<_i19.BoardGamesGeekService>())); + gh.singleton<_i21.PlaythroughService>( + _i21.PlaythroughService(get<_i14.ScoreService>())); + gh.singleton<_i22.PlaythroughsStore>(_i22.PlaythroughsStore( + get<_i21.PlaythroughService>(), + get<_i14.ScoreService>(), + get<_i9.PlayerService>())); + gh.factory<_i23.PlaythroughsViewModel>(() => _i23.PlaythroughsViewModel( + get<_i22.PlaythroughsStore>(), + get<_i10.PlayersStore>(), + get<_i17.AnalyticsService>(), + get<_i20.BoardGamesService>(), + get<_i16.UserStore>())); + gh.factory<_i24.AnalyticsRouteObserver>( + () => _i24.AnalyticsRouteObserver(get<_i17.AnalyticsService>())); + gh.singleton<_i25.BoardGamesStore>(_i25.BoardGamesStore( + get<_i20.BoardGamesService>(), + get<_i21.PlaythroughService>(), + get<_i3.AppStore>())); + gh.factory<_i26.CollectionSearchResultViewModel>( + () => _i26.CollectionSearchResultViewModel(get<_i25.BoardGamesStore>())); + gh.factory<_i27.EditPlaythoughViewModel>( + () => _i27.EditPlaythoughViewModel(get<_i22.PlaythroughsStore>())); + gh.factory<_i28.GamesViewModel>(() => _i28.GamesViewModel( + get<_i16.UserStore>(), + get<_i25.BoardGamesStore>(), + get<_i18.BoardGamesFiltersStore>())); + gh.singleton<_i29.PlaythroughStatisticsViewModel>( + _i29.PlaythroughStatisticsViewModel( + get<_i9.PlayerService>(), + get<_i14.ScoreService>(), + get<_i21.PlaythroughService>(), + get<_i22.PlaythroughsStore>())); + gh.factory<_i30.PlaythroughsGameSettingsViewModel>(() => + _i30.PlaythroughsGameSettingsViewModel( + get<_i25.BoardGamesStore>(), get<_i22.PlaythroughsStore>())); + gh.factory<_i31.PlaythroughsHistoryViewModel>( + () => _i31.PlaythroughsHistoryViewModel(get<_i22.PlaythroughsStore>())); + gh.factory<_i32.PlaythroughsLogGameViewModel>(() => + _i32.PlaythroughsLogGameViewModel(get<_i10.PlayersStore>(), + get<_i22.PlaythroughsStore>(), get<_i17.AnalyticsService>())); + gh.singleton<_i33.SearchBoardGamesViewModel>(_i33.SearchBoardGamesViewModel( + get<_i25.BoardGamesStore>(), + get<_i19.BoardGamesGeekService>(), + get<_i17.AnalyticsService>())); + gh.singleton<_i34.SettingsViewModel>(_i34.SettingsViewModel( + get<_i6.FileService>(), + get<_i20.BoardGamesService>(), + get<_i4.BoardGamesFiltersService>(), + get<_i9.PlayerService>(), + get<_i15.UserService>(), + get<_i21.PlaythroughService>(), + get<_i14.ScoreService>(), + get<_i12.PreferencesService>(), + get<_i3.AppStore>(), + get<_i16.UserStore>(), + get<_i25.BoardGamesStore>())); + gh.factory<_i35.BoardGameDetailsViewModel>(() => + _i35.BoardGameDetailsViewModel( + get<_i25.BoardGamesStore>(), get<_i17.AnalyticsService>())); return get; } -class _$RegisterModule extends _i33.RegisterModule { +class _$RegisterModule extends _i36.RegisterModule { @override - _i6.FirebaseAnalytics get firebaseAnalytics => _i6.FirebaseAnalytics(); + _i7.FirebaseAnalytics get firebaseAnalytics => _i7.FirebaseAnalytics(); } diff --git a/board_games_companion/lib/main.dart b/board_games_companion/lib/main.dart index 5f3e0b63..54e71e7c 100644 --- a/board_games_companion/lib/main.dart +++ b/board_games_companion/lib/main.dart @@ -34,10 +34,8 @@ import 'pages/players/players_view_model.dart'; import 'services/analytics_service.dart'; import 'services/board_games_geek_service.dart'; import 'services/preferences_service.dart'; -import 'services/user_service.dart'; import 'stores/search_bar_board_games_store.dart'; import 'stores/search_board_games_store.dart'; -import 'stores/user_store.dart'; Future main() async { Fimber.plantTree(DebugTree()); @@ -69,7 +67,7 @@ Future main() async { configureDependencies(); - final PreferencesService preferencesService = getIt(); + final preferencesService = getIt(); await preferencesService.initialize(); LicenseRegistry.addLicense(() async* { @@ -103,18 +101,9 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { + preferencesService.setAppLaunchDate(); return MultiProvider( providers: [ - ChangeNotifierProvider( - create: (context) { - final UserService userService = getIt(); - final userStore = UserStore(userService); - - preferencesService.setAppLaunchDate(); - userStore.loadUser(); - return userStore; - }, - ), ChangeNotifierProvider( create: (context) => SearchBarBoardGamesStore(), ), diff --git a/board_games_companion/lib/mixins/enter_score_dialog.dart b/board_games_companion/lib/mixins/enter_score_dialog.dart index 8de07321..f97dc42b 100644 --- a/board_games_companion/lib/mixins/enter_score_dialog.dart +++ b/board_games_companion/lib/mixins/enter_score_dialog.dart @@ -7,7 +7,7 @@ import '../pages/enter_score/enter_score_dialog.dart'; mixin EnterScoreDialogMixin { Future showEnterScoreDialog(BuildContext context, EnterScoreViewModel viewModel) async { - showGeneralDialog( + await showGeneralDialog( context: context, pageBuilder: (_, __, ___) { return EnterScoreDialog(viewModel: viewModel); diff --git a/board_games_companion/lib/mixins/import_collection.dart b/board_games_companion/lib/mixins/import_collection.dart index 52fbddd4..443291b9 100644 --- a/board_games_companion/lib/mixins/import_collection.dart +++ b/board_games_companion/lib/mixins/import_collection.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../common/app_text.dart'; import '../common/dimensions.dart'; @@ -11,15 +10,12 @@ import '../stores/user_store.dart'; mixin ImportCollection { Future importCollections(BuildContext context, String username) async { - final userStore = Provider.of( - context, - listen: false, - ); - if (username.isEmpty) { return CollectionImportResult(); } + final userStore = getIt(); + final messenger = ScaffoldMessenger.of(context); final boardGamesStore = getIt(); final importResult = await boardGamesStore.importCollections(username); diff --git a/board_games_companion/lib/models/backup_file.dart b/board_games_companion/lib/models/backup_file.dart new file mode 100644 index 00000000..5793e78f --- /dev/null +++ b/board_games_companion/lib/models/backup_file.dart @@ -0,0 +1,34 @@ +import 'dart:math'; + +import 'package:intl/intl.dart'; +import 'package:path/path.dart'; + +class BackupFile { + BackupFile({ + required this.path, + required this.size, + required this.changed, + }); + + static final NumberFormat _fileSizeFormat = NumberFormat('#,##0.#'); + static final List _fileSizeUnits = ['B', 'kB', 'MB', 'GB', 'TB']; + + int size; + DateTime changed; + String path; + + String get name => basenameWithoutExtension(path); + + String get nameWithExtension => basename(path); + + String get readableFileSize { + if (size <= 0) { + return '0'; + } + + final digitGroups = logBase(size, 10) ~/ logBase(1024, 10); + return '${_fileSizeFormat.format(size / pow(1024, digitGroups))} ${_fileSizeUnits[digitGroups]}'; + } + + double logBase(num x, num base) => log(x) / log(base); +} diff --git a/board_games_companion/lib/models/hive/playthrough.dart b/board_games_companion/lib/models/hive/playthrough.dart index 9a20e16f..e9fd9e88 100644 --- a/board_games_companion/lib/models/hive/playthrough.dart +++ b/board_games_companion/lib/models/hive/playthrough.dart @@ -1,49 +1,26 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; import '../../common/enums/playthrough_status.dart'; import '../../common/hive_boxes.dart'; +part 'playthrough.freezed.dart'; part 'playthrough.g.dart'; -@HiveType(typeId: HiveBoxes.playthroughTypeId) -class Playthrough = _Playthrough with _$Playthrough; - -abstract class _Playthrough with Store { - _Playthrough({ - required this.id, - required this.boardGameId, - required this.playerIds, - required this.scoreIds, - required this.startDate, - // MK It is used but for whatever reason linter thinks it isn't - // ignore: unused_element - this.bggPlayId, - }); - - @HiveField(0) - String id; - @HiveField(1) - String boardGameId; - @HiveField(2) - List playerIds; - @HiveField(3) - List scoreIds; - - @HiveField(4) - @observable - DateTime startDate; - @HiveField(5) - @observable - DateTime? endDate; - @HiveField(6) - @observable - PlaythroughStatus? status; - @HiveField(7) - @observable - bool? isDeleted = false; - @HiveField(8) - int? bggPlayId; +@freezed +abstract class Playthrough with _$Playthrough { + @HiveType(typeId: HiveBoxes.playthroughTypeId, adapterName: 'PlaythroughAdapter') + const factory Playthrough({ + @HiveField(0) required String id, + @HiveField(1) required String boardGameId, + @HiveField(2) required List playerIds, + @HiveField(3) required List scoreIds, + @HiveField(4) required DateTime startDate, + @HiveField(5) DateTime? endDate, + @HiveField(6) PlaythroughStatus? status, + @Default(false) @HiveField(7) bool? isDeleted, + @HiveField(8) int? bggPlayId, + }) = _Playthrough; } diff --git a/board_games_companion/lib/models/hive/playthrough.freezed.dart b/board_games_companion/lib/models/hive/playthrough.freezed.dart new file mode 100644 index 00000000..ad0c3761 --- /dev/null +++ b/board_games_companion/lib/models/hive/playthrough.freezed.dart @@ -0,0 +1,345 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of '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 _$Playthrough { + @HiveField(0) + String get id => throw _privateConstructorUsedError; + @HiveField(1) + String get boardGameId => throw _privateConstructorUsedError; + @HiveField(2) + List get playerIds => throw _privateConstructorUsedError; + @HiveField(3) + List get scoreIds => throw _privateConstructorUsedError; + @HiveField(4) + DateTime get startDate => throw _privateConstructorUsedError; + @HiveField(5) + DateTime? get endDate => throw _privateConstructorUsedError; + @HiveField(6) + PlaythroughStatus? get status => throw _privateConstructorUsedError; + @HiveField(7) + bool? get isDeleted => throw _privateConstructorUsedError; + @HiveField(8) + int? get bggPlayId => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PlaythroughCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaythroughCopyWith<$Res> { + factory $PlaythroughCopyWith( + Playthrough value, $Res Function(Playthrough) then) = + _$PlaythroughCopyWithImpl<$Res>; + $Res call( + {@HiveField(0) String id, + @HiveField(1) String boardGameId, + @HiveField(2) List playerIds, + @HiveField(3) List scoreIds, + @HiveField(4) DateTime startDate, + @HiveField(5) DateTime? endDate, + @HiveField(6) PlaythroughStatus? status, + @HiveField(7) bool? isDeleted, + @HiveField(8) int? bggPlayId}); +} + +/// @nodoc +class _$PlaythroughCopyWithImpl<$Res> implements $PlaythroughCopyWith<$Res> { + _$PlaythroughCopyWithImpl(this._value, this._then); + + final Playthrough _value; + // ignore: unused_field + final $Res Function(Playthrough) _then; + + @override + $Res call({ + Object? id = freezed, + Object? boardGameId = freezed, + Object? playerIds = freezed, + Object? scoreIds = freezed, + Object? startDate = freezed, + Object? endDate = freezed, + Object? status = freezed, + Object? isDeleted = freezed, + Object? bggPlayId = freezed, + }) { + return _then(_value.copyWith( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + playerIds: playerIds == freezed + ? _value.playerIds + : playerIds // ignore: cast_nullable_to_non_nullable + as List, + scoreIds: scoreIds == freezed + ? _value.scoreIds + : scoreIds // ignore: cast_nullable_to_non_nullable + as List, + startDate: startDate == freezed + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: endDate == freezed + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: status == freezed + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as PlaythroughStatus?, + isDeleted: isDeleted == freezed + ? _value.isDeleted + : isDeleted // ignore: cast_nullable_to_non_nullable + as bool?, + bggPlayId: bggPlayId == freezed + ? _value.bggPlayId + : bggPlayId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +abstract class _$$_PlaythroughCopyWith<$Res> + implements $PlaythroughCopyWith<$Res> { + factory _$$_PlaythroughCopyWith( + _$_Playthrough value, $Res Function(_$_Playthrough) then) = + __$$_PlaythroughCopyWithImpl<$Res>; + @override + $Res call( + {@HiveField(0) String id, + @HiveField(1) String boardGameId, + @HiveField(2) List playerIds, + @HiveField(3) List scoreIds, + @HiveField(4) DateTime startDate, + @HiveField(5) DateTime? endDate, + @HiveField(6) PlaythroughStatus? status, + @HiveField(7) bool? isDeleted, + @HiveField(8) int? bggPlayId}); +} + +/// @nodoc +class __$$_PlaythroughCopyWithImpl<$Res> extends _$PlaythroughCopyWithImpl<$Res> + implements _$$_PlaythroughCopyWith<$Res> { + __$$_PlaythroughCopyWithImpl( + _$_Playthrough _value, $Res Function(_$_Playthrough) _then) + : super(_value, (v) => _then(v as _$_Playthrough)); + + @override + _$_Playthrough get _value => super._value as _$_Playthrough; + + @override + $Res call({ + Object? id = freezed, + Object? boardGameId = freezed, + Object? playerIds = freezed, + Object? scoreIds = freezed, + Object? startDate = freezed, + Object? endDate = freezed, + Object? status = freezed, + Object? isDeleted = freezed, + Object? bggPlayId = freezed, + }) { + return _then(_$_Playthrough( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + playerIds: playerIds == freezed + ? _value._playerIds + : playerIds // ignore: cast_nullable_to_non_nullable + as List, + scoreIds: scoreIds == freezed + ? _value._scoreIds + : scoreIds // ignore: cast_nullable_to_non_nullable + as List, + startDate: startDate == freezed + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: endDate == freezed + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: status == freezed + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as PlaythroughStatus?, + isDeleted: isDeleted == freezed + ? _value.isDeleted + : isDeleted // ignore: cast_nullable_to_non_nullable + as bool?, + bggPlayId: bggPlayId == freezed + ? _value.bggPlayId + : bggPlayId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc + +@HiveType( + typeId: HiveBoxes.playthroughTypeId, adapterName: 'PlaythroughAdapter') +class _$_Playthrough implements _Playthrough { + const _$_Playthrough( + {@HiveField(0) required this.id, + @HiveField(1) required this.boardGameId, + @HiveField(2) required final List playerIds, + @HiveField(3) required final List scoreIds, + @HiveField(4) required this.startDate, + @HiveField(5) this.endDate, + @HiveField(6) this.status, + @HiveField(7) this.isDeleted = false, + @HiveField(8) this.bggPlayId}) + : _playerIds = playerIds, + _scoreIds = scoreIds; + + @override + @HiveField(0) + final String id; + @override + @HiveField(1) + final String boardGameId; + final List _playerIds; + @override + @HiveField(2) + List get playerIds { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_playerIds); + } + + final List _scoreIds; + @override + @HiveField(3) + List get scoreIds { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_scoreIds); + } + + @override + @HiveField(4) + final DateTime startDate; + @override + @HiveField(5) + final DateTime? endDate; + @override + @HiveField(6) + final PlaythroughStatus? status; + @override + @JsonKey() + @HiveField(7) + final bool? isDeleted; + @override + @HiveField(8) + final int? bggPlayId; + + @override + String toString() { + return 'Playthrough(id: $id, boardGameId: $boardGameId, playerIds: $playerIds, scoreIds: $scoreIds, startDate: $startDate, endDate: $endDate, status: $status, isDeleted: $isDeleted, bggPlayId: $bggPlayId)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Playthrough && + const DeepCollectionEquality().equals(other.id, id) && + const DeepCollectionEquality() + .equals(other.boardGameId, boardGameId) && + const DeepCollectionEquality() + .equals(other._playerIds, _playerIds) && + const DeepCollectionEquality().equals(other._scoreIds, _scoreIds) && + const DeepCollectionEquality().equals(other.startDate, startDate) && + const DeepCollectionEquality().equals(other.endDate, endDate) && + const DeepCollectionEquality().equals(other.status, status) && + const DeepCollectionEquality().equals(other.isDeleted, isDeleted) && + const DeepCollectionEquality().equals(other.bggPlayId, bggPlayId)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(id), + const DeepCollectionEquality().hash(boardGameId), + const DeepCollectionEquality().hash(_playerIds), + const DeepCollectionEquality().hash(_scoreIds), + const DeepCollectionEquality().hash(startDate), + const DeepCollectionEquality().hash(endDate), + const DeepCollectionEquality().hash(status), + const DeepCollectionEquality().hash(isDeleted), + const DeepCollectionEquality().hash(bggPlayId)); + + @JsonKey(ignore: true) + @override + _$$_PlaythroughCopyWith<_$_Playthrough> get copyWith => + __$$_PlaythroughCopyWithImpl<_$_Playthrough>(this, _$identity); +} + +abstract class _Playthrough implements Playthrough { + const factory _Playthrough( + {@HiveField(0) required final String id, + @HiveField(1) required final String boardGameId, + @HiveField(2) required final List playerIds, + @HiveField(3) required final List scoreIds, + @HiveField(4) required final DateTime startDate, + @HiveField(5) final DateTime? endDate, + @HiveField(6) final PlaythroughStatus? status, + @HiveField(7) final bool? isDeleted, + @HiveField(8) final int? bggPlayId}) = _$_Playthrough; + + @override + @HiveField(0) + String get id; + @override + @HiveField(1) + String get boardGameId; + @override + @HiveField(2) + List get playerIds; + @override + @HiveField(3) + List get scoreIds; + @override + @HiveField(4) + DateTime get startDate; + @override + @HiveField(5) + DateTime? get endDate; + @override + @HiveField(6) + PlaythroughStatus? get status; + @override + @HiveField(7) + bool? get isDeleted; + @override + @HiveField(8) + int? get bggPlayId; + @override + @JsonKey(ignore: true) + _$$_PlaythroughCopyWith<_$_Playthrough> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/models/hive/playthrough.g.dart b/board_games_companion/lib/models/hive/playthrough.g.dart index 96d84627..61e4eca1 100644 --- a/board_games_companion/lib/models/hive/playthrough.g.dart +++ b/board_games_companion/lib/models/hive/playthrough.g.dart @@ -6,41 +6,37 @@ part of 'playthrough.dart'; // TypeAdapterGenerator // ************************************************************************** -class PlaythroughAdapter extends TypeAdapter { +class PlaythroughAdapter extends TypeAdapter<_$_Playthrough> { @override final int typeId = 3; @override - Playthrough read(BinaryReader reader) { + _$_Playthrough read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return Playthrough( + return _$_Playthrough( id: fields[0] as String, boardGameId: fields[1] as String, playerIds: (fields[2] as List).cast(), scoreIds: (fields[3] as List).cast(), startDate: fields[4] as DateTime, + endDate: fields[5] as DateTime?, + status: fields[6] as PlaythroughStatus?, + isDeleted: fields[7] as bool?, bggPlayId: fields[8] as int?, - ) - ..endDate = fields[5] as DateTime? - ..status = fields[6] as PlaythroughStatus? - ..isDeleted = fields[7] as bool?; + ); } @override - void write(BinaryWriter writer, Playthrough obj) { + void write(BinaryWriter writer, _$_Playthrough obj) { writer ..writeByte(9) ..writeByte(0) ..write(obj.id) ..writeByte(1) ..write(obj.boardGameId) - ..writeByte(2) - ..write(obj.playerIds) - ..writeByte(3) - ..write(obj.scoreIds) ..writeByte(4) ..write(obj.startDate) ..writeByte(5) @@ -50,7 +46,11 @@ class PlaythroughAdapter extends TypeAdapter { ..writeByte(7) ..write(obj.isDeleted) ..writeByte(8) - ..write(obj.bggPlayId); + ..write(obj.bggPlayId) + ..writeByte(2) + ..write(obj.playerIds) + ..writeByte(3) + ..write(obj.scoreIds); } @override @@ -63,84 +63,3 @@ class PlaythroughAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } - -// ************************************************************************** -// StoreGenerator -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers - -mixin _$Playthrough on _Playthrough, Store { - late final _$startDateAtom = - Atom(name: '_Playthrough.startDate', context: context); - - @override - DateTime get startDate { - _$startDateAtom.reportRead(); - return super.startDate; - } - - @override - set startDate(DateTime value) { - _$startDateAtom.reportWrite(value, super.startDate, () { - super.startDate = value; - }); - } - - late final _$endDateAtom = - Atom(name: '_Playthrough.endDate', context: context); - - @override - DateTime? get endDate { - _$endDateAtom.reportRead(); - return super.endDate; - } - - @override - set endDate(DateTime? value) { - _$endDateAtom.reportWrite(value, super.endDate, () { - super.endDate = value; - }); - } - - late final _$statusAtom = Atom(name: '_Playthrough.status', context: context); - - @override - PlaythroughStatus? get status { - _$statusAtom.reportRead(); - return super.status; - } - - @override - set status(PlaythroughStatus? value) { - _$statusAtom.reportWrite(value, super.status, () { - super.status = value; - }); - } - - late final _$isDeletedAtom = - Atom(name: '_Playthrough.isDeleted', context: context); - - @override - bool? get isDeleted { - _$isDeletedAtom.reportRead(); - return super.isDeleted; - } - - @override - set isDeleted(bool? value) { - _$isDeletedAtom.reportWrite(value, super.isDeleted, () { - super.isDeleted = value; - }); - } - - @override - String toString() { - return ''' -startDate: ${startDate}, -endDate: ${endDate}, -status: ${status}, -isDeleted: ${isDeleted} - '''; - } -} diff --git a/board_games_companion/lib/models/hive/score.dart b/board_games_companion/lib/models/hive/score.dart index b6228d31..d21f02c5 100644 --- a/board_games_companion/lib/models/hive/score.dart +++ b/board_games_companion/lib/models/hive/score.dart @@ -1,30 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; import '../../common/hive_boxes.dart'; +part 'score.freezed.dart'; part 'score.g.dart'; -@HiveType(typeId: HiveBoxes.scoreTypeId) -class Score { - Score({ - required this.id, - required this.playerId, - required this.boardGameId, - this.value, - }); +@freezed +abstract class Score with _$Score { + @HiveType(typeId: HiveBoxes.scoreTypeId, adapterName: 'ScoreAdapter') + const factory Score({ + @HiveField(0) required String id, + @HiveField(2) required String playerId, + @HiveField(3) required String boardGameId, + @HiveField(4) String? value, + @HiveField(1) String? playthroughId, + }) = _Score; - @HiveField(0) - String id; - - @HiveField(1) - String? playthroughId; - @HiveField(2) - String playerId; - @HiveField(3) - String boardGameId; - - @HiveField(4) - String? value; + const Score._(); int get valueInt => int.tryParse(value ?? '0') ?? 0; } diff --git a/board_games_companion/lib/models/hive/score.freezed.dart b/board_games_companion/lib/models/hive/score.freezed.dart new file mode 100644 index 00000000..2b793dcf --- /dev/null +++ b/board_games_companion/lib/models/hive/score.freezed.dart @@ -0,0 +1,232 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'score.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 _$Score { + @HiveField(0) + String get id => throw _privateConstructorUsedError; + @HiveField(2) + String get playerId => throw _privateConstructorUsedError; + @HiveField(3) + String get boardGameId => throw _privateConstructorUsedError; + @HiveField(4) + String? get value => throw _privateConstructorUsedError; + @HiveField(1) + String? get playthroughId => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ScoreCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ScoreCopyWith<$Res> { + factory $ScoreCopyWith(Score value, $Res Function(Score) then) = + _$ScoreCopyWithImpl<$Res>; + $Res call( + {@HiveField(0) String id, + @HiveField(2) String playerId, + @HiveField(3) String boardGameId, + @HiveField(4) String? value, + @HiveField(1) String? playthroughId}); +} + +/// @nodoc +class _$ScoreCopyWithImpl<$Res> implements $ScoreCopyWith<$Res> { + _$ScoreCopyWithImpl(this._value, this._then); + + final Score _value; + // ignore: unused_field + final $Res Function(Score) _then; + + @override + $Res call({ + Object? id = freezed, + Object? playerId = freezed, + Object? boardGameId = freezed, + Object? value = freezed, + Object? playthroughId = freezed, + }) { + return _then(_value.copyWith( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + playerId: playerId == freezed + ? _value.playerId + : playerId // ignore: cast_nullable_to_non_nullable + as String, + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + value: value == freezed + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String?, + playthroughId: playthroughId == freezed + ? _value.playthroughId + : playthroughId // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +abstract class _$$_ScoreCopyWith<$Res> implements $ScoreCopyWith<$Res> { + factory _$$_ScoreCopyWith(_$_Score value, $Res Function(_$_Score) then) = + __$$_ScoreCopyWithImpl<$Res>; + @override + $Res call( + {@HiveField(0) String id, + @HiveField(2) String playerId, + @HiveField(3) String boardGameId, + @HiveField(4) String? value, + @HiveField(1) String? playthroughId}); +} + +/// @nodoc +class __$$_ScoreCopyWithImpl<$Res> extends _$ScoreCopyWithImpl<$Res> + implements _$$_ScoreCopyWith<$Res> { + __$$_ScoreCopyWithImpl(_$_Score _value, $Res Function(_$_Score) _then) + : super(_value, (v) => _then(v as _$_Score)); + + @override + _$_Score get _value => super._value as _$_Score; + + @override + $Res call({ + Object? id = freezed, + Object? playerId = freezed, + Object? boardGameId = freezed, + Object? value = freezed, + Object? playthroughId = freezed, + }) { + return _then(_$_Score( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + playerId: playerId == freezed + ? _value.playerId + : playerId // ignore: cast_nullable_to_non_nullable + as String, + boardGameId: boardGameId == freezed + ? _value.boardGameId + : boardGameId // ignore: cast_nullable_to_non_nullable + as String, + value: value == freezed + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String?, + playthroughId: playthroughId == freezed + ? _value.playthroughId + : playthroughId // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +@HiveType(typeId: HiveBoxes.scoreTypeId, adapterName: 'ScoreAdapter') +class _$_Score extends _Score { + const _$_Score( + {@HiveField(0) required this.id, + @HiveField(2) required this.playerId, + @HiveField(3) required this.boardGameId, + @HiveField(4) this.value, + @HiveField(1) this.playthroughId}) + : super._(); + + @override + @HiveField(0) + final String id; + @override + @HiveField(2) + final String playerId; + @override + @HiveField(3) + final String boardGameId; + @override + @HiveField(4) + final String? value; + @override + @HiveField(1) + final String? playthroughId; + + @override + String toString() { + return 'Score(id: $id, playerId: $playerId, boardGameId: $boardGameId, value: $value, playthroughId: $playthroughId)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Score && + const DeepCollectionEquality().equals(other.id, id) && + const DeepCollectionEquality().equals(other.playerId, playerId) && + const DeepCollectionEquality() + .equals(other.boardGameId, boardGameId) && + const DeepCollectionEquality().equals(other.value, value) && + const DeepCollectionEquality() + .equals(other.playthroughId, playthroughId)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(id), + const DeepCollectionEquality().hash(playerId), + const DeepCollectionEquality().hash(boardGameId), + const DeepCollectionEquality().hash(value), + const DeepCollectionEquality().hash(playthroughId)); + + @JsonKey(ignore: true) + @override + _$$_ScoreCopyWith<_$_Score> get copyWith => + __$$_ScoreCopyWithImpl<_$_Score>(this, _$identity); +} + +abstract class _Score extends Score { + const factory _Score( + {@HiveField(0) required final String id, + @HiveField(2) required final String playerId, + @HiveField(3) required final String boardGameId, + @HiveField(4) final String? value, + @HiveField(1) final String? playthroughId}) = _$_Score; + const _Score._() : super._(); + + @override + @HiveField(0) + String get id; + @override + @HiveField(2) + String get playerId; + @override + @HiveField(3) + String get boardGameId; + @override + @HiveField(4) + String? get value; + @override + @HiveField(1) + String? get playthroughId; + @override + @JsonKey(ignore: true) + _$$_ScoreCopyWith<_$_Score> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/models/hive/score.g.dart b/board_games_companion/lib/models/hive/score.g.dart index 960fb272..20a1e755 100644 --- a/board_games_companion/lib/models/hive/score.g.dart +++ b/board_games_companion/lib/models/hive/score.g.dart @@ -6,38 +6,39 @@ part of 'score.dart'; // TypeAdapterGenerator // ************************************************************************** -class ScoreAdapter extends TypeAdapter { +class ScoreAdapter extends TypeAdapter<_$_Score> { @override final int typeId = 4; @override - Score read(BinaryReader reader) { + _$_Score read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return Score( + return _$_Score( id: fields[0] as String, playerId: fields[2] as String, boardGameId: fields[3] as String, value: fields[4] as String?, - )..playthroughId = fields[1] as String?; + playthroughId: fields[1] as String?, + ); } @override - void write(BinaryWriter writer, Score obj) { + void write(BinaryWriter writer, _$_Score obj) { writer ..writeByte(5) ..writeByte(0) ..write(obj.id) - ..writeByte(1) - ..write(obj.playthroughId) ..writeByte(2) ..write(obj.playerId) ..writeByte(3) ..write(obj.boardGameId) ..writeByte(4) - ..write(obj.value); + ..write(obj.value) + ..writeByte(1) + ..write(obj.playthroughId); } @override diff --git a/board_games_companion/lib/models/navigation/edit_playthrough_page_arguments.dart b/board_games_companion/lib/models/navigation/edit_playthrough_page_arguments.dart index e3e34b36..4bb90518 100644 --- a/board_games_companion/lib/models/navigation/edit_playthrough_page_arguments.dart +++ b/board_games_companion/lib/models/navigation/edit_playthrough_page_arguments.dart @@ -1,7 +1,6 @@ -import 'package:board_games_companion/pages/playthroughs/playthrough_view_model.dart'; - class EditPlaythroughPageArguments { - const EditPlaythroughPageArguments(this.playthroughViewModel); + const EditPlaythroughPageArguments(this.playthroughId, this.boardGameId); - final PlaythroughViewModel playthroughViewModel; + final String playthroughId; + final String boardGameId; } diff --git a/board_games_companion/lib/models/player_score.dart b/board_games_companion/lib/models/player_score.dart index 4878943b..fdf77301 100644 --- a/board_games_companion/lib/models/player_score.dart +++ b/board_games_companion/lib/models/player_score.dart @@ -1,32 +1,19 @@ -import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'hive/player.dart'; import 'hive/score.dart'; -class PlayerScore with ChangeNotifier { - PlayerScore(this._player, this._score); +part 'player_score.freezed.dart'; - PlayerScore.withPlace(this._player, this._score, this._place); +@freezed +abstract class PlayerScore with _$PlayerScore { + const factory PlayerScore({ + required Player? player, + required Score score, + int? place, + }) = _PlayerScore; - final Player? _player; + const PlayerScore._(); - Player? get player => _player; - - final Score _score; - Score get score => _score; - - int? _place; - int? get place => _place; - - bool updatePlayerScore(String score) { - if (score.isEmpty) { - return false; - } - - _score.value = score; - - notifyListeners(); - - return true; - } + String? get id => player?.id; } diff --git a/board_games_companion/lib/models/player_score.freezed.dart b/board_games_companion/lib/models/player_score.freezed.dart new file mode 100644 index 00000000..24a46822 --- /dev/null +++ b/board_games_companion/lib/models/player_score.freezed.dart @@ -0,0 +1,180 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'player_score.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 _$PlayerScore { + Player? get player => throw _privateConstructorUsedError; + Score get score => throw _privateConstructorUsedError; + int? get place => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PlayerScoreCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlayerScoreCopyWith<$Res> { + factory $PlayerScoreCopyWith( + PlayerScore value, $Res Function(PlayerScore) then) = + _$PlayerScoreCopyWithImpl<$Res>; + $Res call({Player? player, Score score, int? place}); + + $ScoreCopyWith<$Res> get score; +} + +/// @nodoc +class _$PlayerScoreCopyWithImpl<$Res> implements $PlayerScoreCopyWith<$Res> { + _$PlayerScoreCopyWithImpl(this._value, this._then); + + final PlayerScore _value; + // ignore: unused_field + final $Res Function(PlayerScore) _then; + + @override + $Res call({ + Object? player = freezed, + Object? score = freezed, + Object? place = freezed, + }) { + return _then(_value.copyWith( + player: player == freezed + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as Player?, + score: score == freezed + ? _value.score + : score // ignore: cast_nullable_to_non_nullable + as Score, + place: place == freezed + ? _value.place + : place // ignore: cast_nullable_to_non_nullable + as int?, + )); + } + + @override + $ScoreCopyWith<$Res> get score { + return $ScoreCopyWith<$Res>(_value.score, (value) { + return _then(_value.copyWith(score: value)); + }); + } +} + +/// @nodoc +abstract class _$$_PlayerScoreCopyWith<$Res> + implements $PlayerScoreCopyWith<$Res> { + factory _$$_PlayerScoreCopyWith( + _$_PlayerScore value, $Res Function(_$_PlayerScore) then) = + __$$_PlayerScoreCopyWithImpl<$Res>; + @override + $Res call({Player? player, Score score, int? place}); + + @override + $ScoreCopyWith<$Res> get score; +} + +/// @nodoc +class __$$_PlayerScoreCopyWithImpl<$Res> extends _$PlayerScoreCopyWithImpl<$Res> + implements _$$_PlayerScoreCopyWith<$Res> { + __$$_PlayerScoreCopyWithImpl( + _$_PlayerScore _value, $Res Function(_$_PlayerScore) _then) + : super(_value, (v) => _then(v as _$_PlayerScore)); + + @override + _$_PlayerScore get _value => super._value as _$_PlayerScore; + + @override + $Res call({ + Object? player = freezed, + Object? score = freezed, + Object? place = freezed, + }) { + return _then(_$_PlayerScore( + player: player == freezed + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as Player?, + score: score == freezed + ? _value.score + : score // ignore: cast_nullable_to_non_nullable + as Score, + place: place == freezed + ? _value.place + : place // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc + +class _$_PlayerScore extends _PlayerScore { + const _$_PlayerScore({required this.player, required this.score, this.place}) + : super._(); + + @override + final Player? player; + @override + final Score score; + @override + final int? place; + + @override + String toString() { + return 'PlayerScore(player: $player, score: $score, place: $place)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PlayerScore && + const DeepCollectionEquality().equals(other.player, player) && + const DeepCollectionEquality().equals(other.score, score) && + const DeepCollectionEquality().equals(other.place, place)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(player), + const DeepCollectionEquality().hash(score), + const DeepCollectionEquality().hash(place)); + + @JsonKey(ignore: true) + @override + _$$_PlayerScoreCopyWith<_$_PlayerScore> get copyWith => + __$$_PlayerScoreCopyWithImpl<_$_PlayerScore>(this, _$identity); +} + +abstract class _PlayerScore extends PlayerScore { + const factory _PlayerScore( + {required final Player? player, + required final Score score, + final int? place}) = _$_PlayerScore; + const _PlayerScore._() : super._(); + + @override + Player? get player; + @override + Score get score; + @override + int? get place; + @override + @JsonKey(ignore: true) + _$$_PlayerScoreCopyWith<_$_PlayerScore> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/models/playthrough_details.dart b/board_games_companion/lib/models/playthrough_details.dart new file mode 100644 index 00000000..74a4d1c8 --- /dev/null +++ b/board_games_companion/lib/models/playthrough_details.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../common/enums/playthrough_status.dart'; +import 'hive/playthrough.dart'; +import 'player_score.dart'; + +part 'playthrough_details.freezed.dart'; + +@freezed +class PlaythroughDetails with _$PlaythroughDetails { + const factory PlaythroughDetails({ + required Playthrough playthrough, + required List playerScores, + }) = _PlaythroughDetails; + + const PlaythroughDetails._(); + + int? get daysSinceStart => DateTime.now().toUtc().difference(playthrough.startDate).inDays; + + Duration get duration { + final nowUtc = DateTime.now().toUtc(); + final playthroughEndDate = playthrough.endDate ?? nowUtc; + return playthroughEndDate.difference(playthrough.startDate); + } + + bool get playthoughEnded => playthrough.status == PlaythroughStatus.Finished; + + PlaythroughStatus? get status => playthrough.status; + + String get id => playthrough.id; + + String get boardGameId => playthrough.boardGameId; + + int? get bggPlayId => playthrough.bggPlayId; + + DateTime get startDate => playthrough.startDate; + + DateTime? get endDate => playthrough.endDate; +} diff --git a/board_games_companion/lib/models/playthrough_details.freezed.dart b/board_games_companion/lib/models/playthrough_details.freezed.dart new file mode 100644 index 00000000..6f90d86f --- /dev/null +++ b/board_games_companion/lib/models/playthrough_details.freezed.dart @@ -0,0 +1,174 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'playthrough_details.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 _$PlaythroughDetails { + Playthrough get playthrough => throw _privateConstructorUsedError; + List get playerScores => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PlaythroughDetailsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaythroughDetailsCopyWith<$Res> { + factory $PlaythroughDetailsCopyWith( + PlaythroughDetails value, $Res Function(PlaythroughDetails) then) = + _$PlaythroughDetailsCopyWithImpl<$Res>; + $Res call({Playthrough playthrough, List playerScores}); + + $PlaythroughCopyWith<$Res> get playthrough; +} + +/// @nodoc +class _$PlaythroughDetailsCopyWithImpl<$Res> + implements $PlaythroughDetailsCopyWith<$Res> { + _$PlaythroughDetailsCopyWithImpl(this._value, this._then); + + final PlaythroughDetails _value; + // ignore: unused_field + final $Res Function(PlaythroughDetails) _then; + + @override + $Res call({ + Object? playthrough = freezed, + Object? playerScores = freezed, + }) { + return _then(_value.copyWith( + playthrough: playthrough == freezed + ? _value.playthrough + : playthrough // ignore: cast_nullable_to_non_nullable + as Playthrough, + playerScores: playerScores == freezed + ? _value.playerScores + : playerScores // ignore: cast_nullable_to_non_nullable + as List, + )); + } + + @override + $PlaythroughCopyWith<$Res> get playthrough { + return $PlaythroughCopyWith<$Res>(_value.playthrough, (value) { + return _then(_value.copyWith(playthrough: value)); + }); + } +} + +/// @nodoc +abstract class _$$_PlaythroughDetailsCopyWith<$Res> + implements $PlaythroughDetailsCopyWith<$Res> { + factory _$$_PlaythroughDetailsCopyWith(_$_PlaythroughDetails value, + $Res Function(_$_PlaythroughDetails) then) = + __$$_PlaythroughDetailsCopyWithImpl<$Res>; + @override + $Res call({Playthrough playthrough, List playerScores}); + + @override + $PlaythroughCopyWith<$Res> get playthrough; +} + +/// @nodoc +class __$$_PlaythroughDetailsCopyWithImpl<$Res> + extends _$PlaythroughDetailsCopyWithImpl<$Res> + implements _$$_PlaythroughDetailsCopyWith<$Res> { + __$$_PlaythroughDetailsCopyWithImpl( + _$_PlaythroughDetails _value, $Res Function(_$_PlaythroughDetails) _then) + : super(_value, (v) => _then(v as _$_PlaythroughDetails)); + + @override + _$_PlaythroughDetails get _value => super._value as _$_PlaythroughDetails; + + @override + $Res call({ + Object? playthrough = freezed, + Object? playerScores = freezed, + }) { + return _then(_$_PlaythroughDetails( + playthrough: playthrough == freezed + ? _value.playthrough + : playthrough // ignore: cast_nullable_to_non_nullable + as Playthrough, + playerScores: playerScores == freezed + ? _value._playerScores + : playerScores // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_PlaythroughDetails extends _PlaythroughDetails { + const _$_PlaythroughDetails( + {required this.playthrough, + required final List playerScores}) + : _playerScores = playerScores, + super._(); + + @override + final Playthrough playthrough; + final List _playerScores; + @override + List get playerScores { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_playerScores); + } + + @override + String toString() { + return 'PlaythroughDetails(playthrough: $playthrough, playerScores: $playerScores)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PlaythroughDetails && + const DeepCollectionEquality() + .equals(other.playthrough, playthrough) && + const DeepCollectionEquality() + .equals(other._playerScores, _playerScores)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(playthrough), + const DeepCollectionEquality().hash(_playerScores)); + + @JsonKey(ignore: true) + @override + _$$_PlaythroughDetailsCopyWith<_$_PlaythroughDetails> get copyWith => + __$$_PlaythroughDetailsCopyWithImpl<_$_PlaythroughDetails>( + this, _$identity); +} + +abstract class _PlaythroughDetails extends PlaythroughDetails { + const factory _PlaythroughDetails( + {required final Playthrough playthrough, + required final List playerScores}) = _$_PlaythroughDetails; + const _PlaythroughDetails._() : super._(); + + @override + Playthrough get playthrough; + @override + List get playerScores; + @override + @JsonKey(ignore: true) + _$$_PlaythroughDetailsCopyWith<_$_PlaythroughDetails> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/pages/about/about_page.dart b/board_games_companion/lib/pages/about/about_page.dart index 1f7f5c7b..705f99f0 100644 --- a/board_games_companion/lib/pages/about/about_page.dart +++ b/board_games_companion/lib/pages/about/about_page.dart @@ -153,12 +153,8 @@ class AboutPageState extends BasePageState { title: 'Url Launcher', subtitle: "Launching Uri's", uri: 'https://pub.dev/packages/url_launcher'), - Divider( - color: AppColors.accentColor, - ), - SectionTitle( - title: 'Licenses', - ), + Divider(color: AppColors.accentColor), + SectionTitle(title: 'Licenses'), _LicensePageDetailsItem(), ], ), 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 5ac1f5cb..80b64ee5 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 @@ -430,9 +430,7 @@ class _BodySectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Dimensions.standardSpacing, - ), + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 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 66ae3912..4cb8deb7 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 @@ -4,7 +4,6 @@ import 'package:board_games_companion/pages/enter_score/enter_score_view_model.d import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:numberpicker/numberpicker.dart'; -import 'package:provider/provider.dart'; import '../../common/app_colors.dart'; import '../../common/app_styles.dart'; @@ -22,8 +21,8 @@ import '../../widgets/playthrough/calendar_card.dart'; import '../playthroughs/playthroughs_page.dart'; import 'edit_playthrough_view_model.dart'; -class EditPlaythoughPage extends StatefulWidget { - const EditPlaythoughPage({ +class EditPlaythroughPage extends StatefulWidget { + const EditPlaythroughPage({ required this.viewModel, Key? key, }) : super(key: key); @@ -33,10 +32,10 @@ class EditPlaythoughPage extends StatefulWidget { final EditPlaythoughViewModel viewModel; @override - EditPlaythoughPageState createState() => EditPlaythoughPageState(); + EditPlaythroughPageState createState() => EditPlaythroughPageState(); } -class EditPlaythoughPageState extends State with EnterScoreDialogMixin { +class EditPlaythroughPageState extends State with EnterScoreDialogMixin { @override Widget build(BuildContext context) { return WillPopScope( @@ -79,7 +78,10 @@ class EditPlaythoughPageState extends State with EnterScoreD child: _ScoresSection( viewModel: widget.viewModel, onItemTapped: (PlayerScore playerScore) async { - await showEnterScoreDialog(context, EnterScoreViewModel(playerScore)); + final viewModel = EnterScoreViewModel(playerScore); + await showEnterScoreDialog(context, viewModel); + widget.viewModel.updatePlayerScore(playerScore, viewModel.score); + return viewModel.score.toString(); }, ), ), @@ -151,7 +153,7 @@ class EditPlaythoughPageState extends State with EnterScoreD } Future _handleOnWillPop(BuildContext context) async { - if (!widget.viewModel.isDirty()) { + if (!widget.viewModel.isDirty) { return true; } @@ -188,7 +190,7 @@ class EditPlaythoughPageState extends State with EnterScoreD } } -class _ScoresSection extends StatefulWidget { +class _ScoresSection extends StatelessWidget { const _ScoresSection({ Key? key, required this.viewModel, @@ -196,62 +198,86 @@ class _ScoresSection extends StatefulWidget { }) : super(key: key); final EditPlaythoughViewModel viewModel; - final void Function(PlayerScore) onItemTapped; + final Future Function(PlayerScore) onItemTapped; - @override - State<_ScoresSection> createState() => _ScoresSectionState(); -} - -class _ScoresSectionState extends State<_ScoresSection> { @override Widget build(BuildContext context) { - return ListView.separated( - itemCount: widget.viewModel.playerScores.length, - separatorBuilder: (context, index) { - return const SizedBox(height: Dimensions.doubleStandardSpacing); - }, - itemBuilder: (context, index) { - return InkWell( - onTap: () => widget.onItemTapped(widget.viewModel.playerScores[index]), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), - child: _PlayerScoreTile( - playerScore: widget.viewModel.playerScores[index], - playthroughId: widget.viewModel.playthrough.id, - ), - ), + return Observer( + builder: (_) { + return ListView.separated( + itemCount: viewModel.playerScores.length, + separatorBuilder: (context, index) { + return const SizedBox(height: Dimensions.doubleStandardSpacing); + }, + itemBuilder: (context, index) { + return _PlayerScoreTile( + playerScore: viewModel.playerScores[index], + playthroughId: viewModel.playthroughDetails.id, + onItemTapped: onItemTapped, + ); + }, ); }, ); } } -class _PlayerScoreTile extends StatelessWidget { +class _PlayerScoreTile extends StatefulWidget { const _PlayerScoreTile({ Key? key, required this.playerScore, required this.playthroughId, + required this.onItemTapped, }) : super(key: key); final PlayerScore playerScore; final String playthroughId; + final Future Function(PlayerScore) onItemTapped; + + @override + State<_PlayerScoreTile> createState() => _PlayerScoreTileState(); +} + +class _PlayerScoreTileState extends State<_PlayerScoreTile> { + late String? score; + + @override + void initState() { + super.initState(); + + score = widget.playerScore.score.value; + } @override Widget build(BuildContext context) { - return SizedBox( - height: Dimensions.smallPlayerAvatarSize, - child: Row( - children: [ - SizedBox( - height: Dimensions.smallPlayerAvatarSize, - width: Dimensions.smallPlayerAvatarSize, - child: PlayerAvatar(playerScore.player, playerHeroIdSuffix: playthroughId), - ), - const SizedBox(width: Dimensions.standardSpacing), - Expanded( - child: _PlayerScore(playerScore: playerScore), + return InkWell( + onTap: () async { + final newScore = await widget.onItemTapped(widget.playerScore); + setState(() { + score = newScore; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: SizedBox( + height: Dimensions.smallPlayerAvatarSize, + child: Row( + children: [ + SizedBox( + height: Dimensions.smallPlayerAvatarSize, + width: Dimensions.smallPlayerAvatarSize, + child: PlayerAvatar( + widget.playerScore.player, + playerHeroIdSuffix: widget.playthroughId, + ), + ), + const SizedBox(width: Dimensions.standardSpacing), + Expanded( + child: _PlayerScore(score: score), + ), + ], ), - ], + ), ), ); } @@ -260,10 +286,10 @@ class _PlayerScoreTile extends StatelessWidget { class _PlayerScore extends StatelessWidget { const _PlayerScore({ Key? key, - required this.playerScore, + required this.score, }) : super(key: key); - final PlayerScore playerScore; + final String? score; @override Widget build(BuildContext context) { @@ -274,16 +300,9 @@ class _PlayerScore extends StatelessWidget { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - ChangeNotifierProvider.value( - value: playerScore, - child: Consumer( - builder: (_, playerScore, __) { - return Text( - playerScore.score.value ?? '-', - style: AppStyles.playerScoreTextStyle, - ); - }, - ), + Text( + score ?? '-', + style: AppStyles.playerScoreTextStyle, ), const SizedBox(height: Dimensions.halfStandardSpacing), Text(AppText.editPlaythroughScorePoints, style: AppTheme.theme.textTheme.bodyText2), 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 770a59dc..ca0cdb5f 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,104 +1,114 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/models/hive/playthrough.dart'; +import 'package:board_games_companion/models/player_score.dart'; +import 'package:board_games_companion/models/playthrough_details.dart'; import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; import '../../common/enums/playthrough_status.dart'; -import '../../models/hive/playthrough.dart'; -import '../../models/hive/score.dart'; -import '../../models/player_score.dart'; -import '../playthroughs/playthrough_view_model.dart'; part 'edit_playthrough_view_model.g.dart'; +@injectable class EditPlaythoughViewModel = _EditPlaythoughViewModel with _$EditPlaythoughViewModel; abstract class _EditPlaythoughViewModel with Store { - _EditPlaythoughViewModel(this._playthroughViewModel, this._playthroughsStore); + _EditPlaythoughViewModel(this._playthroughsStore); - final PlaythroughViewModel _playthroughViewModel; + late final String _playthroughId; final PlaythroughsStore _playthroughsStore; -// MK Doing a copy of player scores to prevent data from being updated without saving - List? _playerScores; - List get playerScores { - if (_playerScores == null) { - _playerScores = []; - for (final PlayerScore playerScore in _playthroughViewModel.playerScores!) { - final score = Score( - id: playerScore.score.id, - playerId: playerScore.score.playerId, - boardGameId: playerScore.score.boardGameId, - ); - - score.value = playerScore.score.value; - score.playthroughId = playerScore.score.playthroughId; - - _playerScores!.add(PlayerScore(playerScore.player, score)); - } - } + @observable + PlaythroughDetails? _updatedPlaythroughDetails; - return _playerScores!; - } + PlaythroughDetails get updatedPlaythroughDetails => _updatedPlaythroughDetails!; @computed - Playthrough get playthrough => _playthroughViewModel.playthrough; + PlaythroughDetails get playthroughDetails => + _playthroughsStore.playthroughs.firstWhere((pd) => pd.id == _playthroughId); @computed - DateTime get playthroughStartTime => playthrough.startDate; + Playthrough get playthrough => updatedPlaythroughDetails.playthrough; @computed - bool get playthoughEnded => playthrough.status == PlaythroughStatus.Finished; + ObservableList get playerScores => + updatedPlaythroughDetails.playerScores.asObservable(); @computed - Duration get playthoughDuration => - (playthrough.endDate ?? DateTime.now()).difference(playthrough.startDate); - - bool isDirty() { - bool playerScoresUpdated = false; - final Map playerScoresMap = { - for (final PlayerScore playerScore in _playthroughViewModel.playerScores!) - playerScore.player!.id: playerScore.score - }; - - for (final PlayerScore playerScore in playerScores) { - if (playerScoresMap[playerScore.player!.id]!.value != playerScore.score.value) { - playerScoresUpdated = true; - break; - } - } + DateTime get playthroughStartTime => updatedPlaythroughDetails.startDate; + + @computed + bool get playthoughEnded => updatedPlaythroughDetails.playthoughEnded; - return playerScoresUpdated || - _playthroughViewModel.playthrough.startDate != playthrough.startDate || - _playthroughViewModel.playthrough.endDate != playthrough.endDate; + @computed + Duration get playthoughDuration => (updatedPlaythroughDetails.endDate ?? DateTime.now()) + .difference(updatedPlaythroughDetails.startDate); + + bool get isDirty => updatedPlaythroughDetails != playthroughDetails; + + @action + void setPlaythroughId(String playthroughId) { + _playthroughId = playthroughId; + _updatedPlaythroughDetails = playthroughDetails.copyWith(); } @action Future stopPlaythrough() async { - await _playthroughViewModel.stopPlaythrough(); + final updatedPlaythrough = playthrough.copyWith( + status: PlaythroughStatus.Finished, + endDate: DateTime.now().toUtc(), + ); + _updatedPlaythroughDetails = + _updatedPlaythroughDetails?.copyWith(playthrough: updatedPlaythrough); + await _playthroughsStore.updatePlaythrough(_updatedPlaythroughDetails); } @action Future saveChanges() async { - await _playthroughViewModel.updatePlaythrough(playthrough, playerScores); + if (isDirty) { + await _playthroughsStore.updatePlaythrough(updatedPlaythroughDetails); + } } @action void updateStartDate(DateTime newStartDate) { - final Duration playthroughDuration = playthoughDuration; - playthrough.startDate = newStartDate; - playthrough.status = PlaythroughStatus.Finished; - playthrough.endDate = newStartDate.add(playthroughDuration); + final updatedPlaythrough = playthrough.copyWith( + startDate: newStartDate, + status: PlaythroughStatus.Finished, + endDate: newStartDate.add(playthoughDuration), + ); + _updatedPlaythroughDetails = + _updatedPlaythroughDetails?.copyWith(playthrough: updatedPlaythrough); } @action void updateDuration(int hoursPlayed, int minutesPlyed) { - playthrough.endDate = - playthrough.startDate.add(Duration(hours: hoursPlayed, minutes: minutesPlyed)); + final updatedPlaythrough = playthrough.copyWith( + endDate: + playthroughDetails.startDate.add(Duration(hours: hoursPlayed, minutes: minutesPlyed))); + + _updatedPlaythroughDetails = + _updatedPlaythroughDetails?.copyWith(playthrough: updatedPlaythrough); + } + + @action + void updatePlayerScore(PlayerScore playerScore, int newScore) { + if (playerScore.score.valueInt == newScore) { + return; + } + + final updatedPlayerScore = + playerScore.copyWith(score: playerScore.score.copyWith(value: newScore.toString())); + final playerScoreIndex = playerScores.indexOf(playerScore); + playerScores[playerScoreIndex] = updatedPlayerScore; + + _updatedPlaythroughDetails = updatedPlaythroughDetails.copyWith(playerScores: playerScores); } @action Future deletePlaythrough() async { - await _playthroughsStore.deletePlaythrough(playthrough.id); + await _playthroughsStore.deletePlaythrough(playthroughDetails.id); } } 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 3c8b28b7..8a56ca9e 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,6 +9,13 @@ 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; + + @override + PlaythroughDetails get playthroughDetails => (_$playthroughDetailsComputed ??= + Computed(() => super.playthroughDetails, + name: '_EditPlaythoughViewModel.playthroughDetails')) + .value; Computed? _$playthroughComputed; @override @@ -16,6 +23,13 @@ mixin _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { (_$playthroughComputed ??= Computed(() => super.playthrough, name: '_EditPlaythoughViewModel.playthrough')) .value; + Computed>? _$playerScoresComputed; + + @override + ObservableList get playerScores => (_$playerScoresComputed ??= + Computed>(() => super.playerScores, + name: '_EditPlaythoughViewModel.playerScores')) + .value; Computed? _$playthroughStartTimeComputed; @override @@ -38,6 +52,24 @@ mixin _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { name: '_EditPlaythoughViewModel.playthoughDuration')) .value; + late final _$_updatedPlaythroughDetailsAtom = Atom( + name: '_EditPlaythoughViewModel._updatedPlaythroughDetails', + context: context); + + @override + PlaythroughDetails? get _updatedPlaythroughDetails { + _$_updatedPlaythroughDetailsAtom.reportRead(); + return super._updatedPlaythroughDetails; + } + + @override + set _updatedPlaythroughDetails(PlaythroughDetails? value) { + _$_updatedPlaythroughDetailsAtom + .reportWrite(value, super._updatedPlaythroughDetails, () { + super._updatedPlaythroughDetails = value; + }); + } + late final _$stopPlaythroughAsyncAction = AsyncAction('_EditPlaythoughViewModel.stopPlaythrough', context: context); @@ -66,6 +98,17 @@ mixin _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { late final _$_EditPlaythoughViewModelActionController = ActionController(name: '_EditPlaythoughViewModel', context: context); + @override + void setPlaythroughId(String playthroughId) { + final _$actionInfo = _$_EditPlaythoughViewModelActionController.startAction( + name: '_EditPlaythoughViewModel.setPlaythroughId'); + try { + return super.setPlaythroughId(playthroughId); + } finally { + _$_EditPlaythoughViewModelActionController.endAction(_$actionInfo); + } + } + @override void updateStartDate(DateTime newStartDate) { final _$actionInfo = _$_EditPlaythoughViewModelActionController.startAction( @@ -88,10 +131,23 @@ mixin _$EditPlaythoughViewModel on _EditPlaythoughViewModel, Store { } } + @override + void updatePlayerScore(PlayerScore playerScore, int newScore) { + final _$actionInfo = _$_EditPlaythoughViewModelActionController.startAction( + name: '_EditPlaythoughViewModel.updatePlayerScore'); + try { + return super.updatePlayerScore(playerScore, newScore); + } finally { + _$_EditPlaythoughViewModelActionController.endAction(_$actionInfo); + } + } + @override String toString() { return ''' +playthroughDetails: ${playthroughDetails}, playthrough: ${playthrough}, +playerScores: ${playerScores}, playthroughStartTime: ${playthroughStartTime}, playthoughEnded: ${playthoughEnded}, playthoughDuration: ${playthoughDuration} diff --git a/board_games_companion/lib/pages/enter_score/enter_score_dialog.dart b/board_games_companion/lib/pages/enter_score/enter_score_dialog.dart index d8fcc566..f933285a 100644 --- a/board_games_companion/lib/pages/enter_score/enter_score_dialog.dart +++ b/board_games_companion/lib/pages/enter_score/enter_score_dialog.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:board_games_companion/widgets/elevated_container.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:sprintf/sprintf.dart'; import '../../common/app_colors.dart'; @@ -32,55 +32,60 @@ class EnterScoreDialog extends StatelessWidget { final enterScoreDialogWidth = max(_minWidth, min(width - 2 * Dimensions.doubleStandardSpacing, _maxWidth)); - return ChangeNotifierProvider.value( - value: viewModel, - builder: (_, __) { - return Center( - child: SizedBox( - width: enterScoreDialogWidth, - child: ElevatedContainer( - backgroundColor: AppColors.primaryColorLight, - elevation: AppStyles.defaultElevation, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Dimensions.doubleStandardSpacing, - vertical: Dimensions.standardSpacing, + return Center( + child: SizedBox( + width: enterScoreDialogWidth, + child: ElevatedContainer( + backgroundColor: AppColors.primaryColorLight, + elevation: AppStyles.defaultElevation, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Dimensions.doubleStandardSpacing, + vertical: Dimensions.standardSpacing, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Observer( + builder: (_) { + return _Score(playerName: viewModel.playerName, score: viewModel.score); + }, + ), + Observer( + builder: (_) { + return _ScoreHistory(partialScores: viewModel.partialScores); + }, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _Score(playerName: viewModel.playerName), - const _ScoreHistory(), - const SizedBox(height: Dimensions.trippleStandardSpacing), - _CircularNumberPicker( - strokeWidth: 50, - thumbSize: 50, - onEnded: (int partialScore) { + const SizedBox(height: Dimensions.trippleStandardSpacing), + _CircularNumberPicker( + strokeWidth: 50, + thumbSize: 50, + onEnded: (int partialScore) => viewModel.updateScore(partialScore), + ), + const SizedBox(height: Dimensions.doubleStandardSpacing), + Observer( + builder: (_) { + return _InstantScorePanel( + operation: viewModel.operation, + onOperationChange: (EnterScoreOperation operation) => + viewModel.updateOperation(operation), + onScoreChange: (int partialScore) { + if (viewModel.operation == EnterScoreOperation.subtract) { + partialScore = -partialScore; + } + viewModel.updateScore(partialScore); }, - ), - const SizedBox(height: Dimensions.doubleStandardSpacing), - Consumer( - builder: (_, viewModel, __) { - return _InstantScorePanel( - operation: viewModel.operation, - onOperationChange: (EnterScoreOperation operation) { - viewModel.updateOperation(operation); - }, - onScoreChange: (int partialScore) async { - if (viewModel.operation == EnterScoreOperation.subtract) { - partialScore = -partialScore; - } - - viewModel.updateScore(partialScore); - }, - ); - }, - ), - const SizedBox(height: Dimensions.trippleStandardSpacing), - _ActionButtons( + ); + }, + ), + const SizedBox(height: Dimensions.trippleStandardSpacing), + Observer( + builder: (_) { + return _ActionButtons( + canUndo: viewModel.canUndo, onUndo: () => viewModel.undo(), onDone: () { // MK In case score was not entered assume 0 was the score @@ -90,64 +95,65 @@ class EnterScoreDialog extends StatelessWidget { Navigator.pop(context); }, - ), - ], + ); + }, ), - ), + ], ), ), - ); - }, + ), + ), ); } } class _ScoreHistory extends StatelessWidget { const _ScoreHistory({ + required this.partialScores, Key? key, }) : super(key: key); static const double _height = 20; + final List partialScores; + @override Widget build(BuildContext context) { - return Consumer(builder: (_, viewModel, __) { - if (viewModel.partialScores.isEmpty) { - return const SizedBox(height: _height); - } + if (partialScores.isEmpty) { + return const SizedBox(height: _height); + } - return SizedBox( - height: _height, - child: Wrap( - alignment: WrapAlignment.center, - children: [ - Text('(', style: AppTheme.theme.textTheme.bodyText1), - for (var i = 0; i < viewModel.partialScores.length; i++) ...[ - Text( - viewModel.partialScores[i] > 0 - ? '+${viewModel.partialScores[i]}' - : '${viewModel.partialScores[i]}', - style: AppTheme.theme.textTheme.bodyText1, - ), - if (i != viewModel.partialScores.length - 1) ...[ - Text(', ', style: AppTheme.theme.textTheme.bodyText1), - ] - ], - Text(')', style: AppTheme.theme.textTheme.bodyText1), + return SizedBox( + height: _height, + child: Wrap( + alignment: WrapAlignment.center, + children: [ + Text('(', style: AppTheme.theme.textTheme.bodyText1), + for (var i = 0; i < partialScores.length; i++) ...[ + Text( + partialScores[i] > 0 ? '+${partialScores[i]}' : '${partialScores[i]}', + style: AppTheme.theme.textTheme.bodyText1, + ), + if (i != partialScores.length - 1) ...[ + Text(', ', style: AppTheme.theme.textTheme.bodyText1), + ] ], - ), - ); - }); + Text(')', style: AppTheme.theme.textTheme.bodyText1), + ], + ), + ); } } class _Score extends StatelessWidget { const _Score({ required this.playerName, + required this.score, Key? key, }) : super(key: key); final String? playerName; + final int score; @override Widget build(BuildContext context) { @@ -162,16 +168,12 @@ class _Score extends StatelessWidget { sprintf(AppText.editPlaythroughPlayerScored, [playerName]), style: AppTheme.theme.textTheme.headline1!.copyWith(fontWeight: FontWeight.normal), ), - Consumer( - builder: (_, viewModel, __) { - return Text( - ' ${viewModel.score} ', - style: AppTheme.theme.textTheme.headline1!.copyWith( - fontSize: Dimensions.doubleExtraLargeFontSize, - color: AppColors.accentColor, - ), - ); - }, + Text( + ' $score ', + style: AppTheme.theme.textTheme.headline1!.copyWith( + fontSize: Dimensions.doubleExtraLargeFontSize, + color: AppColors.accentColor, + ), ), Text( AppText.editPlaythroughScorePoints, @@ -298,11 +300,13 @@ class _InstantScoreTile extends StatelessWidget { class _ActionButtons extends StatelessWidget { const _ActionButtons({ + required this.canUndo, required this.onUndo, required this.onDone, Key? key, }) : super(key: key); + final bool canUndo; final VoidCallback onUndo; final VoidCallback onDone; @@ -310,15 +314,11 @@ class _ActionButtons extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Consumer( - builder: (_, viewModel, __) { - return ElevatedIconButton( - title: AppText.enterScoreDialogUndoButtonText, - icon: const Icon(Icons.undo), - color: AppColors.blueColor, - onPressed: viewModel.canUndo ? onUndo : null, - ); - }, + ElevatedIconButton( + title: AppText.enterScoreDialogUndoButtonText, + icon: const Icon(Icons.undo), + color: AppColors.blueColor, + onPressed: canUndo ? onUndo : null, ), const Expanded(child: SizedBox.shrink()), ElevatedIconButton( diff --git a/board_games_companion/lib/pages/enter_score/enter_score_view_model.dart b/board_games_companion/lib/pages/enter_score/enter_score_view_model.dart index 591705ea..8837a576 100644 --- a/board_games_companion/lib/pages/enter_score/enter_score_view_model.dart +++ b/board_games_companion/lib/pages/enter_score/enter_score_view_model.dart @@ -1,66 +1,75 @@ -import 'package:flutter/material.dart'; +// ignore_for_file: library_private_types_in_public_api + +import 'package:mobx/mobx.dart'; import '../../models/player_score.dart'; +part 'enter_score_view_model.g.dart'; + enum EnterScoreOperation { add, subtract, } -class EnterScoreViewModel with ChangeNotifier { - EnterScoreViewModel(this._playerScore) : _initialScore = _playerScore.score.valueInt; +class EnterScoreViewModel = _EnterScoreViewModel with _$EnterScoreViewModel; + +abstract class _EnterScoreViewModel with Store { + _EnterScoreViewModel(this._playerScore) : _initialScore = _playerScore.score.valueInt; final int _initialScore; - final PlayerScore _playerScore; - int? _score; - int get score { - _score ??= _playerScore.score.valueInt; - return _score!; - } + @observable + PlayerScore _playerScore; - String? get playerName => _playerScore.player?.name; + @observable + EnterScoreOperation operation = EnterScoreOperation.add; + + @observable + ObservableList partialScores = [].asObservable(); - EnterScoreOperation _operation = EnterScoreOperation.add; - EnterScoreOperation get operation => _operation; + @computed + int get score => _playerScore.score.valueInt; + @computed + String? get playerName => _playerScore.player?.name; + + @computed bool get canUndo => partialScores.isNotEmpty; + @computed bool get hasUnsavedChanged => partialScores.isNotEmpty; - List partialScores = []; - - void updateOperation(EnterScoreOperation operation) { - _operation = operation; - notifyListeners(); - } + @action + void updateOperation(EnterScoreOperation operation) => this.operation = operation; + @action void updateScore(int partialScore) { - _score = score + partialScore; - partialScores.add(partialScore); + final newScore = score + partialScore; + partialScores = ObservableList.of(partialScores..add(partialScore)); - _playerScore.updatePlayerScore(_score.toString()); - - notifyListeners(); + _updatePlayerScore(newScore); } + @action void scoreZero() { - _playerScore.updatePlayerScore(0.toString()); - - notifyListeners(); + _updatePlayerScore(0); } + @action void undo() { if (!canUndo) { return; } - partialScores.removeLast(); + partialScores = ObservableList.of(partialScores..removeLast()); - _score = _initialScore + _partialScoresSum; - _playerScore.updatePlayerScore(_score.toString()); + final newScore = _initialScore + _partialScoresSum; + _updatePlayerScore(newScore); + } - notifyListeners(); + void _updatePlayerScore(int? score) { + _playerScore = + _playerScore.copyWith(score: _playerScore.score.copyWith(value: score.toString())); } int get _partialScoresSum { diff --git a/board_games_companion/lib/pages/enter_score/enter_score_view_model.g.dart b/board_games_companion/lib/pages/enter_score/enter_score_view_model.g.dart new file mode 100644 index 00000000..a41f521e --- /dev/null +++ b/board_games_companion/lib/pages/enter_score/enter_score_view_model.g.dart @@ -0,0 +1,145 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enter_score_view_model.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$EnterScoreViewModel on _EnterScoreViewModel, Store { + Computed? _$scoreComputed; + + @override + int get score => (_$scoreComputed ??= + Computed(() => super.score, name: '_EnterScoreViewModel.score')) + .value; + Computed? _$playerNameComputed; + + @override + String? get playerName => + (_$playerNameComputed ??= Computed(() => super.playerName, + name: '_EnterScoreViewModel.playerName')) + .value; + Computed? _$canUndoComputed; + + @override + bool get canUndo => (_$canUndoComputed ??= Computed(() => super.canUndo, + name: '_EnterScoreViewModel.canUndo')) + .value; + Computed? _$hasUnsavedChangedComputed; + + @override + bool get hasUnsavedChanged => (_$hasUnsavedChangedComputed ??= Computed( + () => super.hasUnsavedChanged, + name: '_EnterScoreViewModel.hasUnsavedChanged')) + .value; + + late final _$_playerScoreAtom = + Atom(name: '_EnterScoreViewModel._playerScore', context: context); + + @override + PlayerScore get _playerScore { + _$_playerScoreAtom.reportRead(); + return super._playerScore; + } + + @override + set _playerScore(PlayerScore value) { + _$_playerScoreAtom.reportWrite(value, super._playerScore, () { + super._playerScore = value; + }); + } + + late final _$operationAtom = + Atom(name: '_EnterScoreViewModel.operation', context: context); + + @override + EnterScoreOperation get operation { + _$operationAtom.reportRead(); + return super.operation; + } + + @override + set operation(EnterScoreOperation value) { + _$operationAtom.reportWrite(value, super.operation, () { + super.operation = value; + }); + } + + late final _$partialScoresAtom = + Atom(name: '_EnterScoreViewModel.partialScores', context: context); + + @override + ObservableList get partialScores { + _$partialScoresAtom.reportRead(); + return super.partialScores; + } + + @override + set partialScores(ObservableList value) { + _$partialScoresAtom.reportWrite(value, super.partialScores, () { + super.partialScores = value; + }); + } + + late final _$_EnterScoreViewModelActionController = + ActionController(name: '_EnterScoreViewModel', context: context); + + @override + void updateOperation(EnterScoreOperation operation) { + final _$actionInfo = _$_EnterScoreViewModelActionController.startAction( + name: '_EnterScoreViewModel.updateOperation'); + try { + return super.updateOperation(operation); + } finally { + _$_EnterScoreViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void updateScore(int partialScore) { + final _$actionInfo = _$_EnterScoreViewModelActionController.startAction( + name: '_EnterScoreViewModel.updateScore'); + try { + return super.updateScore(partialScore); + } finally { + _$_EnterScoreViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void scoreZero() { + final _$actionInfo = _$_EnterScoreViewModelActionController.startAction( + name: '_EnterScoreViewModel.scoreZero'); + try { + return super.scoreZero(); + } finally { + _$_EnterScoreViewModelActionController.endAction(_$actionInfo); + } + } + + @override + void undo() { + final _$actionInfo = _$_EnterScoreViewModelActionController.startAction( + name: '_EnterScoreViewModel.undo'); + try { + return super.undo(); + } finally { + _$_EnterScoreViewModelActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +operation: ${operation}, +partialScores: ${partialScores}, +score: ${score}, +playerName: ${playerName}, +canUndo: ${canUndo}, +hasUnsavedChanged: ${hasUnsavedChanged} + '''; + } +} diff --git a/board_games_companion/lib/pages/games/games_page.dart b/board_games_companion/lib/pages/games/games_page.dart index 5cb6d002..2532e106 100644 --- a/board_games_companion/lib/pages/games/games_page.dart +++ b/board_games_companion/lib/pages/games/games_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:basics/basics.dart'; import 'package:board_games_companion/common/app_text.dart'; import 'package:board_games_companion/injectable.dart'; import 'package:board_games_companion/pages/games/games_view_model.dart'; @@ -8,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; import 'package:sprintf/sprintf.dart'; import 'package:tuple/tuple.dart'; @@ -27,7 +27,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/user_store.dart'; import '../../widgets/board_games/board_game_tile.dart'; import '../../widgets/common/bgg_community_member_text_widget.dart'; import '../../widgets/common/bgg_community_member_user_name_text_field_widget.dart'; @@ -56,7 +55,6 @@ typedef BoardGameResultAction = void Function( class GamesPage extends StatefulWidget { const GamesPage( this.viewModel, - this.userStore, this.boardGamesFiltersStore, this.analyticsService, this.rateAndReviewService, { @@ -64,7 +62,6 @@ class GamesPage extends StatefulWidget { }) : super(key: key); final GamesViewModel viewModel; - final UserStore userStore; final BoardGamesFiltersStore boardGamesFiltersStore; final AnalyticsService analyticsService; final RateAndReviewService rateAndReviewService; @@ -100,7 +97,7 @@ class GamesPageState extends State return const Center(child: GenericErrorMessage()); case FutureStatus.fulfilled: if (!widget.viewModel.anyBoardGamesInCollections && - (widget.userStore.user?.name.isEmpty ?? true)) { + (widget.viewModel.isUserNameEmpty)) { return const _Empty(); } @@ -177,7 +174,15 @@ class _Collection extends StatelessWidget { analyticsService: analyticsService, rateAndReviewService: rateAndReviewService, ), - if (isCollectionEmpty) _EmptyCollection(selectedTab: selectedTab), + if (isCollectionEmpty) + Observer( + builder: (_) { + return _EmptyCollection( + selectedTab: selectedTab, + userName: viewModel.userName, + ); + }, + ), if (!isCollectionEmpty) ...[ if (hasMainGames) ...[ SliverPersistentHeader( @@ -497,9 +502,11 @@ class _EmptyCollection extends StatelessWidget { const _EmptyCollection({ Key? key, required this.selectedTab, + required this.userName, }) : super(key: key); final GamesTab selectedTab; + final String? userName; @override Widget build(BuildContext context) { @@ -509,60 +516,52 @@ class _EmptyCollection extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Consumer( - builder: (_, userStore, __) { - return Observer( - builder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text.rich( - TextSpan( - children: [ - const TextSpan( - text: "It looks like you don't have any board games in your ", - ), - TextSpan( - text: selectedTab.toCollectionType().toHumandReadableText(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: ' collection yet.'), - if (selectedTab == GamesTab.wishlist && - (userStore.user?.name.isNotEmpty ?? false)) ...[ - const TextSpan( - text: "\n\nIf you want to see board games from BGG's "), - const TextSpan( - text: 'Wishlist ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: 'or '), - const TextSpan( - text: 'Want to Buy ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: 'collections then tap the below '), - const TextSpan( - text: '${AppText.importCollectionsButtonText} ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: 'button.'), - ] - ], + Observer( + builder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text.rich( + TextSpan( + children: [ + const TextSpan( + text: "It looks like you don't have any board games in your ", ), - textAlign: TextAlign.justify, - ), - if (selectedTab == GamesTab.wishlist && - (userStore.user?.name.isNotEmpty ?? false)) ...[ - const SizedBox(height: Dimensions.doubleStandardSpacing), - Align( - alignment: Alignment.centerRight, - child: ImportCollectionsButton( - usernameCallback: () => userStore.user!.name), + TextSpan( + text: selectedTab.toCollectionType().toHumandReadableText(), + style: const TextStyle(fontWeight: FontWeight.bold), ), - ] - ], - ); - }, + const TextSpan(text: ' collection yet.'), + if (selectedTab == GamesTab.wishlist && (userName.isNotNullOrBlank)) ...[ + const TextSpan(text: "\n\nIf you want to see board games from BGG's "), + const TextSpan( + text: 'Wishlist ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan(text: 'or '), + const TextSpan( + text: 'Want to Buy ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan(text: 'collections then tap the below '), + const TextSpan( + text: '${AppText.importCollectionsButtonText} ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan(text: 'button.'), + ] + ], + ), + textAlign: TextAlign.justify, + ), + if (selectedTab == GamesTab.wishlist && (userName.isNotNullOrBlank)) ...[ + const SizedBox(height: Dimensions.doubleStandardSpacing), + Align( + alignment: Alignment.centerRight, + child: ImportCollectionsButton(usernameCallback: () => userName!), + ), + ] + ], ); }, ), diff --git a/board_games_companion/lib/pages/games/games_view_model.dart b/board_games_companion/lib/pages/games/games_view_model.dart index 68711b8c..dd3b25cb 100644 --- a/board_games_companion/lib/pages/games/games_view_model.dart +++ b/board_games_companion/lib/pages/games/games_view_model.dart @@ -2,11 +2,13 @@ import 'dart:math'; +import 'package:basics/basics.dart'; 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/extensions/string_extensions.dart'; import 'package:board_games_companion/models/sort_by.dart'; import 'package:board_games_companion/stores/board_games_filters_store.dart'; +import 'package:board_games_companion/stores/user_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; @@ -29,10 +31,12 @@ class GamesViewModel = _GamesViewModel with _$GamesViewModel; abstract class _GamesViewModel with Store { _GamesViewModel( + this._userStore, this._boardGamesStore, this._boardGamesFiltersStore, ); + final UserStore _userStore; final BoardGamesStore _boardGamesStore; final BoardGamesFiltersStore _boardGamesFiltersStore; @@ -209,15 +213,21 @@ abstract class _GamesViewModel with Store { @observable GamesTab selectedTab = GamesTab.owned; - @action - void setSelectedTab(GamesTab newlySelectedTab) => selectedTab = newlySelectedTab; + @observable + ObservableFuture? futureLoadBoardGames; @computed List get _allExpansions => allBoardGames.where((boardGame) => boardGame.isExpansion ?? false).toList(); - @observable - ObservableFuture? futureLoadBoardGames; + @computed + String? get userName => _userStore.user?.name; + + @computed + bool get isUserNameEmpty => userName.isNullOrBlank; + + @action + void setSelectedTab(GamesTab newlySelectedTab) => selectedTab = newlySelectedTab; @action void loadBoardGames() => futureLoadBoardGames = ObservableFuture(_loadBoardGames()); @@ -243,6 +253,7 @@ abstract class _GamesViewModel with Store { Future _loadBoardGames() async { try { + await _userStore.loadUser(); await _boardGamesStore.loadBoardGames(); await _boardGamesFiltersStore.loadFilterPreferences(); } catch (e, stack) { diff --git a/board_games_companion/lib/pages/games/games_view_model.g.dart b/board_games_companion/lib/pages/games/games_view_model.g.dart index 43c52d95..a09256d2 100644 --- a/board_games_companion/lib/pages/games/games_view_model.g.dart +++ b/board_games_companion/lib/pages/games/games_view_model.g.dart @@ -192,6 +192,20 @@ mixin _$GamesViewModel on _GamesViewModel, Store { Computed>(() => super._allExpansions, name: '_GamesViewModel._allExpansions')) .value; + Computed? _$userNameComputed; + + @override + String? get userName => + (_$userNameComputed ??= Computed(() => super.userName, + name: '_GamesViewModel.userName')) + .value; + Computed? _$isUserNameEmptyComputed; + + @override + bool get isUserNameEmpty => + (_$isUserNameEmptyComputed ??= Computed(() => super.isUserNameEmpty, + name: '_GamesViewModel.isUserNameEmpty')) + .value; late final _$selectedTabAtom = Atom(name: '_GamesViewModel.selectedTab', context: context); @@ -332,7 +346,9 @@ anyMainGamesInCollection: ${anyMainGamesInCollection}, totalMainGamesInCollection: ${totalMainGamesInCollection}, totalExpansionsInCollection: ${totalExpansionsInCollection}, anyExpansionsInCollection: ${anyExpansionsInCollection}, -expansionsInCollectionMap: ${expansionsInCollectionMap} +expansionsInCollectionMap: ${expansionsInCollectionMap}, +userName: ${userName}, +isUserNameEmpty: ${isUserNameEmpty} '''; } } diff --git a/board_games_companion/lib/pages/home/home_page.dart b/board_games_companion/lib/pages/home/home_page.dart index c3b85651..82ec9447 100644 --- a/board_games_companion/lib/pages/home/home_page.dart +++ b/board_games_companion/lib/pages/home/home_page.dart @@ -3,14 +3,12 @@ import 'package:board_games_companion/pages/players/players_view_model.dart'; import 'package:board_games_companion/pages/search_board_games/search_board_games_view_model.dart'; import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../../common/app_colors.dart'; import '../../common/dimensions.dart'; import '../../services/analytics_service.dart'; import '../../services/rate_and_review_service.dart'; import '../../stores/board_games_filters_store.dart'; -import '../../stores/user_store.dart'; import '../../widgets/bottom_tab_icon.dart'; import '../../widgets/common/page_container_widget.dart'; import '../base_page_state.dart'; @@ -74,16 +72,11 @@ class HomePageState extends BasePageState with SingleTickerProviderSta child: TabBarView( controller: tabController, children: [ - Consumer( - builder: (_, userStore, __) { - return GamesPage( - widget.gamesViewModel, - userStore, - widget.boardGamesFiltersStore, - widget.analyticsService, - widget.rateAndReviewService, - ); - }, + GamesPage( + widget.gamesViewModel, + widget.boardGamesFiltersStore, + widget.analyticsService, + widget.rateAndReviewService, ), SearchBoardGamesPage(viewModel: widget.searchViewModel), PlayersPage(playersViewModel: widget.playersViewModel), diff --git a/board_games_companion/lib/pages/players/player_page.dart b/board_games_companion/lib/pages/players/player_page.dart index 7f9621d1..b5c5ed51 100644 --- a/board_games_companion/lib/pages/players/player_page.dart +++ b/board_games_companion/lib/pages/players/player_page.dart @@ -70,9 +70,7 @@ class PlayerPageState extends BasePageState { final hasName = nameController.text.isNotEmpty; return WillPopScope( - onWillPop: () async { - return _handleOnWillPop(context, player); - }, + onWillPop: () async => _handleOnWillPop(context, player), child: Scaffold( appBar: AppBar( title: Text( @@ -117,9 +115,7 @@ class PlayerPageState extends BasePageState { ), onTap: () => _handleImagePicking(player), ), - const Divider( - indent: Dimensions.halfStandardSpacing, - ), + const Divider(indent: Dimensions.halfStandardSpacing), CustomIconButton( const Icon( Icons.camera, @@ -165,9 +161,7 @@ class PlayerPageState extends BasePageState { style: AppTheme.defaultTextFieldStyle, ), ], - const Expanded( - child: SizedBox.shrink(), - ), + const Expanded(child: SizedBox.shrink()), _ActionButtons( isEditMode: isEditMode, onCreate: (BuildContext context) => _createOrUpdatePlayer(context), diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart index c860e5e1..cfea2d40 100644 --- a/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthrough_statistics_view_model.dart @@ -178,8 +178,8 @@ abstract class _PlaythroughStatisticsViewModel with Store { } boardGameStatistics.lastWinner = PlayerScore( - playersById[lastPlaythroughBestScore.playerId], - lastPlaythroughBestScore, + player: playersById[lastPlaythroughBestScore.playerId], + score: lastPlaythroughBestScore, ); } diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthrough_view_model.dart deleted file mode 100644 index 6335dfc0..00000000 --- a/board_games_companion/lib/pages/playthroughs/playthrough_view_model.dart +++ /dev/null @@ -1,123 +0,0 @@ -// ignore_for_file: library_private_types_in_public_api - -import 'package:board_games_companion/common/enums/game_winning_condition.dart'; -import 'package:collection/collection.dart' show IterableExtension; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:injectable/injectable.dart'; -import 'package:mobx/mobx.dart'; - -import '../../common/enums/playthrough_status.dart'; -import '../../extensions/scores_extensions.dart'; -import '../../models/hive/player.dart'; -import '../../models/hive/playthrough.dart'; -import '../../models/hive/score.dart'; -import '../../models/player_score.dart'; -import '../../services/player_service.dart'; -import '../../services/score_service.dart'; -import '../../stores/playthroughs_store.dart'; - -part 'playthrough_view_model.g.dart'; - -@injectable -class PlaythroughViewModel = _PlaythroughViewModel with _$PlaythroughViewModel; - -abstract class _PlaythroughViewModel with Store { - _PlaythroughViewModel( - this._playerService, - this._scoreService, - this._playthroughsStore, - ); - - final PlayerService _playerService; - final ScoreService _scoreService; - final PlaythroughsStore _playthroughsStore; - - // MK Can't use late with mobx becuase of this issue https://github.com/mobxjs/mobx.dart/issues/598 - @observable - Playthrough? _playthrough; - - @computed - Playthrough get playthrough => _playthrough!; - - int? _daysSinceStart; - int? get daysSinceStart => _daysSinceStart; - - Duration get duration { - final nowUtc = DateTime.now().toUtc(); - final playthroughEndDate = playthrough.endDate ?? nowUtc; - return playthroughEndDate.difference(playthrough.startDate); - } - - @computed - bool get playthoughEnded => playthrough.status == PlaythroughStatus.Finished; - - @observable - ObservableList? scores; - - @observable - ObservableList? players; - - @observable - ObservableList? playerScores; - - @observable - ObservableFuture? futureLoadPlaythrough; - - @action - void loadPlaythrough(Playthrough playthrough) => - futureLoadPlaythrough = ObservableFuture(_loadPlaythrough(playthrough)); - - Future _loadPlaythrough(Playthrough playthrough) async { - if (playthrough.id.isEmpty) { - return; - } - - final nowUtc = DateTime.now().toUtc(); - _playthrough = playthrough; - _daysSinceStart = nowUtc.difference(_playthrough!.startDate).inDays; - - try { - scores = ObservableList.of((await _scoreService.retrieveScores([_playthrough!.id])) - ..sortByScore(_playthroughsStore.boardGame.settings?.winningCondition ?? - GameWinningCondition.HighestScore) - ..toList()); - players = ObservableList.of(await _playerService.retrievePlayers( - playerIds: _playthrough!.playerIds, - includeDeleted: true, - )); - - playerScores = ObservableList.of(scores!.mapIndexed((int index, Score score) { - final player = players!.firstWhereOrNull((Player p) => score.playerId == p.id); - return PlayerScore.withPlace(player, score, index + 1); - }).toList()); - } catch (e, stack) { - FirebaseCrashlytics.instance.recordError(e, stack); - } - } - - Future updatePlaythrough(Playthrough playthrough, List playerScores) async { - await _playthroughsStore.updatePlaythrough(playthrough); - for (final PlayerScore playerScore in playerScores) { - await _scoreService.addOrUpdateScore(playerScore.score); - } - - // MK Reload all playthrough data from Hive's db - loadPlaythrough(playthrough); - } - - Future stopPlaythrough() async { - final oldStatus = playthrough.status; - - playthrough.status = PlaythroughStatus.Finished; - playthrough.endDate = DateTime.now().toUtc(); - - final updateSucceeded = await _playthroughsStore.updatePlaythrough(playthrough); - if (!updateSucceeded) { - playthrough.status = oldStatus; - playthrough.endDate = null; - return false; - } - - return true; - } -} diff --git a/board_games_companion/lib/pages/playthroughs/playthrough_view_model.g.dart b/board_games_companion/lib/pages/playthroughs/playthrough_view_model.g.dart deleted file mode 100644 index 2b38ef8f..00000000 --- a/board_games_companion/lib/pages/playthroughs/playthrough_view_model.g.dart +++ /dev/null @@ -1,133 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'playthrough_view_model.dart'; - -// ************************************************************************** -// StoreGenerator -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers - -mixin _$PlaythroughViewModel on _PlaythroughViewModel, Store { - Computed? _$playthroughComputed; - - @override - Playthrough get playthrough => - (_$playthroughComputed ??= Computed(() => super.playthrough, - name: '_PlaythroughViewModel.playthrough')) - .value; - Computed? _$playthoughEndedComputed; - - @override - bool get playthoughEnded => - (_$playthoughEndedComputed ??= Computed(() => super.playthoughEnded, - name: '_PlaythroughViewModel.playthoughEnded')) - .value; - - late final _$_playthroughAtom = - Atom(name: '_PlaythroughViewModel._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 _$scoresAtom = - Atom(name: '_PlaythroughViewModel.scores', context: context); - - @override - ObservableList? get scores { - _$scoresAtom.reportRead(); - return super.scores; - } - - @override - set scores(ObservableList? value) { - _$scoresAtom.reportWrite(value, super.scores, () { - super.scores = value; - }); - } - - late final _$playersAtom = - Atom(name: '_PlaythroughViewModel.players', context: context); - - @override - ObservableList? get players { - _$playersAtom.reportRead(); - return super.players; - } - - @override - set players(ObservableList? value) { - _$playersAtom.reportWrite(value, super.players, () { - super.players = value; - }); - } - - late final _$playerScoresAtom = - Atom(name: '_PlaythroughViewModel.playerScores', context: context); - - @override - ObservableList? get playerScores { - _$playerScoresAtom.reportRead(); - return super.playerScores; - } - - @override - set playerScores(ObservableList? value) { - _$playerScoresAtom.reportWrite(value, super.playerScores, () { - super.playerScores = value; - }); - } - - late final _$futureLoadPlaythroughAtom = Atom( - name: '_PlaythroughViewModel.futureLoadPlaythrough', context: context); - - @override - ObservableFuture? get futureLoadPlaythrough { - _$futureLoadPlaythroughAtom.reportRead(); - return super.futureLoadPlaythrough; - } - - @override - set futureLoadPlaythrough(ObservableFuture? value) { - _$futureLoadPlaythroughAtom.reportWrite(value, super.futureLoadPlaythrough, - () { - super.futureLoadPlaythrough = value; - }); - } - - late final _$_PlaythroughViewModelActionController = - ActionController(name: '_PlaythroughViewModel', context: context); - - @override - void loadPlaythrough(Playthrough playthrough) { - final _$actionInfo = _$_PlaythroughViewModelActionController.startAction( - name: '_PlaythroughViewModel.loadPlaythrough'); - try { - return super.loadPlaythrough(playthrough); - } finally { - _$_PlaythroughViewModelActionController.endAction(_$actionInfo); - } - } - - @override - String toString() { - return ''' -scores: ${scores}, -players: ${players}, -playerScores: ${playerScores}, -futureLoadPlaythrough: ${futureLoadPlaythrough}, -playthrough: ${playthrough}, -playthoughEnded: ${playthoughEnded} - '''; - } -} diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_history_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_history_page.dart index edf655ee..855d633c 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_history_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_history_page.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:board_games_companion/models/playthrough_details.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -13,7 +14,6 @@ import '../../models/navigation/edit_playthrough_page_arguments.dart'; import '../../utilities/periodic_boardcast_stream.dart'; import '../../widgets/common/default_icon.dart'; 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/panel_container_widget.dart'; import '../../widgets/common/text/item_property_title_widget.dart'; @@ -21,7 +21,6 @@ import '../../widgets/common/text/item_property_value_widget.dart'; import '../../widgets/playthrough/calendar_card.dart'; import '../../widgets/playthrough/player_score_rank_avatar.dart'; import '../edit_playthrough/edit_playthrough_page.dart'; -import 'playthrough_view_model.dart'; import 'playthroughs_history_view_model.dart'; class PlaythroughsHistoryPage extends StatefulWidget { @@ -58,27 +57,21 @@ class PlaythroughsHistoryPageState extends State { ); } - return Observer( - builder: (_) { - return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: Dimensions.standardSpacing), - itemBuilder: (_, index) { - final playthroughViewModel = getIt(); - playthroughViewModel.loadPlaythrough(viewModel.playthroughs[index]); - - return _Playthrough( - viewModel: playthroughViewModel, - playthroughNumber: viewModel.playthroughs.length - index, - isLast: index == viewModel.playthroughs.length - 1, - ); - }, - separatorBuilder: (_, index) { - return const SizedBox(height: Dimensions.doubleStandardSpacing); - }, - itemCount: viewModel.playthroughs.length, + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: Dimensions.standardSpacing), + itemBuilder: (_, index) { + return _Playthrough( + playthroughDetails: viewModel.playthroughs[index], + playthroughNumber: viewModel.playthroughs.length - index, + isLast: index == viewModel.playthroughs.length - 1, ); }, + separatorBuilder: (_, index) { + return const SizedBox(height: Dimensions.doubleStandardSpacing); + }, + itemCount: viewModel.playthroughs.length, ); + default: return const SizedBox.shrink(); } @@ -89,13 +82,13 @@ class PlaythroughsHistoryPageState extends State { class _Playthrough extends StatefulWidget { const _Playthrough({ - required this.viewModel, + required this.playthroughDetails, required this.isLast, this.playthroughNumber, Key? key, }) : super(key: key); - final PlaythroughViewModel viewModel; + final PlaythroughDetails playthroughDetails; final int? playthroughNumber; final bool isLast; @@ -122,32 +115,19 @@ class _PlaythroughState extends State<_Playthrough> { ), child: Padding( padding: const EdgeInsets.all(Dimensions.standardSpacing), - child: Observer( - builder: (_) { - switch (widget.viewModel.futureLoadPlaythrough?.status ?? FutureStatus.pending) { - case FutureStatus.pending: - return const LoadingIndicator(); - - case FutureStatus.rejected: - return const Center(child: GenericErrorMessage()); - - case FutureStatus.fulfilled: - return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _PlaythroughGameStats( - playthroughViewModel: widget.viewModel, - playthroughNumber: widget.playthroughNumber, - ), - const SizedBox(width: Dimensions.doubleStandardSpacing), - Expanded( - child: _PlaythroughPlayersStats(playthroughViewModel: widget.viewModel), - ), - ], - ); - } - }, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _PlaythroughGameStats( + playthroughDetails: widget.playthroughDetails, + playthroughNumber: widget.playthroughNumber, + ), + const SizedBox(width: Dimensions.doubleStandardSpacing), + Expanded( + child: _PlaythroughPlayersStats(playthroughDetails: widget.playthroughDetails), + ), + ], ), ), ), @@ -159,17 +139,17 @@ class _PlaythroughState extends State<_Playthrough> { class _PlaythroughPlayersStats extends StatelessWidget { const _PlaythroughPlayersStats({ Key? key, - required this.playthroughViewModel, + required this.playthroughDetails, }) : super(key: key); - final PlaythroughViewModel playthroughViewModel; + final PlaythroughDetails playthroughDetails; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded(child: _PlaythroughPlayerList(playthroughViewModel: playthroughViewModel)), + Expanded(child: _PlaythroughPlayerList(playthroughDetails: playthroughDetails)), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -179,8 +159,11 @@ class _PlaythroughPlayersStats extends StatelessWidget { color: AppColors.accentColor, onPressed: () => Navigator.pushNamed( context, - EditPlaythoughPage.pageRoute, - arguments: EditPlaythroughPageArguments(playthroughViewModel), + EditPlaythroughPage.pageRoute, + arguments: EditPlaythroughPageArguments( + playthroughDetails.id, + playthroughDetails.boardGameId, + ), ), ), ], @@ -193,26 +176,26 @@ class _PlaythroughPlayersStats extends StatelessWidget { class _PlaythroughPlayerList extends StatelessWidget { const _PlaythroughPlayerList({ Key? key, - required PlaythroughViewModel playthroughViewModel, - }) : _playthroughStore = playthroughViewModel, + required PlaythroughDetails playthroughDetails, + }) : _playthroughDetails = playthroughDetails, super(key: key); - final PlaythroughViewModel _playthroughStore; + final PlaythroughDetails _playthroughDetails; @override Widget build(BuildContext context) { return ListView.separated( scrollDirection: Axis.horizontal, - itemCount: _playthroughStore.playerScores?.length ?? 0, + itemCount: _playthroughDetails.playerScores.length, separatorBuilder: (context, index) { return const SizedBox(width: Dimensions.doubleStandardSpacing); }, itemBuilder: (context, index) { return PlayerScoreRankAvatar( - player: _playthroughStore.playerScores![index].player, - playerHeroIdSuffix: _playthroughStore.playthrough.id, - rank: _playthroughStore.playerScores![index].place, - score: _playthroughStore.playerScores![index].score.value, + player: _playthroughDetails.playerScores[index].player, + playerHeroIdSuffix: _playthroughDetails.id, + rank: _playthroughDetails.playerScores[index].place, + score: _playthroughDetails.playerScores[index].score.value, ); }, ); @@ -222,11 +205,11 @@ class _PlaythroughPlayerList extends StatelessWidget { class _PlaythroughGameStats extends StatelessWidget { const _PlaythroughGameStats({ Key? key, - required this.playthroughViewModel, + required this.playthroughDetails, required this.playthroughNumber, }) : super(key: key); - final PlaythroughViewModel playthroughViewModel; + final PlaythroughDetails playthroughDetails; final int? playthroughNumber; @override @@ -235,16 +218,16 @@ class _PlaythroughGameStats extends StatelessWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - CalendarCard(playthroughViewModel.playthrough.startDate), + CalendarCard(playthroughDetails.startDate), _PlaythroughItemDetail( - playthroughViewModel.daysSinceStart?.toString(), + playthroughDetails.daysSinceStart?.toString(), 'day(s) ago', ), _PlaythroughItemDetail( '$playthroughNumber${playthroughNumber.toOrdinalAbbreviations()}', 'game', ), - _PlaythroughDuration(viewModel: playthroughViewModel), + _PlaythroughDuration(playthroughDetails: playthroughDetails), ], ); } @@ -252,11 +235,11 @@ class _PlaythroughGameStats extends StatelessWidget { class _PlaythroughDuration extends StatefulWidget { const _PlaythroughDuration({ - required this.viewModel, + required this.playthroughDetails, Key? key, }) : super(key: key); - final PlaythroughViewModel viewModel; + final PlaythroughDetails playthroughDetails; @override _PlaythroughDurationState createState() => _PlaythroughDurationState(); @@ -270,14 +253,14 @@ class _PlaythroughDurationState extends State<_PlaythroughDuration> { void initState() { super.initState(); - if (!widget.viewModel.playthoughEnded) { + if (!widget.playthroughDetails.playthoughEnded) { periodicBroadcastStream.stream.listen(_updateDuration); } } @override void dispose() { - if (!widget.viewModel.playthoughEnded) { + if (!widget.playthroughDetails.playthoughEnded) { periodicBroadcastStream.dispose(); } @@ -287,7 +270,7 @@ class _PlaythroughDurationState extends State<_PlaythroughDuration> { @override Widget build(BuildContext context) { return _PlaythroughItemDetail( - widget.viewModel.duration.inSeconds.toPlaytimeDuration(), + widget.playthroughDetails.duration.inSeconds.toPlaytimeDuration(), 'duration', ); } diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart index 3a4048ba..3c448293 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.dart @@ -1,10 +1,11 @@ // ignore_for_file: library_private_types_in_public_api -import 'package:board_games_companion/models/hive/playthrough.dart'; import 'package:board_games_companion/stores/playthroughs_store.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; +import '../../models/playthrough_details.dart'; + part 'playthroughs_history_view_model.g.dart'; @injectable @@ -20,7 +21,7 @@ abstract class _PlaythroughsHistoryViewModel with Store { ObservableFuture? futureloadPlaythroughs; @computed - ObservableList get playthroughs { + ObservableList get playthroughs { final sortedPlaythrough = List.of(_playthroughsStore.playthroughs, growable: false); return ObservableList.of(sortedPlaythrough..sort((a, b) => b.startDate.compareTo(a.startDate))); } diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.g.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.g.dart index f155d6fb..738efc8a 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.g.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_history_view_model.g.dart @@ -9,13 +9,14 @@ part of 'playthroughs_history_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 _$PlaythroughsHistoryViewModel on _PlaythroughsHistoryViewModel, Store { - Computed>? _$playthroughsComputed; + Computed>? _$playthroughsComputed; @override - ObservableList get playthroughs => (_$playthroughsComputed ??= - Computed>(() => super.playthroughs, + ObservableList get playthroughs => + (_$playthroughsComputed ??= Computed>( + () => super.playthroughs, name: '_PlaythroughsHistoryViewModel.playthroughs')) - .value; + .value; Computed? _$hasAnyPlaythroughsComputed; @override 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 56cc05d7..4d1ac22b 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,6 @@ import 'dart:math' as math; -import 'package:board_games_companion/models/hive/playthrough.dart'; +import 'package:board_games_companion/models/playthrough_details.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -242,7 +242,7 @@ class _LogPlaythroughStepperState extends State<_LogPlaythroughStepper> { completedSteps = widget.viewModel.logGameStep; }); } else { - final Playthrough? newPlaythrough = + final PlaythroughDetails? newPlaythrough = await widget.viewModel.createPlaythrough(widget.viewModel.boardGame.id); if (!mounted) { return; @@ -711,16 +711,9 @@ class _PlayerScore extends StatelessWidget { const SizedBox(width: Dimensions.doubleStandardSpacing), Column( children: [ - ChangeNotifierProvider.value( - value: playerScore, - child: Consumer( - builder: (_, PlayerScore playerScore, __) { - return Text( - '${playerScore.score.valueInt}', - style: AppStyles.playerScoreTextStyle, - ); - }, - ), + Text( + '${playerScore.score.valueInt}', + style: AppStyles.playerScoreTextStyle, ), const SizedBox(height: Dimensions.halfStandardSpacing), Text('points', style: AppTheme.theme.textTheme.bodyText2), 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 61870b88..4f3caf22 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 @@ -8,9 +8,9 @@ import 'package:uuid/uuid.dart'; import '../../common/analytics.dart'; import '../../models/hive/board_game_details.dart'; -import '../../models/hive/playthrough.dart'; import '../../models/hive/score.dart'; import '../../models/player_score.dart'; +import '../../models/playthrough_details.dart'; import '../../models/playthrough_player.dart'; import '../../services/analytics_service.dart'; import 'playthroughs_log_game_page.dart'; @@ -35,8 +35,8 @@ abstract class _PlaythroughsLogGameViewModel with Store { List get _selectedPlaythroughPlayers => playthroughPlayers.where((player) => player.isChecked).toList(); - final Map _playerScores = {}; - Map get playerScores => _playerScores; + @observable + ObservableMap playerScores = ObservableMap.of({}); @observable DateTime playthroughDate = DateTime.now(); @@ -69,14 +69,17 @@ abstract class _PlaythroughsLogGameViewModel with Store { @action void selectPlayer(PlaythroughPlayer playthroughPlayer) { - final playerScore = Score( + final score = Score( id: const Uuid().v4(), playerId: playthroughPlayer.player.id, boardGameId: _playthroughsStore.boardGame.id, ); playthroughPlayer.isChecked = true; - playerScores[playthroughPlayer.player.id] = PlayerScore(playthroughPlayer.player, playerScore); + playerScores[playthroughPlayer.player.id] = PlayerScore( + player: playthroughPlayer.player, + score: score, + ); } @action @@ -92,8 +95,8 @@ abstract class _PlaythroughsLogGameViewModel with Store { futureLoadPlaythroughPlayers = ObservableFuture(_loadPlaythroughPlayers()); @action - Future createPlaythrough(String boardGameId) async { - final Playthrough? newPlaythrough = await _playthroughsStore.createPlaythrough( + Future createPlaythrough(String boardGameId) async { + final PlaythroughDetails? newPlaythrough = await _playthroughsStore.createPlaythrough( boardGameId, _selectedPlaythroughPlayers, playerScores, 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 8c6ad628..4ff28b20 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 @@ -33,6 +33,22 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { name: '_PlaythroughsLogGameViewModel.anyPlayerSelected')) .value; + late final _$playerScoresAtom = Atom( + name: '_PlaythroughsLogGameViewModel.playerScores', context: context); + + @override + ObservableMap get playerScores { + _$playerScoresAtom.reportRead(); + return super.playerScores; + } + + @override + set playerScores(ObservableMap value) { + _$playerScoresAtom.reportWrite(value, super.playerScores, () { + super.playerScores = value; + }); + } + late final _$playthroughDateAtom = Atom( name: '_PlaythroughsLogGameViewModel.playthroughDate', context: context); @@ -123,7 +139,7 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { context: context); @override - Future createPlaythrough(String boardGameId) { + Future createPlaythrough(String boardGameId) { return _$createPlaythroughAsyncAction .run(() => super.createPlaythrough(boardGameId)); } @@ -179,6 +195,7 @@ mixin _$PlaythroughsLogGameViewModel on _PlaythroughsLogGameViewModel, Store { @override String toString() { return ''' +playerScores: ${playerScores}, playthroughDate: ${playthroughDate}, playthroughDuration: ${playthroughDuration}, playthroughStartTime: ${playthroughStartTime}, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart index 5b7f1a64..c0981233 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart @@ -1,7 +1,7 @@ import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import '../../common/app_colors.dart'; import '../../common/app_text.dart'; @@ -10,7 +10,6 @@ import '../../common/dimensions.dart'; import '../../models/bgg/bgg_plays_import_raport.dart'; import '../../models/hive/board_game_details.dart'; import '../../models/navigation/board_game_details_page_arguments.dart'; -import '../../stores/user_store.dart'; import '../../widgets/bottom_tab_icon.dart'; import '../../widgets/common/loading_overlay.dart'; import '../../widgets/common/page_container_widget.dart'; @@ -63,15 +62,16 @@ class PlaythroughsPageState extends BasePageState appBar: AppBar( title: Text(widget.viewModel.boardGame.name, style: AppTheme.titleTextStyle), actions: [ - Consumer( - builder: (_, store, ___) { - if (store.user?.name.isEmpty ?? true) { + Observer( + builder: (_) { + if (!widget.viewModel.hasUser) { return const SizedBox.shrink(); } return IconButton( icon: const Icon(Icons.download, color: AppColors.accentColor), - onPressed: () => _importBggPlays(store.user!.name, widget.viewModel.boardGame.id), + onPressed: () => + _importBggPlays(widget.viewModel.userName!, widget.viewModel.boardGame.id), ); }, ), 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 c80c2fd4..1b5502dd 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart @@ -2,7 +2,9 @@ import 'package:basics/basics.dart'; import 'package:board_games_companion/models/import_result.dart'; +import 'package:board_games_companion/models/playthrough_details.dart'; import 'package:board_games_companion/stores/playthroughs_store.dart'; +import 'package:board_games_companion/stores/user_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; @@ -12,7 +14,6 @@ import '../../common/analytics.dart'; import '../../models/bgg/bgg_plays_import_raport.dart'; import '../../models/hive/board_game_details.dart'; import '../../models/hive/player.dart'; -import '../../models/hive/playthrough.dart'; import '../../models/hive/score.dart'; import '../../models/player_score.dart'; import '../../models/playthrough_player.dart'; @@ -31,12 +32,14 @@ abstract class _PlaythroughsViewModel with Store { this._playersStore, this._analyticsService, this._boardGamesService, + this._userStore, ); final PlayersStore _playersStore; final PlaythroughsStore _playthroughsStore; final AnalyticsService _analyticsService; final BoardGamesService _boardGamesService; + final UserStore _userStore; List? _playthroughPlayers; List? get playthroughPlayers => _playthroughPlayers; @@ -46,6 +49,12 @@ abstract class _PlaythroughsViewModel with Store { @computed BoardGameDetails get boardGame => _playthroughsStore.boardGame; + @computed + bool get hasUser => _userStore.hasUser; + + @computed + String? get userName => _userStore.userName; + @action void setBoardGame(BoardGameDetails boardGame) { _playthroughsStore.setBoardGame(boardGame); @@ -79,7 +88,7 @@ abstract class _PlaythroughsViewModel with Store { // TODO Consider using isolates to parse and iterate over the results for (final bggPlay in bggPlaysImportResult.data!) { final bggPlayExists = _playthroughsStore.playthroughs - .any((Playthrough playthrough) => playthrough.bggPlayId == bggPlay.id); + .any((PlaythroughDetails playthrough) => playthrough.bggPlayId == bggPlay.id); if (bggPlayExists) { continue; } @@ -119,7 +128,7 @@ abstract class _PlaythroughsViewModel with Store { boardGameId: boardGameId, value: bggPlayer.playerScore.toString(), ); - playerScores[player.id] = PlayerScore(player, playerScore); + playerScores[player.id] = PlayerScore(player: player, score: playerScore); } } diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.g.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.g.dart index eae31e87..1cfa3035 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.g.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.g.dart @@ -16,6 +16,19 @@ mixin _$PlaythroughsViewModel on _PlaythroughsViewModel, Store { (_$boardGameComputed ??= Computed(() => super.boardGame, name: '_PlaythroughsViewModel.boardGame')) .value; + Computed? _$hasUserComputed; + + @override + bool get hasUser => (_$hasUserComputed ??= Computed(() => super.hasUser, + name: '_PlaythroughsViewModel.hasUser')) + .value; + Computed? _$userNameComputed; + + @override + String? get userName => + (_$userNameComputed ??= Computed(() => super.userName, + name: '_PlaythroughsViewModel.userName')) + .value; late final _$_PlaythroughsViewModelActionController = ActionController(name: '_PlaythroughsViewModel', context: context); @@ -34,7 +47,9 @@ mixin _$PlaythroughsViewModel on _PlaythroughsViewModel, Store { @override String toString() { return ''' -boardGame: ${boardGame} +boardGame: ${boardGame}, +hasUser: ${hasUser}, +userName: ${userName} '''; } } diff --git a/board_games_companion/lib/pages/search_board_games/search_board_games_view_model.dart b/board_games_companion/lib/pages/search_board_games/search_board_games_view_model.dart index 825abe0e..60b03f09 100644 --- a/board_games_companion/lib/pages/search_board_games/search_board_games_view_model.dart +++ b/board_games_companion/lib/pages/search_board_games/search_board_games_view_model.dart @@ -18,7 +18,10 @@ class SearchBoardGamesViewModel = _SearchBoardGamesViewModel with _$SearchBoardG abstract class _SearchBoardGamesViewModel with Store { _SearchBoardGamesViewModel( - this._boardGamesStore, this._boardGameGeekService, this._analyticsService); + this._boardGamesStore, + this._boardGameGeekService, + this._analyticsService, + ); final BoardGamesStore _boardGamesStore; final BoardGamesGeekService _boardGameGeekService; diff --git a/board_games_companion/lib/pages/settings/settings_page.dart b/board_games_companion/lib/pages/settings/settings_page.dart index 4aca924c..bdc2560e 100644 --- a/board_games_companion/lib/pages/settings/settings_page.dart +++ b/board_games_companion/lib/pages/settings/settings_page.dart @@ -1,15 +1,21 @@ +import 'dart:io' show Platform; + +import 'package:board_games_companion/models/backup_file.dart'; +import 'package:board_games_companion/pages/settings/settings_page_visual_states.dart'; +import 'package:board_games_companion/pages/settings/settings_view_model.dart'; +import 'package:board_games_companion/widgets/common/loading_overlay.dart'; import 'package:board_games_companion/widgets/common/page_container_widget.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mobx/mobx.dart'; import '../../common/app_colors.dart'; import '../../common/app_text.dart'; import '../../common/app_theme.dart'; import '../../common/constants.dart'; import '../../common/dimensions.dart'; -import '../../injectable.dart'; -import '../../stores/board_games_store.dart'; -import '../../stores/user_store.dart'; import '../../widgets/about/detail_item.dart'; import '../../widgets/about/section_title.dart'; import '../../widgets/common/bgg_community_member_text_widget.dart'; @@ -19,7 +25,12 @@ import '../../widgets/common/elevated_icon_button.dart'; import '../../widgets/common/import_collections_button.dart'; class SettingsPage extends StatefulWidget { - const SettingsPage({Key? key}) : super(key: key); + const SettingsPage({ + required this.viewModel, + Key? key, + }) : super(key: key); + + final SettingsViewModel viewModel; @override SettingsPageState createState() => SettingsPageState(); @@ -30,37 +41,60 @@ class SettingsPage extends StatefulWidget { class SettingsPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text(AppText.settingsPageTitle, style: AppTheme.titleTextStyle)), - body: SafeArea( - child: PageContainer( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: const [ - SizedBox(height: Dimensions.standardFontSize), - _UserDetailsPanel(), - ], + return Observer( + builder: (_) { + final scaffold = Scaffold( + appBar: + AppBar(title: const Text(AppText.settingsPageTitle, style: AppTheme.titleTextStyle)), + body: SafeArea( + child: PageContainer( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Theme( + data: AppTheme.theme.copyWith( + dividerTheme: AppTheme.theme.dividerTheme.copyWith( + space: Dimensions.doubleStandardSpacing, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: Dimensions.standardFontSize), + _UserDetailsPanel(viewModel: widget.viewModel), + const Divider(color: AppColors.accentColor), + _BackupSection(viewModel: widget.viewModel), + ], + ), + ), + ), ), - ), + ], ), - ], + ), ), - ), - ), + ); + + if (widget.viewModel.visualState == const SettingsPageVisualState.restoring()) { + return LoadingOverlay(child: scaffold); + } + + return scaffold; + }, ); } } class _UserDetailsPanel extends StatefulWidget { const _UserDetailsPanel({ + required this.viewModel, Key? key, }) : super(key: key); + final SettingsViewModel viewModel; + @override State<_UserDetailsPanel> createState() => _UserDetailsPanelState(); } @@ -83,44 +117,18 @@ class _UserDetailsPanelState extends State<_UserDetailsPanel> { } @override - Widget build(BuildContext context) => Consumer( - builder: (_, userStore, __) { - if (userStore.user?.name.isEmpty ?? true) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SectionTitle(title: 'BGG Username', padding: EdgeInsets.zero), - const SizedBox(height: Dimensions.standardSpacing), - const BggCommunityMemberText(), - BggCommunityMemberUserNameTextField( - controller: _bggUserNameController, - onSubmit: () => setState(() { - _triggerImport = true; - }), - ), - const SizedBox(height: Dimensions.standardSpacing), - Align( - alignment: Alignment.centerRight, - child: ImportCollectionsButton( - usernameCallback: () => _bggUserNameController.text, - triggerImport: _triggerImport ?? false, - ), - ), - ], - ), - ); - } - - return Column( + Widget build(BuildContext context) { + return Observer( + builder: (_) { + return widget.viewModel.userVisualState.when( + user: () => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle(title: 'User'), DetailsItem( - title: userStore.user?.name ?? '', + title: widget.viewModel.userName!, subtitle: 'BGG profile page', - uri: '${Constants.boardGameGeekBaseApiUrl}user/${userStore.user?.name}', + uri: '${Constants.boardGameGeekBaseApiUrl}user/${widget.viewModel.userName!}', ), Padding( padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), @@ -132,20 +140,52 @@ class _UserDetailsPanelState extends State<_UserDetailsPanel> { title: 'Remove', icon: const DefaultIcon(Icons.remove_circle_outline), color: AppColors.redColor, - onPressed: () async => _showRemoveBggUserDialog(context, userStore), + onPressed: () async { + final shouldRemoveUser = await _showRemoveBggUserDialog(context); + if (shouldRemoveUser ?? false) { + widget.viewModel.removeUser(); + } + }, ), const SizedBox(width: Dimensions.standardSpacing), - ImportCollectionsButton(usernameCallback: () => userStore.user!.name), + ImportCollectionsButton(usernameCallback: () => widget.viewModel.userName!), ], ), ), ], - ); - }, - ); + ), + noUser: () => Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionTitle(title: 'BGG Username', padding: EdgeInsets.zero), + const SizedBox(height: Dimensions.standardSpacing), + const BggCommunityMemberText(), + BggCommunityMemberUserNameTextField( + controller: _bggUserNameController, + onSubmit: () => setState(() { + _triggerImport = true; + }), + ), + const SizedBox(height: Dimensions.standardSpacing), + Align( + alignment: Alignment.centerRight, + child: ImportCollectionsButton( + usernameCallback: () => _bggUserNameController.text, + triggerImport: _triggerImport ?? false, + ), + ), + ], + ), + ), + ); + }, + ); + } - Future _showRemoveBggUserDialog(BuildContext context, UserStore userStore) async { - await showDialog( + Future _showRemoveBggUserDialog(BuildContext context) async { + return showDialog( context: context, builder: (context) { return AlertDialog( @@ -155,7 +195,7 @@ class _UserDetailsPanelState extends State<_UserDetailsPanel> { ], ), content: const Text( - 'This will delete your synchronized board games collection, including the history of gameplays', + 'This will delete your imported board games collection, including the history of gameplays', ), elevation: Dimensions.defaultElevation, actions: [ @@ -164,24 +204,11 @@ class _UserDetailsPanelState extends State<_UserDetailsPanel> { AppText.cancel, style: TextStyle(color: AppColors.accentColor), ), - onPressed: () { - Navigator.of(context).pop(); - }, + onPressed: () => Navigator.of(context).pop(false), ), TextButton( style: TextButton.styleFrom(backgroundColor: AppColors.redColor), - onPressed: () async { - final boardGameStore = getIt(); - - await userStore.removeUser(userStore.user!); - await boardGameStore.removeAllBggBoardGames(); - - if (!mounted) { - return; - } - - Navigator.of(context).pop(); - }, + onPressed: () => Navigator.of(context).pop(true), child: const Text( 'Remove', style: TextStyle(color: AppColors.defaultTextColor), @@ -193,3 +220,210 @@ class _UserDetailsPanelState extends State<_UserDetailsPanel> { ); } } + +class _BackupSection extends StatefulWidget { + const _BackupSection({ + required this.viewModel, + Key? key, + }) : super(key: key); + + final SettingsViewModel viewModel; + + @override + State<_BackupSection> createState() => _BackupSectionState(); +} + +class _BackupSectionState extends State<_BackupSection> with TickerProviderStateMixin { + late AnimationController _fadeInAnimationController; + late AnimationController _sizeAnimationController; + + @override + void initState() { + super.initState(); + + widget.viewModel.loadBackups(); + + _fadeInAnimationController = AnimationController( + vsync: this, + duration: const Duration(microseconds: 500), + ); + _sizeAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + value: 1, + ); + } + + @override + void dispose() { + _fadeInAnimationController.dispose(); + _sizeAnimationController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionTitle( + title: AppText.settingsPageBackupAndRestoreSectionTitle, + padding: EdgeInsets.zero, + ), + const SizedBox(height: Dimensions.standardSpacing), + const Text( + AppText.settingsPageBackupAndRestoreSectionBody, + style: TextStyle(fontSize: Dimensions.mediumFontSize), + ), + const SizedBox(height: Dimensions.standardSpacing), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedIconButton( + icon: const Icon(Icons.settings_backup_restore), + title: AppText.settingsPageRestireButtonText, + onPressed: () async => widget.viewModel.restoreAppData(), + ), + const SizedBox(width: Dimensions.standardSpacing), + AnimatedButton( + text: AppText.settingsPageBackupButtonText, + icon: const DefaultIcon(Icons.archive), + sizeAnimationController: _sizeAnimationController, + fadeInAnimationController: _fadeInAnimationController, + onPressed: () async { + await widget.viewModel.backupAppsData(); + if (mounted) { + _sizeAnimationController.forward(); + _fadeInAnimationController.reverse(); + } + }, + ), + ], + ), + ], + ), + ), + Observer( + builder: (BuildContext context) { + switch (widget.viewModel.futureLoadBackups?.status ?? FutureStatus.pending) { + case FutureStatus.fulfilled: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.viewModel.hasAnyBackupFiles) ...[ + const Padding( + padding: EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: Text( + AppText.settingsPageBackupsListTitle, + style: AppTheme.sectionHeaderTextStyle, + ), + ), + const SizedBox(height: Dimensions.halfStandardSpacing), + ], + for (final backupFile in widget.viewModel.backupFiles) + _BackupFile( + backupFile: backupFile, + onDeleteBackup: (BackupFile backupFile) => + widget.viewModel.deleteBackup(backupFile), + onShareBackup: (BackupFile backupFile) async => + _shareBackup(context, backupFile), + ), + ], + ); + case FutureStatus.pending: + case FutureStatus.rejected: + return const SizedBox.shrink(); + } + }, + ), + ], + ), + ); + } + + Future _shareBackup(BuildContext context, BackupFile backupFile) async { + Rect? sharePositionOrigin; + if (Platform.isIOS) { + // MK https://pub.dev/packages/share_plus#known-issues + final box = context.findRenderObject() as RenderBox?; + if (box != null) { + sharePositionOrigin = box.localToGlobal(Offset.zero) & box.size; + } + } + + await widget.viewModel.shareBackupFile( + backupFile, + sharePositionOrigin: sharePositionOrigin, + ); + } +} + +class _BackupFile extends StatelessWidget { + const _BackupFile({ + Key? key, + required this.backupFile, + required this.onDeleteBackup, + required this.onShareBackup, + }) : super(key: key); + + final BackupFile backupFile; + final Function(BackupFile) onDeleteBackup; + final Function(BackupFile) onShareBackup; + + @override + Widget build(BuildContext context) { + return Slidable( + endActionPane: ActionPane( + extentRatio: 0.25, + motion: const ScrollMotion(), + children: [ + SlidableAction( + icon: Icons.delete, + onPressed: (_) => onDeleteBackup(backupFile), + backgroundColor: AppColors.redColor, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimensions.standardSpacing), + child: Row( + children: [ + const FaIcon( + FontAwesomeIcons.boxArchive, + color: AppColors.whiteColor, + ), + const SizedBox(width: Dimensions.standardSpacing), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + backupFile.name, + style: const TextStyle(color: AppColors.whiteColor), + ), + const SizedBox(height: Dimensions.halfStandardSpacing), + Text( + backupFile.readableFileSize, + style: AppTheme.theme.textTheme.subtitle1, + ), + ], + ), + const Expanded(child: SizedBox.shrink()), + IconButton( + onPressed: () => onShareBackup(backupFile), + icon: const Icon(Icons.share), + color: AppColors.accentColor, + ), + ], + ), + ), + ); + } +} diff --git a/board_games_companion/lib/pages/settings/settings_page_user_visual_states.dart b/board_games_companion/lib/pages/settings/settings_page_user_visual_states.dart new file mode 100644 index 00000000..fd443281 --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_page_user_visual_states.dart @@ -0,0 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'settings_page_user_visual_states.freezed.dart'; + +@freezed +class SettingsPageUserVisualState with _$SettingsPageUserVisualState { + const factory SettingsPageUserVisualState.noUser() = NoUser; + const factory SettingsPageUserVisualState.user() = User; +} diff --git a/board_games_companion/lib/pages/settings/settings_page_user_visual_states.freezed.dart b/board_games_companion/lib/pages/settings/settings_page_user_visual_states.freezed.dart new file mode 100644 index 00000000..9fcd45d5 --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_page_user_visual_states.freezed.dart @@ -0,0 +1,281 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'settings_page_user_visual_states.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 _$SettingsPageUserVisualState { + @optionalTypeArgs + TResult when({ + required TResult Function() noUser, + required TResult Function() user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? noUser, + TResult Function()? user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? noUser, + TResult Function()? user, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(NoUser value) noUser, + required TResult Function(User value) user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SettingsPageUserVisualStateCopyWith<$Res> { + factory $SettingsPageUserVisualStateCopyWith( + SettingsPageUserVisualState value, + $Res Function(SettingsPageUserVisualState) then) = + _$SettingsPageUserVisualStateCopyWithImpl<$Res>; +} + +/// @nodoc +class _$SettingsPageUserVisualStateCopyWithImpl<$Res> + implements $SettingsPageUserVisualStateCopyWith<$Res> { + _$SettingsPageUserVisualStateCopyWithImpl(this._value, this._then); + + final SettingsPageUserVisualState _value; + // ignore: unused_field + final $Res Function(SettingsPageUserVisualState) _then; +} + +/// @nodoc +abstract class _$$NoUserCopyWith<$Res> { + factory _$$NoUserCopyWith(_$NoUser value, $Res Function(_$NoUser) then) = + __$$NoUserCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$NoUserCopyWithImpl<$Res> + extends _$SettingsPageUserVisualStateCopyWithImpl<$Res> + implements _$$NoUserCopyWith<$Res> { + __$$NoUserCopyWithImpl(_$NoUser _value, $Res Function(_$NoUser) _then) + : super(_value, (v) => _then(v as _$NoUser)); + + @override + _$NoUser get _value => super._value as _$NoUser; +} + +/// @nodoc + +class _$NoUser implements NoUser { + const _$NoUser(); + + @override + String toString() { + return 'SettingsPageUserVisualState.noUser()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$NoUser); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() noUser, + required TResult Function() user, + }) { + return noUser(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? noUser, + TResult Function()? user, + }) { + return noUser?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? noUser, + TResult Function()? user, + required TResult orElse(), + }) { + if (noUser != null) { + return noUser(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NoUser value) noUser, + required TResult Function(User value) user, + }) { + return noUser(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + }) { + return noUser?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + required TResult orElse(), + }) { + if (noUser != null) { + return noUser(this); + } + return orElse(); + } +} + +abstract class NoUser implements SettingsPageUserVisualState { + const factory NoUser() = _$NoUser; +} + +/// @nodoc +abstract class _$$UserCopyWith<$Res> { + factory _$$UserCopyWith(_$User value, $Res Function(_$User) then) = + __$$UserCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$UserCopyWithImpl<$Res> + extends _$SettingsPageUserVisualStateCopyWithImpl<$Res> + implements _$$UserCopyWith<$Res> { + __$$UserCopyWithImpl(_$User _value, $Res Function(_$User) _then) + : super(_value, (v) => _then(v as _$User)); + + @override + _$User get _value => super._value as _$User; +} + +/// @nodoc + +class _$User implements User { + const _$User(); + + @override + String toString() { + return 'SettingsPageUserVisualState.user()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$User); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() noUser, + required TResult Function() user, + }) { + return user(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? noUser, + TResult Function()? user, + }) { + return user?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? noUser, + TResult Function()? user, + required TResult orElse(), + }) { + if (user != null) { + return user(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NoUser value) noUser, + required TResult Function(User value) user, + }) { + return user(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + }) { + return user?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NoUser value)? noUser, + TResult Function(User value)? user, + required TResult orElse(), + }) { + if (user != null) { + return user(this); + } + return orElse(); + } +} + +abstract class User implements SettingsPageUserVisualState { + const factory User() = _$User; +} diff --git a/board_games_companion/lib/pages/settings/settings_page_visual_states.dart b/board_games_companion/lib/pages/settings/settings_page_visual_states.dart new file mode 100644 index 00000000..26a22f96 --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_page_visual_states.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'settings_page_visual_states.freezed.dart'; + +@freezed +class SettingsPageVisualState with _$SettingsPageVisualState { + const factory SettingsPageVisualState.initial() = Initial; + const factory SettingsPageVisualState.restoring() = Restoring; + const factory SettingsPageVisualState.restoringSuccess() = RestoringSucceeded; + const factory SettingsPageVisualState.restoringFailure([String? message]) = RestoringFailed; +} diff --git a/board_games_companion/lib/pages/settings/settings_page_visual_states.freezed.dart b/board_games_companion/lib/pages/settings/settings_page_visual_states.freezed.dart new file mode 100644 index 00000000..407faf6b --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_page_visual_states.freezed.dart @@ -0,0 +1,581 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'settings_page_visual_states.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 _$SettingsPageVisualState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() restoring, + required TResult Function() restoringSuccess, + required TResult Function(String? message) restoringFailure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Restoring value) restoring, + required TResult Function(RestoringSucceeded value) restoringSuccess, + required TResult Function(RestoringFailed value) restoringFailure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SettingsPageVisualStateCopyWith<$Res> { + factory $SettingsPageVisualStateCopyWith(SettingsPageVisualState value, + $Res Function(SettingsPageVisualState) then) = + _$SettingsPageVisualStateCopyWithImpl<$Res>; +} + +/// @nodoc +class _$SettingsPageVisualStateCopyWithImpl<$Res> + implements $SettingsPageVisualStateCopyWith<$Res> { + _$SettingsPageVisualStateCopyWithImpl(this._value, this._then); + + final SettingsPageVisualState _value; + // ignore: unused_field + final $Res Function(SettingsPageVisualState) _then; +} + +/// @nodoc +abstract class _$$InitialCopyWith<$Res> { + factory _$$InitialCopyWith(_$Initial value, $Res Function(_$Initial) then) = + __$$InitialCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialCopyWithImpl<$Res> + extends _$SettingsPageVisualStateCopyWithImpl<$Res> + implements _$$InitialCopyWith<$Res> { + __$$InitialCopyWithImpl(_$Initial _value, $Res Function(_$Initial) _then) + : super(_value, (v) => _then(v as _$Initial)); + + @override + _$Initial get _value => super._value as _$Initial; +} + +/// @nodoc + +class _$Initial implements Initial { + const _$Initial(); + + @override + String toString() { + return 'SettingsPageVisualState.initial()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$Initial); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() restoring, + required TResult Function() restoringSuccess, + required TResult Function(String? message) restoringFailure, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Restoring value) restoring, + required TResult Function(RestoringSucceeded value) restoringSuccess, + required TResult Function(RestoringFailed value) restoringFailure, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class Initial implements SettingsPageVisualState { + const factory Initial() = _$Initial; +} + +/// @nodoc +abstract class _$$RestoringCopyWith<$Res> { + factory _$$RestoringCopyWith( + _$Restoring value, $Res Function(_$Restoring) then) = + __$$RestoringCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RestoringCopyWithImpl<$Res> + extends _$SettingsPageVisualStateCopyWithImpl<$Res> + implements _$$RestoringCopyWith<$Res> { + __$$RestoringCopyWithImpl( + _$Restoring _value, $Res Function(_$Restoring) _then) + : super(_value, (v) => _then(v as _$Restoring)); + + @override + _$Restoring get _value => super._value as _$Restoring; +} + +/// @nodoc + +class _$Restoring implements Restoring { + const _$Restoring(); + + @override + String toString() { + return 'SettingsPageVisualState.restoring()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$Restoring); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() restoring, + required TResult Function() restoringSuccess, + required TResult Function(String? message) restoringFailure, + }) { + return restoring(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + }) { + return restoring?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + required TResult orElse(), + }) { + if (restoring != null) { + return restoring(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Restoring value) restoring, + required TResult Function(RestoringSucceeded value) restoringSuccess, + required TResult Function(RestoringFailed value) restoringFailure, + }) { + return restoring(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + }) { + return restoring?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + required TResult orElse(), + }) { + if (restoring != null) { + return restoring(this); + } + return orElse(); + } +} + +abstract class Restoring implements SettingsPageVisualState { + const factory Restoring() = _$Restoring; +} + +/// @nodoc +abstract class _$$RestoringSucceededCopyWith<$Res> { + factory _$$RestoringSucceededCopyWith(_$RestoringSucceeded value, + $Res Function(_$RestoringSucceeded) then) = + __$$RestoringSucceededCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RestoringSucceededCopyWithImpl<$Res> + extends _$SettingsPageVisualStateCopyWithImpl<$Res> + implements _$$RestoringSucceededCopyWith<$Res> { + __$$RestoringSucceededCopyWithImpl( + _$RestoringSucceeded _value, $Res Function(_$RestoringSucceeded) _then) + : super(_value, (v) => _then(v as _$RestoringSucceeded)); + + @override + _$RestoringSucceeded get _value => super._value as _$RestoringSucceeded; +} + +/// @nodoc + +class _$RestoringSucceeded implements RestoringSucceeded { + const _$RestoringSucceeded(); + + @override + String toString() { + return 'SettingsPageVisualState.restoringSuccess()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$RestoringSucceeded); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() restoring, + required TResult Function() restoringSuccess, + required TResult Function(String? message) restoringFailure, + }) { + return restoringSuccess(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + }) { + return restoringSuccess?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + required TResult orElse(), + }) { + if (restoringSuccess != null) { + return restoringSuccess(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Restoring value) restoring, + required TResult Function(RestoringSucceeded value) restoringSuccess, + required TResult Function(RestoringFailed value) restoringFailure, + }) { + return restoringSuccess(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + }) { + return restoringSuccess?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + required TResult orElse(), + }) { + if (restoringSuccess != null) { + return restoringSuccess(this); + } + return orElse(); + } +} + +abstract class RestoringSucceeded implements SettingsPageVisualState { + const factory RestoringSucceeded() = _$RestoringSucceeded; +} + +/// @nodoc +abstract class _$$RestoringFailedCopyWith<$Res> { + factory _$$RestoringFailedCopyWith( + _$RestoringFailed value, $Res Function(_$RestoringFailed) then) = + __$$RestoringFailedCopyWithImpl<$Res>; + $Res call({String? message}); +} + +/// @nodoc +class __$$RestoringFailedCopyWithImpl<$Res> + extends _$SettingsPageVisualStateCopyWithImpl<$Res> + implements _$$RestoringFailedCopyWith<$Res> { + __$$RestoringFailedCopyWithImpl( + _$RestoringFailed _value, $Res Function(_$RestoringFailed) _then) + : super(_value, (v) => _then(v as _$RestoringFailed)); + + @override + _$RestoringFailed get _value => super._value as _$RestoringFailed; + + @override + $Res call({ + Object? message = freezed, + }) { + return _then(_$RestoringFailed( + message == freezed + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$RestoringFailed implements RestoringFailed { + const _$RestoringFailed([this.message]); + + @override + final String? message; + + @override + String toString() { + return 'SettingsPageVisualState.restoringFailure(message: $message)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RestoringFailed && + const DeepCollectionEquality().equals(other.message, message)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(message)); + + @JsonKey(ignore: true) + @override + _$$RestoringFailedCopyWith<_$RestoringFailed> get copyWith => + __$$RestoringFailedCopyWithImpl<_$RestoringFailed>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() restoring, + required TResult Function() restoringSuccess, + required TResult Function(String? message) restoringFailure, + }) { + return restoringFailure(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + }) { + return restoringFailure?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? restoring, + TResult Function()? restoringSuccess, + TResult Function(String? message)? restoringFailure, + required TResult orElse(), + }) { + if (restoringFailure != null) { + return restoringFailure(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Restoring value) restoring, + required TResult Function(RestoringSucceeded value) restoringSuccess, + required TResult Function(RestoringFailed value) restoringFailure, + }) { + return restoringFailure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + }) { + return restoringFailure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Restoring value)? restoring, + TResult Function(RestoringSucceeded value)? restoringSuccess, + TResult Function(RestoringFailed value)? restoringFailure, + required TResult orElse(), + }) { + if (restoringFailure != null) { + return restoringFailure(this); + } + return orElse(); + } +} + +abstract class RestoringFailed implements SettingsPageVisualState { + const factory RestoringFailed([final String? message]) = _$RestoringFailed; + + String? get message; + @JsonKey(ignore: true) + _$$RestoringFailedCopyWith<_$RestoringFailed> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/board_games_companion/lib/pages/settings/settings_view_model.dart b/board_games_companion/lib/pages/settings/settings_view_model.dart new file mode 100644 index 00000000..a0c51617 --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_view_model.dart @@ -0,0 +1,135 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:board_games_companion/services/board_games_filters_service.dart'; +import 'package:board_games_companion/services/file_service.dart'; +import 'package:board_games_companion/services/player_service.dart'; +import 'package:board_games_companion/stores/app_store.dart'; +import 'package:board_games_companion/stores/board_games_store.dart'; +import 'package:board_games_companion/stores/user_store.dart'; +import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../models/backup_file.dart'; +import '../../services/board_games_service.dart'; +import '../../services/playthroughs_service.dart'; +import '../../services/preferences_service.dart'; +import '../../services/score_service.dart'; +import '../../services/user_service.dart'; +import 'settings_page_user_visual_states.dart'; +import 'settings_page_visual_states.dart'; + +part 'settings_view_model.g.dart'; + +@singleton +class SettingsViewModel = _SettingsViewModel with _$SettingsViewModel; + +abstract class _SettingsViewModel with Store { + _SettingsViewModel( + this._fileService, + this._boardGamesService, + this._boardGamesFilterService, + this._playerService, + this._userService, + this._playthroughService, + this._scoreService, + this._preferencesService, + this._appStore, + this._userStore, + this._boardGamesStore, + ); + + final FileService _fileService; + final BoardGamesService _boardGamesService; + final BoardGamesFiltersService _boardGamesFilterService; + final PlayerService _playerService; + final UserService _userService; + final PlaythroughService _playthroughService; + final ScoreService _scoreService; + final PreferencesService _preferencesService; + final AppStore _appStore; + final UserStore _userStore; + final BoardGamesStore _boardGamesStore; + + @observable + ObservableList backupFiles = ObservableList.of([]); + + @observable + SettingsPageVisualState visualState = const SettingsPageVisualState.initial(); + + @computed + SettingsPageUserVisualState get userVisualState => _userStore.hasUser + ? const SettingsPageUserVisualState.user() + : const SettingsPageUserVisualState.noUser(); + + @computed + String? get userName => _userStore.userName; + + @observable + ObservableFuture? futureLoadBackups; + + @computed + bool get hasAnyBackupFiles => backupFiles.isNotEmpty; + + @action + void loadBackups() => futureLoadBackups = ObservableFuture(_loadBackups()); + + Future shareBackupFile(BackupFile backupFile, {Rect? sharePositionOrigin}) async => + Share.shareFiles( + [backupFile.path], + mimeTypes: ['application/zip'], + sharePositionOrigin: sharePositionOrigin, + ); + + @action + Future deleteBackup(BackupFile backupFile) async { + await _fileService.deleteFileFromDocumentsDirectory( + '${FileService.backupDirectoryName}/${backupFile.nameWithExtension}'); + backupFiles.remove(backupFile); + } + + @action + Future removeUser() async { + await _userStore.removeUser(); + await _boardGamesStore.removeAllBggBoardGames(); + } + + @action + Future backupAppsData() async { + await _fileService.backupAppData(); + await _loadBackups(); + } + + @action + Future restoreAppData() async { + try { + _appStore.setBackupRestore(false); + + visualState = const SettingsPageVisualState.restoring(); + + _boardGamesService.closeBox(); + _boardGamesFilterService.closeBox(); + _playerService.closeBox(); + _userService.closeBox(); + _playthroughService.closeBox(); + _scoreService.closeBox(); + _preferencesService.closeBox(); + + // MK Restore files + await _fileService.restoreAppData(); + + await _preferencesService.initialize(); + _appStore.setBackupRestore(true); + } on Exception { + visualState = const SettingsPageVisualState.restoringFailure(); + } + + visualState = const SettingsPageVisualState.restoringSuccess(); + } + + Future _loadBackups() async => + backupFiles = ObservableList.of(await _fileService.getBackups() + ..sort((BackupFile backupFile, BackupFile otherBackupFile) => + otherBackupFile.changed.compareTo(backupFile.changed))); +} diff --git a/board_games_companion/lib/pages/settings/settings_view_model.g.dart b/board_games_companion/lib/pages/settings/settings_view_model.g.dart new file mode 100644 index 00000000..69c21d19 --- /dev/null +++ b/board_games_companion/lib/pages/settings/settings_view_model.g.dart @@ -0,0 +1,140 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings_view_model.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$SettingsViewModel on _SettingsViewModel, Store { + Computed? _$userVisualStateComputed; + + @override + SettingsPageUserVisualState get userVisualState => + (_$userVisualStateComputed ??= Computed( + () => super.userVisualState, + name: '_SettingsViewModel.userVisualState')) + .value; + Computed? _$userNameComputed; + + @override + String? get userName => + (_$userNameComputed ??= Computed(() => super.userName, + name: '_SettingsViewModel.userName')) + .value; + Computed? _$hasAnyBackupFilesComputed; + + @override + bool get hasAnyBackupFiles => (_$hasAnyBackupFilesComputed ??= Computed( + () => super.hasAnyBackupFiles, + name: '_SettingsViewModel.hasAnyBackupFiles')) + .value; + + late final _$backupFilesAtom = + Atom(name: '_SettingsViewModel.backupFiles', context: context); + + @override + ObservableList get backupFiles { + _$backupFilesAtom.reportRead(); + return super.backupFiles; + } + + @override + set backupFiles(ObservableList value) { + _$backupFilesAtom.reportWrite(value, super.backupFiles, () { + super.backupFiles = value; + }); + } + + late final _$visualStateAtom = + Atom(name: '_SettingsViewModel.visualState', context: context); + + @override + SettingsPageVisualState get visualState { + _$visualStateAtom.reportRead(); + return super.visualState; + } + + @override + set visualState(SettingsPageVisualState value) { + _$visualStateAtom.reportWrite(value, super.visualState, () { + super.visualState = value; + }); + } + + late final _$futureLoadBackupsAtom = + Atom(name: '_SettingsViewModel.futureLoadBackups', context: context); + + @override + ObservableFuture? get futureLoadBackups { + _$futureLoadBackupsAtom.reportRead(); + return super.futureLoadBackups; + } + + @override + set futureLoadBackups(ObservableFuture? value) { + _$futureLoadBackupsAtom.reportWrite(value, super.futureLoadBackups, () { + super.futureLoadBackups = value; + }); + } + + late final _$deleteBackupAsyncAction = + AsyncAction('_SettingsViewModel.deleteBackup', context: context); + + @override + Future deleteBackup(BackupFile backupFile) { + return _$deleteBackupAsyncAction.run(() => super.deleteBackup(backupFile)); + } + + late final _$removeUserAsyncAction = + AsyncAction('_SettingsViewModel.removeUser', context: context); + + @override + Future removeUser() { + return _$removeUserAsyncAction.run(() => super.removeUser()); + } + + late final _$backupAppsDataAsyncAction = + AsyncAction('_SettingsViewModel.backupAppsData', context: context); + + @override + Future backupAppsData() { + return _$backupAppsDataAsyncAction.run(() => super.backupAppsData()); + } + + late final _$restoreAppDataAsyncAction = + AsyncAction('_SettingsViewModel.restoreAppData', context: context); + + @override + Future restoreAppData() { + return _$restoreAppDataAsyncAction.run(() => super.restoreAppData()); + } + + late final _$_SettingsViewModelActionController = + ActionController(name: '_SettingsViewModel', context: context); + + @override + void loadBackups() { + final _$actionInfo = _$_SettingsViewModelActionController.startAction( + name: '_SettingsViewModel.loadBackups'); + try { + return super.loadBackups(); + } finally { + _$_SettingsViewModelActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +backupFiles: ${backupFiles}, +visualState: ${visualState}, +futureLoadBackups: ${futureLoadBackups}, +userVisualState: ${userVisualState}, +userName: ${userName}, +hasAnyBackupFiles: ${hasAnyBackupFiles} + '''; + } +} diff --git a/board_games_companion/lib/services/board_games_filters_service.dart b/board_games_companion/lib/services/board_games_filters_service.dart index f2dd98c4..6147079d 100644 --- a/board_games_companion/lib/services/board_games_filters_service.dart +++ b/board_games_companion/lib/services/board_games_filters_service.dart @@ -1,15 +1,15 @@ import 'package:injectable/injectable.dart'; -import '../common/hive_boxes.dart'; import '../models/collection_filters.dart'; import 'hive_base_service.dart'; @singleton -class BoardGamesFiltersService extends BaseHiveService { +class BoardGamesFiltersService + extends BaseHiveService { static const String _collectionFiltersPreferenceKey = 'collectionFilters'; Future retrieveCollectionFiltersPreferences() async { - if (!await ensureBoxOpen(HiveBoxes.collectionFilters)) { + if (!await ensureBoxOpen()) { return null; } @@ -25,7 +25,7 @@ class BoardGamesFiltersService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.collectionFilters)) { + if (!await ensureBoxOpen()) { return false; } diff --git a/board_games_companion/lib/services/board_games_service.dart b/board_games_companion/lib/services/board_games_service.dart index 8274d206..f2080c69 100644 --- a/board_games_companion/lib/services/board_games_service.dart +++ b/board_games_companion/lib/services/board_games_service.dart @@ -2,33 +2,25 @@ import 'package:board_games_companion/models/bgg/bgg_import_plays.dart'; import 'package:board_games_companion/models/bgg/bgg_plays_import_result.dart'; import 'package:injectable/injectable.dart'; -import '../common/hive_boxes.dart'; import '../models/collection_import_result.dart'; import '../models/hive/board_game_details.dart'; import 'board_games_geek_service.dart'; import 'hive_base_service.dart'; -import 'preferences_service.dart'; @singleton -class BoardGamesService extends BaseHiveService { - BoardGamesService(this._boardGameGeekService, this._preferenceService); +class BoardGamesService extends BaseHiveService { + BoardGamesService(this._boardGameGeekService); final BoardGamesGeekService _boardGameGeekService; - final PreferencesService _preferenceService; static const int _maxNumberOfImportedPlaysPerPage = 100; Future> retrieveBoardGames() async { - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return []; } - final List boardGames = storageBox.values.toList(); - if (!_preferenceService.getMigratedToMultipleCollections()) { - await _migrateToMultipleCollections(boardGames); - } - - return boardGames; + return storageBox.values.toList(); } Future getBoardGame(String boardGameId) => @@ -39,7 +31,7 @@ class BoardGamesService extends BaseHiveService { return; } - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return; } @@ -51,7 +43,7 @@ class BoardGamesService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return false; } @@ -63,7 +55,7 @@ class BoardGamesService extends BaseHiveService { return; } - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return; } @@ -75,7 +67,7 @@ class BoardGamesService extends BaseHiveService { return; } - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return; } @@ -83,7 +75,7 @@ class BoardGamesService extends BaseHiveService { } Future removeAllBoardGames() async { - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return; } @@ -114,7 +106,7 @@ class BoardGamesService extends BaseHiveService { } Future importCollections(String username) async { - if (!await ensureBoxOpen(HiveBoxes.boardGames)) { + if (!await ensureBoxOpen()) { return CollectionImportResult(); } @@ -158,14 +150,4 @@ class BoardGamesService extends BaseHiveService { return collectionImportResult; } - - Future _migrateToMultipleCollections(List boardGames) async { - for (final boardGame in boardGames.where( - (boardGame) => !boardGame.isOwned! && !boardGame.isOnWishlist! && !boardGame.isFriends!)) { - boardGame.isOwned = true; - await addOrUpdateBoardGame(boardGame); - } - - await _preferenceService.setMigratedToMultipleCollections(migratedToMultipleCollections: true); - } } diff --git a/board_games_companion/lib/services/file_service.dart b/board_games_companion/lib/services/file_service.dart index 4c63f4ef..a993dfe2 100644 --- a/board_games_companion/lib/services/file_service.dart +++ b/board_games_companion/lib/services/file_service.dart @@ -1,14 +1,31 @@ import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:board_games_companion/common/constants.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; import 'package:image_picker/image_picker.dart'; import 'package:injectable/injectable.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart' as path_provider; +import '../models/backup_file.dart'; + @singleton class FileService { - Future saveToDocumentsDirectory(String fileName, XFile pickedFile, - {bool overrideExistingFile = false}) async { + static const String backupDirectoryName = 'backups'; + static const Set backupFileExtensions = {'.jpg', '.hive'}; + static const String backupFileExtension = 'zip'; + + static DateFormat backupDateFormat = DateFormat(Constants.appDataBackupDateFormat); + + Future saveToDocumentsDirectory( + String fileName, + XFile pickedFile, { + bool overrideExistingFile = false, + }) async { try { final fileContent = await pickedFile.readAsBytes(); final avatarImageToSave = await _retrieveDocumentsFile(fileName); @@ -63,8 +80,88 @@ class FileService { return '${documentsDirectory.path}/$fileName'; } + Future> getBackups() async { + final appBackupsDirectory = await _getBackupDirectory(); + final backups = []; + await for (final FileSystemEntity fileSystemEntity in appBackupsDirectory.list()) { + final fileStats = await fileSystemEntity.stat(); + backups.add(BackupFile( + path: fileSystemEntity.path, + size: fileStats.size, + changed: fileStats.changed, + )); + } + + return backups; + } + + Future backupAppData() async { + final appDirectory = await path_provider.getApplicationDocumentsDirectory(); + final appBackupsDirectory = await _getBackupDirectory(); + + return compute(archiveAppData, _ArchiveAppDataModel(appDirectory, appBackupsDirectory)); + } + + // MK This method is completely redundant from the "clean" code point of view but because dart is retarted when it comes to Isolates, + // there was a need for a "top" level method that has a parameter - so here you go dart lords...you won + // ignore: library_private_types_in_public_api + Future archiveAppData(_ArchiveAppDataModel archiveAppDataModel) async { + final zipEncoder = ZipFileEncoder(); + zipEncoder.create( + '${archiveAppDataModel.appBackupsDirectory.path}/BGC Backup ${backupDateFormat.format(DateTime.now())}.zip'); + + await for (final FileSystemEntity fileSystemEntity in archiveAppDataModel.appDirectory.list()) { + final fileStats = await fileSystemEntity.stat(); + final fileName = basename(fileSystemEntity.path); + final fileExtension = extension(fileSystemEntity.path); + if (!backupFileExtensions.contains(fileExtension)) { + continue; + } + + final fileStream = InputFileStream(fileSystemEntity.path); + final archiveFile = ArchiveFile.stream(fileName, fileStats.size, fileStream); + archiveFile.mode = fileStats.mode; + archiveFile.lastModTime = fileStats.modified.millisecondsSinceEpoch ~/ 1000; + + zipEncoder.addArchiveFile(archiveFile); + await fileStream.close(); + } + + zipEncoder.close(); + } + + Future restoreAppData() async { + final FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: [backupFileExtension], + ); + + if (result == null) { + // User canceled the picker + return; + } + + final documentsDirectory = await path_provider.getApplicationDocumentsDirectory(); + await extractFileToDisk(result.files.single.path!, documentsDirectory.path, asyncWrite: true); + + return; + } + Future _retrieveDocumentsFile(String fileName) async { final documentsFilePath = await createDocumentsFilePath(fileName); return File(documentsFilePath); } + + Future _getBackupDirectory() async { + final appDirectory = await path_provider.getApplicationDocumentsDirectory(); + final appBackupDirectory = Directory('${appDirectory.path}/$backupDirectoryName'); + return appBackupDirectory.create(); + } +} + +class _ArchiveAppDataModel { + _ArchiveAppDataModel(this.appDirectory, this.appBackupsDirectory); + + Directory appDirectory; + Directory appBackupsDirectory; } diff --git a/board_games_companion/lib/services/hive_base_service.dart b/board_games_companion/lib/services/hive_base_service.dart index 3274fbc5..7a255cc3 100644 --- a/board_games_companion/lib/services/hive_base_service.dart +++ b/board_games_companion/lib/services/hive_base_service.dart @@ -1,24 +1,34 @@ +import 'package:basics/basics.dart'; +import 'package:board_games_companion/common/hive_boxes.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:uuid/uuid.dart'; -class BaseHiveService { +abstract class BaseHiveService { @protected final uuid = const Uuid(); - late Box storageBox; + late Box storageBox; - void closeBox(String boxName) { - if (boxName.isEmpty) { + String? get _boxName => HiveBoxes.boxesNamesMap[TService]; + + bool get _isBoxOpen => Hive.isBoxOpen(_boxName!); + + void closeBox() { + if (_boxName.isNullOrBlank || !_isBoxOpen) { return; } - Hive.box(boxName).close(); + Hive.box(_boxName!).close(); } - Future ensureBoxOpen(String boxName) async { - if (!Hive.isBoxOpen(boxName)) { - storageBox = await Hive.openBox(boxName); + Future ensureBoxOpen() async { + if (_boxName.isNullOrBlank) { + return false; + } + + if (!_isBoxOpen) { + storageBox = await Hive.openBox(_boxName!); } return storageBox != null; diff --git a/board_games_companion/lib/services/player_service.dart b/board_games_companion/lib/services/player_service.dart index 65c7b063..fbcfd581 100644 --- a/board_games_companion/lib/services/player_service.dart +++ b/board_games_companion/lib/services/player_service.dart @@ -3,13 +3,12 @@ import 'dart:io'; import 'package:injectable/injectable.dart'; import 'package:uuid/uuid.dart' show Uuid; -import '../common/hive_boxes.dart' show HiveBoxes; import '../models/hive/player.dart'; import 'file_service.dart'; import 'hive_base_service.dart'; @singleton -class PlayerService extends BaseHiveService { +class PlayerService extends BaseHiveService { PlayerService(this.fileService); final RegExp _fileExtensionRegex = RegExp(r'\.[0-9a-z]+$', caseSensitive: false); @@ -20,7 +19,7 @@ class PlayerService extends BaseHiveService { List? playerIds, bool includeDeleted = false, }) async { - if (!await ensureBoxOpen(HiveBoxes.players)) { + if (!await ensureBoxOpen()) { return []; } @@ -42,7 +41,7 @@ class PlayerService extends BaseHiveService { } Future addOrUpdatePlayer(Player player) async { - if ((player.name?.isEmpty ?? true) || !await ensureBoxOpen(HiveBoxes.players)) { + if ((player.name?.isEmpty ?? true) || !await ensureBoxOpen()) { return false; } @@ -74,7 +73,7 @@ class PlayerService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.players)) { + if (!await ensureBoxOpen()) { return false; } diff --git a/board_games_companion/lib/services/playthroughs_service.dart b/board_games_companion/lib/services/playthroughs_service.dart index 1654ad20..d5815a26 100644 --- a/board_games_companion/lib/services/playthroughs_service.dart +++ b/board_games_companion/lib/services/playthroughs_service.dart @@ -3,7 +3,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; import '../common/enums/playthrough_status.dart'; -import '../common/hive_boxes.dart'; import '../models/hive/playthrough.dart'; import '../models/hive/score.dart'; import '../models/playthrough_player.dart'; @@ -11,7 +10,7 @@ import 'hive_base_service.dart'; import 'score_service.dart'; @singleton -class PlaythroughService extends BaseHiveService { +class PlaythroughService extends BaseHiveService { PlaythroughService(this.scoreService); final ScoreService scoreService; @@ -24,7 +23,7 @@ class PlaythroughService extends BaseHiveService { return []; } - if (!await ensureBoxOpen(HiveBoxes.playthroughs)) { + if (!await ensureBoxOpen()) { return []; } @@ -50,11 +49,11 @@ class PlaythroughService extends BaseHiveService { } final playthroughPlayerIds = playthoughPlayers.map((p) => p.player.id).toList(); - if (!await ensureBoxOpen(HiveBoxes.playthroughs)) { + if (!await ensureBoxOpen()) { return null; } - final newPlaythrough = Playthrough( + var newPlaythrough = Playthrough( id: uuid.v4(), boardGameId: boardGameId, playerIds: playthroughPlayerIds, @@ -64,33 +63,36 @@ class PlaythroughService extends BaseHiveService { ); if (duration == null) { - newPlaythrough.status = PlaythroughStatus.Started; + newPlaythrough = newPlaythrough.copyWith(status: PlaythroughStatus.Started); } else { - newPlaythrough.status = PlaythroughStatus.Finished; - newPlaythrough.endDate = startDate.add(duration); + newPlaythrough.copyWith( + status: PlaythroughStatus.Finished, + endDate: startDate.add(duration), + ); } try { await storageBox.put(newPlaythrough.id, newPlaythrough); for (final String playthroughPlayerId in playthroughPlayerIds) { - Score playerScore = Score( - id: uuid.v4(), - playerId: playthroughPlayerId, - boardGameId: boardGameId, - ); - - if (playerScores.containsKey(playthroughPlayerId)) { - playerScore = playerScores[playthroughPlayerId]!.score; + var playerScore = playerScores[playthroughPlayerId]?.score ?? + Score( + id: uuid.v4(), + playerId: playthroughPlayerId, + boardGameId: boardGameId, + playthroughId: newPlaythrough.id, + ); + if (playerScore.playthroughId == null) { + playerScore = playerScore.copyWith(playthroughId: newPlaythrough.id); } - playerScore.playthroughId = newPlaythrough.id; if (!await scoreService.addOrUpdateScore(playerScore)) { FirebaseCrashlytics.instance.log( 'Faild to create a player score for player $playthroughPlayerId for a board game $boardGameId', ); } else { - newPlaythrough.scoreIds.add(playerScore.id); + newPlaythrough = newPlaythrough.copyWith( + scoreIds: newPlaythrough.scoreIds.toList()..add(playerScore.id)); } } @@ -103,7 +105,7 @@ class PlaythroughService extends BaseHiveService { } Future updatePlaythrough(Playthrough playthrough) async { - if ((playthrough.id.isEmpty) || !await ensureBoxOpen(HiveBoxes.playthroughs)) { + if ((playthrough.id.isEmpty) || !await ensureBoxOpen()) { return false; } @@ -118,7 +120,7 @@ class PlaythroughService extends BaseHiveService { } Future deletePlaythrough(String playthroughId) async { - if ((playthroughId.isEmpty) || !await ensureBoxOpen(HiveBoxes.playthroughs)) { + if ((playthroughId.isEmpty) || !await ensureBoxOpen()) { return false; } @@ -127,15 +129,13 @@ class PlaythroughService extends BaseHiveService { return false; } - playthroughToDelete.isDeleted = true; - - await storageBox.put(playthroughId, playthroughToDelete); + await storageBox.put(playthroughId, playthroughToDelete.copyWith(isDeleted: true)); return true; } Future deletePlaythroughsForGames(List boardGameIds) async { - if ((boardGameIds.isEmpty) || !await ensureBoxOpen(HiveBoxes.playthroughs)) { + if ((boardGameIds.isEmpty) || !await ensureBoxOpen()) { return false; } @@ -146,13 +146,9 @@ class PlaythroughService extends BaseHiveService { return false; } - for (final playthroughToDelete in playthroughsToDelete) { - playthroughToDelete.isDeleted = true; - } - final Map mappedPlaythroughs = { for (final playthroughToDelete in playthroughsToDelete) - playthroughToDelete.id: playthroughToDelete + playthroughToDelete.id: playthroughToDelete.copyWith(isDeleted: true) }; await storageBox.putAll(mappedPlaythroughs); @@ -161,7 +157,7 @@ class PlaythroughService extends BaseHiveService { } Future deleteAllPlaythrough() async { - if (!await ensureBoxOpen(HiveBoxes.playthroughs)) { + if (!await ensureBoxOpen()) { return false; } @@ -170,12 +166,9 @@ class PlaythroughService extends BaseHiveService { return false; } - for (final playthrough in playthroughs) { - playthrough.isDeleted = true; - } - await storageBox.putAll({ - for (Playthrough playthrough in playthroughs) playthrough.id: playthrough + for (Playthrough playthrough in playthroughs) + playthrough.id: playthrough.copyWith(isDeleted: true) }); return true; diff --git a/board_games_companion/lib/services/preferences_service.dart b/board_games_companion/lib/services/preferences_service.dart index 535a4040..5835e20e 100644 --- a/board_games_companion/lib/services/preferences_service.dart +++ b/board_games_companion/lib/services/preferences_service.dart @@ -1,24 +1,22 @@ import 'package:injectable/injectable.dart'; -import '../common/hive_boxes.dart'; import 'hive_base_service.dart'; @singleton -class PreferencesService extends BaseHiveService { +class PreferencesService extends BaseHiveService { static const String _firstTimeAppLaunchDateKey = 'firstTimeLaunchDate'; static const String _appLaunchDateKey = 'applaunchDate'; static const String _remindMeLaterDateKey = 'remindMeLater'; static const String _numberOfSignificantActionsKey = 'numberOfSignificantActions'; static const String _rateAndReviewDialogSeenKey = 'rateAndReviewDialogSeen'; static const String _expansionsPanelExpandedStateKey = 'expansionsPanelExpandedState'; - static const String _migratedToMultipleCollectionsKey = 'migratedToMultipleCollections'; Future initialize() async { - await ensureBoxOpen(HiveBoxes.preferences); + await ensureBoxOpen(); } Future setAppLaunchDate() async { - final DateTime nowUtc = DateTime.now().toUtc(); + final nowUtc = DateTime.now().toUtc(); if (_isFirstTimeAppLaunch()) { await _setValue(_firstTimeAppLaunchDateKey, nowUtc); } @@ -72,21 +70,6 @@ class PreferencesService extends BaseHiveService { )!; } - bool getMigratedToMultipleCollections() { - return _getValue( - _migratedToMultipleCollectionsKey, - defaultValue: false, - )!; - } - - Future setMigratedToMultipleCollections( - {required bool migratedToMultipleCollections}) async { - await _setValue( - _migratedToMultipleCollectionsKey, - migratedToMultipleCollections, - ); - } - Future setNumberOfSignificantActions(int numberOfSignificantActions) async { await _setValue( _numberOfSignificantActionsKey, diff --git a/board_games_companion/lib/services/score_service.dart b/board_games_companion/lib/services/score_service.dart index 15ab2a91..7e30838b 100644 --- a/board_games_companion/lib/services/score_service.dart +++ b/board_games_companion/lib/services/score_service.dart @@ -1,11 +1,10 @@ import 'package:injectable/injectable.dart'; -import '../common/hive_boxes.dart'; import '../models/hive/score.dart'; import 'hive_base_service.dart'; @singleton -class ScoreService extends BaseHiveService { +class ScoreService extends BaseHiveService { Future addOrUpdateScore(Score score) async { if ((score.playthroughId?.isEmpty ?? true) || (score.playerId.isEmpty) || @@ -13,7 +12,7 @@ class ScoreService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.scores)) { + if (!await ensureBoxOpen()) { return false; } @@ -23,7 +22,7 @@ class ScoreService extends BaseHiveService { } Future> retrieveScores(Iterable playthroughIds) async { - if ((playthroughIds.isEmpty) || !await ensureBoxOpen(HiveBoxes.scores)) { + if ((playthroughIds.isEmpty) || !await ensureBoxOpen()) { return []; } diff --git a/board_games_companion/lib/services/user_service.dart b/board_games_companion/lib/services/user_service.dart index d167c581..7203241c 100644 --- a/board_games_companion/lib/services/user_service.dart +++ b/board_games_companion/lib/services/user_service.dart @@ -1,13 +1,12 @@ import 'package:injectable/injectable.dart'; -import '../common/hive_boxes.dart'; import '../models/hive/user.dart'; import 'hive_base_service.dart'; @singleton -class UserService extends BaseHiveService { +class UserService extends BaseHiveService { Future retrieveUser() async { - if (!await ensureBoxOpen(HiveBoxes.user)) { + if (!await ensureBoxOpen()) { return null; } @@ -23,7 +22,7 @@ class UserService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.user)) { + if (!await ensureBoxOpen()) { return false; } @@ -37,7 +36,7 @@ class UserService extends BaseHiveService { return false; } - if (!await ensureBoxOpen(HiveBoxes.user)) { + if (!await ensureBoxOpen()) { return false; } diff --git a/board_games_companion/lib/stores/app_store.dart b/board_games_companion/lib/stores/app_store.dart new file mode 100644 index 00000000..8ef40e10 --- /dev/null +++ b/board_games_companion/lib/stores/app_store.dart @@ -0,0 +1,17 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; + +part 'app_store.g.dart'; + +@singleton +class AppStore = _AppStore with _$AppStore; + +abstract class _AppStore with Store { + @observable + bool? backupRestored; + + @action + void setBackupRestore(bool value) => backupRestored = value; +} diff --git a/board_games_companion/lib/stores/app_store.g.dart b/board_games_companion/lib/stores/app_store.g.dart new file mode 100644 index 00000000..c5e29d22 --- /dev/null +++ b/board_games_companion/lib/stores/app_store.g.dart @@ -0,0 +1,48 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$AppStore on _AppStore, Store { + late final _$backupRestoredAtom = + Atom(name: '_AppStore.backupRestored', context: context); + + @override + bool? get backupRestored { + _$backupRestoredAtom.reportRead(); + return super.backupRestored; + } + + @override + set backupRestored(bool? value) { + _$backupRestoredAtom.reportWrite(value, super.backupRestored, () { + super.backupRestored = value; + }); + } + + late final _$_AppStoreActionController = + ActionController(name: '_AppStore', context: context); + + @override + void setBackupRestore(bool value) { + final _$actionInfo = _$_AppStoreActionController.startAction( + name: '_AppStore.setBackupRestore'); + try { + return super.setBackupRestore(value); + } finally { + _$_AppStoreActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +backupRestored: ${backupRestored} + '''; + } +} diff --git a/board_games_companion/lib/stores/board_games_store.dart b/board_games_companion/lib/stores/board_games_store.dart index f9cd1df5..921c6d26 100644 --- a/board_games_companion/lib/stores/board_games_store.dart +++ b/board_games_companion/lib/stores/board_games_store.dart @@ -11,6 +11,7 @@ import '../models/hive/board_game_expansion.dart'; import '../models/import_result.dart'; import '../services/board_games_service.dart'; import '../services/playthroughs_service.dart'; +import 'app_store.dart'; part 'board_games_store.g.dart'; @@ -21,10 +22,19 @@ abstract class _BoardGamesStore with Store { _BoardGamesStore( this._boardGamesService, this._playthroughService, - ); + this._appStore, + ) { + // MK When restoring a backup, reload board games + reaction((_) => _appStore.backupRestored, (bool? backupRestored) async { + if (backupRestored ?? false) { + await loadBoardGames(); + } + }); + } final BoardGamesService _boardGamesService; final PlaythroughService _playthroughService; + final AppStore _appStore; @observable ObservableList allBoardGames = ObservableList.of([]); diff --git a/board_games_companion/lib/stores/players_store.dart b/board_games_companion/lib/stores/players_store.dart index 99176ea0..98914cbb 100644 --- a/board_games_companion/lib/stores/players_store.dart +++ b/board_games_companion/lib/stores/players_store.dart @@ -2,6 +2,7 @@ import 'package:board_games_companion/models/hive/player.dart'; import 'package:board_games_companion/services/player_service.dart'; +import 'package:board_games_companion/stores/app_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; @@ -13,9 +14,20 @@ part 'players_store.g.dart'; class PlayersStore = _PlayersStore with _$PlayersStore; abstract class _PlayersStore with Store { - _PlayersStore(this._playerService); + _PlayersStore( + this._playerService, + this._appStore, + ) { + // MK When restoring a backup, reload players + reaction((_) => _appStore.backupRestored, (bool? backupRestored) async { + if (backupRestored ?? false) { + await loadPlayers(); + } + }); + } final PlayerService _playerService; + final AppStore _appStore; @observable ObservableList players = ObservableList.of([]); diff --git a/board_games_companion/lib/stores/playthroughs_store.dart b/board_games_companion/lib/stores/playthroughs_store.dart index 07a0f1db..1695da5e 100644 --- a/board_games_companion/lib/stores/playthroughs_store.dart +++ b/board_games_companion/lib/stores/playthroughs_store.dart @@ -1,13 +1,21 @@ // ignore_for_file: library_private_types_in_public_api +import 'package:board_games_companion/extensions/scores_extensions.dart'; import 'package:board_games_companion/models/player_score.dart'; +import 'package:board_games_companion/services/score_service.dart'; +import 'package:collection/collection.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; +import '../common/enums/game_winning_condition.dart'; import '../models/hive/board_game_details.dart'; +import '../models/hive/player.dart'; import '../models/hive/playthrough.dart'; +import '../models/hive/score.dart'; +import '../models/playthrough_details.dart'; import '../models/playthrough_player.dart'; +import '../services/player_service.dart'; import '../services/playthroughs_service.dart'; part 'playthroughs_store.g.dart'; @@ -16,14 +24,16 @@ part 'playthroughs_store.g.dart'; class PlaythroughsStore = _PlaythroughsStore with _$PlaythroughsStore; abstract class _PlaythroughsStore with Store { - _PlaythroughsStore(this._playthroughService); + _PlaythroughsStore(this._playthroughService, this._scoreService, this._playerService); final PlaythroughService _playthroughService; + final ScoreService _scoreService; + final PlayerService _playerService; late BoardGameDetails boardGame; @observable - ObservableList playthroughs = ObservableList.of([]); + ObservableList playthroughs = ObservableList.of([]); @action Future loadPlaythroughs() async { @@ -31,9 +41,14 @@ abstract class _PlaythroughsStore with Store { return; } + playthroughs.clear(); + try { - playthroughs = - ObservableList.of(await _playthroughService.retrievePlaythroughs([boardGame.id])); + final hivePlaythrough = await _playthroughService.retrievePlaythroughs([boardGame.id]); + for (final hivePlaythrough in hivePlaythrough) { + final playthrough = await createPlaythroughDetails(hivePlaythrough); + playthroughs.add(playthrough); + } } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); } @@ -42,7 +57,7 @@ abstract class _PlaythroughsStore with Store { @action void setBoardGame(BoardGameDetails boardGame) => this.boardGame = boardGame; - Future createPlaythrough( + Future createPlaythrough( String boardGameId, List playthoughPlayers, Map playerScores, @@ -50,7 +65,7 @@ abstract class _PlaythroughsStore with Store { Duration? duration, { int? bggPlayId, }) async { - final newPlaythrough = await _playthroughService.createPlaythrough( + final newHivePlaythrough = await _playthroughService.createPlaythrough( boardGameId, playthoughPlayers, playerScores, @@ -59,7 +74,7 @@ abstract class _PlaythroughsStore with Store { bggPlayId: bggPlayId, ); - if (newPlaythrough == null) { + if (newHivePlaythrough == null) { FirebaseCrashlytics.instance.log( 'Faild to new playthrough for a board game $boardGameId with ${playthoughPlayers.length} players', ); @@ -67,34 +82,35 @@ abstract class _PlaythroughsStore with Store { return null; } - playthroughs.add(newPlaythrough); - - return newPlaythrough; + final playthrough = await createPlaythroughDetails(newHivePlaythrough); + playthroughs.add(playthrough); + return playthrough; } - Future updatePlaythrough(Playthrough? playthrough) async { + Future updatePlaythrough(PlaythroughDetails? playthrough) async { if (playthrough?.id.isEmpty ?? true) { - return false; + return; } try { - final updateSuceeded = await _playthroughService.updatePlaythrough(playthrough!); + final updateSuceeded = await _playthroughService.updatePlaythrough(playthrough!.playthrough); if (updateSuceeded) { - loadPlaythroughs(); - return true; + for (final PlayerScore playerScore in playthrough.playerScores) { + await _scoreService.addOrUpdateScore(playerScore.score); + } + + await loadPlaythroughs(); } } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); } - - return false; } Future deletePlaythrough(String playthroughId) async { try { final deleteSucceeded = await _playthroughService.deletePlaythrough(playthroughId); if (deleteSucceeded) { - playthroughs.removeWhere((p) => p.id == playthroughId); + playthroughs.removeWhere((p) => p.playthrough.id == playthroughId); } return deleteSucceeded; @@ -104,4 +120,24 @@ abstract class _PlaythroughsStore with Store { return false; } + + Future createPlaythroughDetails(Playthrough hivePlaythrough) async { + final scores = (await _scoreService.retrieveScores([hivePlaythrough.id])) + ..sortByScore(boardGame.settings?.winningCondition ?? GameWinningCondition.HighestScore) + ..toList(); + final players = await _playerService.retrievePlayers( + playerIds: hivePlaythrough.playerIds, + includeDeleted: true, + ); + + final playerScores = scores.mapIndexed((int index, Score score) { + final player = players.firstWhereOrNull((Player p) => score.playerId == p.id); + return PlayerScore(player: player, score: score, place: index + 1); + }).toList(); + + return PlaythroughDetails( + playthrough: hivePlaythrough, + playerScores: playerScores, + ); + } } diff --git a/board_games_companion/lib/stores/playthroughs_store.g.dart b/board_games_companion/lib/stores/playthroughs_store.g.dart index 8559c6af..09392a54 100644 --- a/board_games_companion/lib/stores/playthroughs_store.g.dart +++ b/board_games_companion/lib/stores/playthroughs_store.g.dart @@ -13,13 +13,13 @@ mixin _$PlaythroughsStore on _PlaythroughsStore, Store { Atom(name: '_PlaythroughsStore.playthroughs', context: context); @override - ObservableList get playthroughs { + ObservableList get playthroughs { _$playthroughsAtom.reportRead(); return super.playthroughs; } @override - set playthroughs(ObservableList value) { + set playthroughs(ObservableList value) { _$playthroughsAtom.reportWrite(value, super.playthroughs, () { super.playthroughs = value; }); diff --git a/board_games_companion/lib/stores/user_store.dart b/board_games_companion/lib/stores/user_store.dart index 9a383059..50be2d48 100644 --- a/board_games_companion/lib/stores/user_store.dart +++ b/board_games_companion/lib/stores/user_store.dart @@ -1,24 +1,53 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:basics/basics.dart'; +import 'package:board_games_companion/stores/app_store.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; import '../models/hive/user.dart'; import '../services/user_service.dart'; -class UserStore with ChangeNotifier { - UserStore(this._userService); +part 'user_store.g.dart'; + +@singleton +class UserStore = _UserStore with _$UserStore; + +abstract class _UserStore with Store { + _UserStore( + this._userService, + this._appStore, + ) { + // MK When restoring a backup, reload user data + reaction((_) => _appStore.backupRestored, (bool? backupRestored) async { + if (backupRestored ?? false) { + await loadUser(); + } + }); + } final UserService _userService; + final AppStore _appStore; + + @observable + User? user; - User? _user; - User? get user => _user; + @observable + ObservableFuture? futureLoadUser; - Future loadUser() async { + @computed + String? get userName => user?.name; + + @computed + bool get hasUser => userName.isNotNullOrBlank; + + @action + ObservableFuture loadUser() => futureLoadUser = ObservableFuture(_loadUser()); + + Future _loadUser() async { try { - final user = await _userService.retrieveUser(); - if (user != null) { - _user = user; - notifyListeners(); - } + user = await _userService.retrieveUser(); } catch (e, stack) { FirebaseCrashlytics.instance.recordError(e, stack); } @@ -28,8 +57,7 @@ class UserStore with ChangeNotifier { try { final addOrUpdateUserSucceeded = await _userService.addOrUpdateUser(user); if (addOrUpdateUserSucceeded) { - _user = user; - notifyListeners(); + this.user = user; } return true; @@ -40,11 +68,14 @@ class UserStore with ChangeNotifier { return false; } - Future removeUser(User user) async { + Future removeUser() async { + if (user == null) { + return false; + } + try { - if (await _userService.removeUser(user)) { - _user = null; - notifyListeners(); + if (await _userService.removeUser(user!)) { + user = null; } return true; diff --git a/board_games_companion/lib/stores/user_store.g.dart b/board_games_companion/lib/stores/user_store.g.dart new file mode 100644 index 00000000..021f1149 --- /dev/null +++ b/board_games_companion/lib/stores/user_store.g.dart @@ -0,0 +1,79 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$UserStore on _UserStore, Store { + Computed? _$userNameComputed; + + @override + String? get userName => (_$userNameComputed ??= + Computed(() => super.userName, name: '_UserStore.userName')) + .value; + Computed? _$hasUserComputed; + + @override + bool get hasUser => (_$hasUserComputed ??= + Computed(() => super.hasUser, name: '_UserStore.hasUser')) + .value; + + late final _$userAtom = Atom(name: '_UserStore.user', context: context); + + @override + User? get user { + _$userAtom.reportRead(); + return super.user; + } + + @override + set user(User? value) { + _$userAtom.reportWrite(value, super.user, () { + super.user = value; + }); + } + + late final _$futureLoadUserAtom = + Atom(name: '_UserStore.futureLoadUser', context: context); + + @override + ObservableFuture? get futureLoadUser { + _$futureLoadUserAtom.reportRead(); + return super.futureLoadUser; + } + + @override + set futureLoadUser(ObservableFuture? value) { + _$futureLoadUserAtom.reportWrite(value, super.futureLoadUser, () { + super.futureLoadUser = value; + }); + } + + late final _$_UserStoreActionController = + ActionController(name: '_UserStore', context: context); + + @override + ObservableFuture loadUser() { + final _$actionInfo = + _$_UserStoreActionController.startAction(name: '_UserStore.loadUser'); + try { + return super.loadUser(); + } finally { + _$_UserStoreActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +user: ${user}, +futureLoadUser: ${futureLoadUser}, +userName: ${userName}, +hasUser: ${hasUser} + '''; + } +} diff --git a/board_games_companion/lib/utilities/analytics_route_observer.dart b/board_games_companion/lib/utilities/analytics_route_observer.dart index 86dc418e..934333c6 100644 --- a/board_games_companion/lib/utilities/analytics_route_observer.dart +++ b/board_games_companion/lib/utilities/analytics_route_observer.dart @@ -49,12 +49,12 @@ class AnalyticsRouteObserver extends RouteObserver> { }, ); break; - case EditPlaythoughPage.pageRoute: + case EditPlaythroughPage.pageRoute: final arguments = route.settings.arguments as EditPlaythroughPageArguments; _analtyicsService.logEvent( name: Analytics.editPlaythrough, parameters: { - Analytics.boardGameIdParameter: arguments.playthroughViewModel.playthrough.boardGameId, + Analytics.boardGameIdParameter: arguments.boardGameId, }, ); break; diff --git a/board_games_companion/lib/widgets/common/import_collections_button.dart b/board_games_companion/lib/widgets/common/import_collections_button.dart index 3aa1ec0c..cb6792cb 100644 --- a/board_games_companion/lib/widgets/common/import_collections_button.dart +++ b/board_games_companion/lib/widgets/common/import_collections_button.dart @@ -61,7 +61,9 @@ class ImportCollectionsButtonState extends State @override Widget build(BuildContext context) { - return _AnimatedButton( + return AnimatedButton( + text: AppText.importCollectionsButtonText, + icon: const DefaultIcon(Icons.download), sizeAnimationController: _sizeAnimationController, fadeInAnimationController: _fadeInAnimationController, onPressed: () => _import(context), @@ -69,9 +71,6 @@ class ImportCollectionsButtonState extends State } Future _import(BuildContext context) async { - _fadeInAnimationController.forward(); - _sizeAnimationController.reverse(); - await importCollections(context, widget._usernameCallback()); if (mounted) { @@ -81,13 +80,17 @@ class ImportCollectionsButtonState extends State } } -class _AnimatedButton extends AnimatedWidget { - const _AnimatedButton({ - Key? key, +class AnimatedButton extends AnimatedWidget { + const AnimatedButton({ + required String text, + required Widget icon, required AnimationController sizeAnimationController, required AnimationController fadeInAnimationController, - required VoidCallback onPressed, - }) : _sizeAnimationController = sizeAnimationController, + required Future Function() onPressed, + Key? key, + }) : _text = text, + _icon = icon, + _sizeAnimationController = sizeAnimationController, _fadeInAnimationController = fadeInAnimationController, _onPressed = onPressed, super( @@ -95,10 +98,12 @@ class _AnimatedButton extends AnimatedWidget { listenable: sizeAnimationController, ); + final String _text; + final Widget _icon; final AnimationController _sizeAnimationController; final AnimationController _fadeInAnimationController; - final VoidCallback _onPressed; + final Future Function() _onPressed; @override Widget build(BuildContext context) { @@ -107,9 +112,14 @@ class _AnimatedButton extends AnimatedWidget { Transform.scale( scale: _sizeAnimationController.value, child: ElevatedIconButton( - title: AppText.importCollectionsButtonText, - icon: const DefaultIcon(Icons.download), - onPressed: _onPressed, + title: _text, + icon: _icon, + onPressed: () async { + _fadeInAnimationController.forward(); + _sizeAnimationController.reverse(); + + await _onPressed(); + }, ), ), Positioned.fill( diff --git a/board_games_companion/pubspec.lock b/board_games_companion/pubspec.lock index 7a755f99..57a9153d 100644 --- a/board_games_companion/pubspec.lock +++ b/board_games_companion/pubspec.lock @@ -23,12 +23,12 @@ packages: source: hosted version: "2.0.3" archive: - dependency: transitive + dependency: "direct main" description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.3.1" args: dependency: transitive description: @@ -274,6 +274,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fimber: dependency: "direct main" description: @@ -412,6 +419,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_svg: dependency: "direct main" description: @@ -436,6 +450,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "10.1.0" + freezed: + dependency: "direct main" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0+1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" frontend_server_client: dependency: transitive description: @@ -731,7 +759,7 @@ packages: source: hosted version: "2.0.2" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -884,6 +912,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.27.5" + share_plus: + dependency: "direct main" + description: + name: share_plus + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.10+1" + share_plus_linux: + dependency: transitive + description: + name: share_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + share_plus_macos: + dependency: transitive + description: + name: share_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3" + share_plus_web: + dependency: transitive + description: + name: share_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_windows: + dependency: transitive + description: + name: share_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" shelf: dependency: transitive description: diff --git a/board_games_companion/pubspec.yaml b/board_games_companion/pubspec.yaml index 54799dfe..12426fc4 100644 --- a/board_games_companion/pubspec.yaml +++ b/board_games_companion/pubspec.yaml @@ -18,6 +18,7 @@ environment: dependencies: animations: ^2.0.1 + archive: ^3.3.1 async: ^2.8.2 basics: ^0.9.0 cached_network_image: ^3.2.1 @@ -27,6 +28,7 @@ dependencies: dio_cache_interceptor: ^3.3.0 dio_cache_interceptor_hive_store: ^3.2.0 dio_http2_adapter: ^2.0.0 + file_picker: ^5.0.1 fimber: ^0.6.6 firebase_core: ^1.7.0 firebase_crashlytics: ^2.2.2 @@ -34,8 +36,11 @@ dependencies: fl_chart: ^0.55.0 flutter_mobx: ^2.0.6+1 flutter_polygon: ^0.2.0 + flutter_slidable: ^2.0.0 flutter_svg: ^0.22.0 font_awesome_flutter: ^10.1.0 + freezed: ^2.1.0+1 + freezed_annotation: ^2.1.0 google_fonts: ^3.0.1 get_it: ^7.2.0 hive: ^2.2.3 @@ -48,10 +53,12 @@ dependencies: logger: ^1.1.0 mobx: ^2.0.7+4 numberpicker: ^2.1.1 + path: ^1.8.0 path_provider: ^2.0.5 provider: ^6.0.1 package_info: ^2.0.2 retry: ^3.1.0 + share_plus: ^4.0.10+1 sprintf: ^6.0.0 tuple: ^2.0.0 url_launcher: ^6.0.12