369 lines
16 KiB
Dart
369 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: [
|
|
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: 300));
|
|
|
|
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();
|
|
}
|
|
}
|