372 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:cabo_counter/core/constants.dart';
 | |
| import 'package:cabo_counter/core/custom_theme.dart';
 | |
| import 'package:cabo_counter/data/game_manager.dart';
 | |
| import 'package:cabo_counter/l10n/generated/app_localizations.dart';
 | |
| import 'package:cabo_counter/presentation/views/active_game_view.dart';
 | |
| import 'package:cabo_counter/presentation/views/create_game_view.dart';
 | |
| import 'package:cabo_counter/presentation/views/settings_view.dart';
 | |
| import 'package:cabo_counter/services/config_service.dart';
 | |
| import 'package:cabo_counter/services/local_storage_service.dart';
 | |
| import 'package:flutter/cupertino.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:url_launcher/url_launcher.dart';
 | |
| 
 | |
| enum PreRatingDialogDecision { yes, no, cancel }
 | |
| 
 | |
| enum BadRatingDialogDecision { email, cancel }
 | |
| 
 | |
| class MainMenuView extends StatefulWidget {
 | |
|   const MainMenuView({super.key});
 | |
| 
 | |
|   @override
 | |
|   // ignore: library_private_types_in_public_api
 | |
|   _MainMenuViewState createState() => _MainMenuViewState();
 | |
| }
 | |
| 
 | |
| class _MainMenuViewState extends State<MainMenuView> {
 | |
|   bool _isLoading = true;
 | |
| 
 | |
|   @override
 | |
|   initState() {
 | |
|     super.initState();
 | |
|     LocalStorageService.loadGameSessions().then((_) {
 | |
|       setState(() {
 | |
|         _isLoading = false;
 | |
|       });
 | |
|     });
 | |
|     gameManager.addListener(_updateView);
 | |
| 
 | |
|     WidgetsBinding.instance.addPostFrameCallback((_) async {
 | |
|       await Constants.rateMyApp.init();
 | |
| 
 | |
|       if (Constants.rateMyApp.shouldOpenDialog &&
 | |
|           Constants.appDevPhase != 'Beta') {
 | |
|         await Future.delayed(const Duration(milliseconds: 600));
 | |
|         if (!mounted) return;
 | |
|         _handleFeedbackDialog(context);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   void _updateView() {
 | |
|     if (mounted) setState(() {});
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return ListenableBuilder(
 | |
|         listenable: gameManager,
 | |
|         builder: (context, _) {
 | |
|           return CupertinoPageScaffold(
 | |
|               resizeToAvoidBottomInset: false,
 | |
|               navigationBar: CupertinoNavigationBar(
 | |
|                 leading: IconButton(
 | |
|                     onPressed: () {
 | |
|                       Navigator.push(
 | |
|                         context,
 | |
|                         CupertinoPageRoute(
 | |
|                           builder: (context) => const SettingsView(),
 | |
|                         ),
 | |
|                       ).then((_) {
 | |
|                         setState(() {});
 | |
|                       });
 | |
|                     },
 | |
|                     icon: const Icon(CupertinoIcons.settings, size: 30)),
 | |
|                 middle: Text(AppLocalizations.of(context).app_name),
 | |
|                 trailing: IconButton(
 | |
|                     onPressed: () => Navigator.push(
 | |
|                           context,
 | |
|                           CupertinoPageRoute(
 | |
|                             builder: (context) => CreateGameView(
 | |
|                                 gameMode: ConfigService.getGameMode()),
 | |
|                           ),
 | |
|                         ),
 | |
|                     icon: const Icon(CupertinoIcons.add)),
 | |
|               ),
 | |
|               child: CupertinoPageScaffold(
 | |
|                   child: SafeArea(
 | |
|                 child: Visibility(
 | |
|                   visible: _isLoading,
 | |
|                   replacement: Visibility(
 | |
|                     visible: gameManager.gameList.isEmpty,
 | |
|                     replacement: ListView.separated(
 | |
|                       itemCount: gameManager.gameList.length,
 | |
|                       separatorBuilder: (context, index) => Divider(
 | |
|                         height: 1,
 | |
|                         thickness: 0.5,
 | |
|                         color: CustomTheme.white.withAlpha(50),
 | |
|                         indent: 50,
 | |
|                         endIndent: 50,
 | |
|                       ),
 | |
|                       itemBuilder: (context, index) {
 | |
|                         final session = gameManager.gameList[index];
 | |
|                         return ListenableBuilder(
 | |
|                             listenable: session,
 | |
|                             builder: (context, _) {
 | |
|                               return Dismissible(
 | |
|                                 key: Key(session.id),
 | |
|                                 background: Container(
 | |
|                                   color: CupertinoColors.destructiveRed,
 | |
|                                   alignment: Alignment.centerRight,
 | |
|                                   padding: const EdgeInsets.only(right: 20.0),
 | |
|                                   child: const Icon(
 | |
|                                     CupertinoIcons.delete,
 | |
|                                     color: CupertinoColors.white,
 | |
|                                   ),
 | |
|                                 ),
 | |
|                                 direction: DismissDirection.endToStart,
 | |
|                                 confirmDismiss: (direction) async {
 | |
|                                   return await _showDeleteGamePopup(
 | |
|                                       context, session.gameTitle);
 | |
|                                 },
 | |
|                                 onDismissed: (direction) {
 | |
|                                   gameManager.removeGameSessionById(session.id);
 | |
|                                 },
 | |
|                                 dismissThresholds: const {
 | |
|                                   DismissDirection.startToEnd: 0.6
 | |
|                                 },
 | |
|                                 child: Padding(
 | |
|                                   padding: const EdgeInsets.symmetric(
 | |
|                                       vertical: 10.0),
 | |
|                                   child: CupertinoListTile(
 | |
|                                     backgroundColorActivated:
 | |
|                                         CustomTheme.backgroundColor,
 | |
|                                     title: Text(session.gameTitle),
 | |
|                                     subtitle: Visibility(
 | |
|                                         visible: session.isGameFinished,
 | |
|                                         replacement: Text(
 | |
|                                           '${AppLocalizations.of(context).mode}: ${_translateGameMode(session.isPointsLimitEnabled)}',
 | |
|                                           style: const TextStyle(fontSize: 14),
 | |
|                                         ),
 | |
|                                         child: Text(
 | |
|                                           '\u{1F947} ${session.winner}',
 | |
|                                           style: const TextStyle(fontSize: 14),
 | |
|                                         )),
 | |
|                                     trailing: Row(
 | |
|                                       children: [
 | |
|                                         const SizedBox(
 | |
|                                           width: 5,
 | |
|                                         ),
 | |
|                                         Text('${session.roundNumber}'),
 | |
|                                         const SizedBox(width: 3),
 | |
|                                         const Icon(CupertinoIcons
 | |
|                                             .arrow_2_circlepath_circle_fill),
 | |
|                                         const SizedBox(width: 15),
 | |
|                                         Text('${session.players.length}'),
 | |
|                                         const SizedBox(width: 3),
 | |
|                                         const Icon(
 | |
|                                             CupertinoIcons.person_2_fill),
 | |
|                                       ],
 | |
|                                     ),
 | |
|                                     onTap: () {
 | |
|                                       final session =
 | |
|                                           gameManager.gameList[index];
 | |
|                                       Navigator.push(
 | |
|                                         context,
 | |
|                                         CupertinoPageRoute(
 | |
|                                           builder: (context) => ActiveGameView(
 | |
|                                               gameSession: session),
 | |
|                                         ),
 | |
|                                       ).then((_) {
 | |
|                                         setState(() {});
 | |
|                                       });
 | |
|                                     },
 | |
|                                   ),
 | |
|                                 ),
 | |
|                               );
 | |
|                             });
 | |
|                       },
 | |
|                     ),
 | |
|                     child: Column(
 | |
|                       mainAxisAlignment: MainAxisAlignment.center,
 | |
|                       children: [
 | |
|                         const SizedBox(height: 30),
 | |
|                         Center(
 | |
|                             child: GestureDetector(
 | |
|                           onTap: () => Navigator.push(
 | |
|                             context,
 | |
|                             CupertinoPageRoute(
 | |
|                               builder: (context) => CreateGameView(
 | |
|                                   gameMode: ConfigService.getGameMode()),
 | |
|                             ),
 | |
|                           ),
 | |
|                           child: Icon(
 | |
|                             CupertinoIcons.plus,
 | |
|                             size: 60,
 | |
|                             color: CustomTheme.primaryColor,
 | |
|                           ),
 | |
|                         )),
 | |
|                         const SizedBox(height: 10),
 | |
|                         Padding(
 | |
|                           padding: const EdgeInsets.symmetric(horizontal: 70),
 | |
|                           child: Text(
 | |
|                             '${AppLocalizations.of(context).empty_text_1}\n${AppLocalizations.of(context).empty_text_2}',
 | |
|                             textAlign: TextAlign.center,
 | |
|                             style: const TextStyle(fontSize: 16),
 | |
|                           ),
 | |
|                         ),
 | |
|                       ],
 | |
|                     ),
 | |
|                   ),
 | |
|                   child: const Center(child: CupertinoActivityIndicator()),
 | |
|                 ),
 | |
|               )));
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   /// Translates the game mode boolean into the corresponding String.
 | |
|   /// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'.
 | |
|   String _translateGameMode(bool isPointLimitEnabled) {
 | |
|     if (isPointLimitEnabled) {
 | |
|       return '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}';
 | |
|     }
 | |
|     return AppLocalizations.of(context).unlimited;
 | |
|   }
 | |
| 
 | |
|   /// Handles the feedback dialog when the conditions for rating are met.
 | |
|   /// It shows a dialog asking the user if they like the app,
 | |
|   /// and based on their response, it either opens the rating dialog or an email client for feedback.
 | |
|   Future<void> _handleFeedbackDialog(BuildContext context) async {
 | |
|     final String emailSubject = AppLocalizations.of(context).email_subject;
 | |
|     final String emailBody = AppLocalizations.of(context).email_body;
 | |
| 
 | |
|     final Uri emailUri = Uri(
 | |
|       scheme: 'mailto',
 | |
|       path: Constants.kEmail,
 | |
|       query: 'subject=$emailSubject'
 | |
|           '&body=$emailBody',
 | |
|     );
 | |
| 
 | |
|     PreRatingDialogDecision preRatingDecision =
 | |
|         await _showPreRatingDialog(context);
 | |
|     BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel;
 | |
| 
 | |
|     // so that the bad rating dialog is not shown immediately
 | |
|     await Future.delayed(const Duration(milliseconds: Constants.popUpDelay));
 | |
| 
 | |
|     switch (preRatingDecision) {
 | |
|       case PreRatingDialogDecision.yes:
 | |
|         if (context.mounted) Constants.rateMyApp.showStarRateDialog(context);
 | |
|         break;
 | |
|       case PreRatingDialogDecision.no:
 | |
|         if (context.mounted) {
 | |
|           badRatingDecision = await _showBadRatingDialog(context);
 | |
|         }
 | |
|         if (badRatingDecision == BadRatingDialogDecision.email) {
 | |
|           if (context.mounted) {
 | |
|             launchUrl(emailUri);
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|       case PreRatingDialogDecision.cancel:
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Shows a confirmation dialog to delete all game sessions.
 | |
|   /// Returns true if the user confirms the deletion, false otherwise.
 | |
|   /// [gameTitle] is the title of the game session to be deleted.
 | |
|   Future<bool> _showDeleteGamePopup(
 | |
|       BuildContext context, String gameTitle) async {
 | |
|     return await showCupertinoDialog<bool>(
 | |
|           context: context,
 | |
|           builder: (BuildContext context) {
 | |
|             return CupertinoAlertDialog(
 | |
|                 title: Text(
 | |
|                   AppLocalizations.of(context).delete_game_title,
 | |
|                 ),
 | |
|                 content: Text(AppLocalizations.of(context)
 | |
|                     .delete_game_message(gameTitle)),
 | |
|                 actions: [
 | |
|                   CupertinoDialogAction(
 | |
|                     onPressed: () {
 | |
|                       Navigator.of(context).pop(false);
 | |
|                     },
 | |
|                     child: Text(AppLocalizations.of(context).cancel),
 | |
|                   ),
 | |
|                   CupertinoDialogAction(
 | |
|                     isDestructiveAction: true,
 | |
|                     isDefaultAction: true,
 | |
|                     onPressed: () {
 | |
|                       Navigator.of(context).pop(true);
 | |
|                     },
 | |
|                     child: Text(
 | |
|                       AppLocalizations.of(context).delete,
 | |
|                     ),
 | |
|                   )
 | |
|                 ]);
 | |
|           },
 | |
|         ) ??
 | |
|         false;
 | |
|   }
 | |
| 
 | |
|   /// Shows a dialog asking the user if they like the app.
 | |
|   /// Returns the user's decision as an integer.
 | |
|   /// - PRE_RATING_DIALOG_YES: User likes the app and wants to rate it.
 | |
|   /// - PRE_RATING_DIALOG_NO: User does not like the app and wants to provide feedback.
 | |
|   /// - PRE_RATING_DIALOG_CANCEL: User cancels the dialog.
 | |
|   Future<PreRatingDialogDecision> _showPreRatingDialog(
 | |
|       BuildContext context) async {
 | |
|     return await showCupertinoDialog<PreRatingDialogDecision>(
 | |
|             context: context,
 | |
|             builder: (BuildContext context) => CupertinoAlertDialog(
 | |
|                   title: Text(AppLocalizations.of(context).pre_rating_title),
 | |
|                   content:
 | |
|                       Text(AppLocalizations.of(context).pre_rating_message),
 | |
|                   actions: [
 | |
|                     CupertinoDialogAction(
 | |
|                       onPressed: () => Navigator.of(context)
 | |
|                           .pop(PreRatingDialogDecision.yes),
 | |
|                       isDefaultAction: true,
 | |
|                       child: Text(AppLocalizations.of(context).yes),
 | |
|                     ),
 | |
|                     CupertinoDialogAction(
 | |
|                       onPressed: () =>
 | |
|                           Navigator.of(context).pop(PreRatingDialogDecision.no),
 | |
|                       child: Text(AppLocalizations.of(context).no),
 | |
|                     ),
 | |
|                     CupertinoDialogAction(
 | |
|                       onPressed: () => Navigator.of(context).pop(),
 | |
|                       isDestructiveAction: true,
 | |
|                       child: Text(AppLocalizations.of(context).cancel),
 | |
|                     )
 | |
|                   ],
 | |
|                 )) ??
 | |
|         PreRatingDialogDecision.cancel;
 | |
|   }
 | |
| 
 | |
|   /// Shows a dialog asking the user for feedback if they do not like the app.
 | |
|   /// Returns the user's decision as an integer.
 | |
|   /// - BAD_RATING_DIALOG_EMAIL: User wants to send an email with feedback.
 | |
|   /// - BAD_RATING_DIALOG_CANCEL: User cancels the dialog.
 | |
|   Future<BadRatingDialogDecision> _showBadRatingDialog(
 | |
|       BuildContext context) async {
 | |
|     return await showCupertinoDialog<BadRatingDialogDecision>(
 | |
|             context: context,
 | |
|             builder: (BuildContext context) => CupertinoAlertDialog(
 | |
|                   title: Text(AppLocalizations.of(context).bad_rating_title),
 | |
|                   content:
 | |
|                       Text(AppLocalizations.of(context).bad_rating_message),
 | |
|                   actions: [
 | |
|                     CupertinoDialogAction(
 | |
|                       isDefaultAction: true,
 | |
|                       onPressed: () => Navigator.of(context)
 | |
|                           .pop(BadRatingDialogDecision.email),
 | |
|                       child: Text(AppLocalizations.of(context).contact_email),
 | |
|                     ),
 | |
|                     CupertinoDialogAction(
 | |
|                         isDestructiveAction: true,
 | |
|                         onPressed: () => Navigator.of(context).pop(),
 | |
|                         child: Text(AppLocalizations.of(context).cancel))
 | |
|                   ],
 | |
|                 )) ??
 | |
|         BadRatingDialogDecision.cancel;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     gameManager.removeListener(_updateView);
 | |
|     super.dispose();
 | |
|   }
 | |
| }
 |