Merge branch 'develop' into enhance/34-improvement-for-visual-hierachy
# Conflicts: # lib/presentation/views/create_game_view.dart # pubspec.yaml
This commit is contained in:
78
lib/presentation/views/about_view.dart
Normal file
78
lib/presentation/views/about_view.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AboutView extends StatelessWidget {
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).about),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app_name,
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).app_version} ${VersionService.getVersionWithBuild()}',
|
||||
style: TextStyle(fontSize: 15, color: Colors.grey[300]),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('assets/cabo_counter-logo_rounded.png'),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).about_text,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
const Text(
|
||||
'\u00A9 Felix Kirchner',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kInstagramLink)),
|
||||
icon: const Icon(FontAwesomeIcons.instagram)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('mailto:${Constants.kEmail}')),
|
||||
icon: const Icon(CupertinoIcons.envelope)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubLink)),
|
||||
icon: const Icon(FontAwesomeIcons.github)),
|
||||
],
|
||||
),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
423
lib/presentation/views/active_game_view.dart
Normal file
423
lib/presentation/views/active_game_view.dart
Normal file
@@ -0,0 +1,423 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/graph_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/points_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/round_view.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActiveGameView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
const ActiveGameView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_ActiveGameViewState createState() => _ActiveGameViewState();
|
||||
}
|
||||
|
||||
class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
late final GameSession gameSession;
|
||||
late List<int> denseRanks;
|
||||
late List<int> sortedPlayerIndices;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
gameSession = widget.gameSession;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: gameSession,
|
||||
builder: (context, _) {
|
||||
sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
denseRanks = _calculateDenseRank(
|
||||
gameSession.playerScores, sortedPlayerIndices);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(gameSession.gameTitle),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: gameSession.players.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
int playerIndex = sortedPlayerIndices[index];
|
||||
return CupertinoListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
_getPlacementTextWidget(index),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
gameSession.players[playerIndex],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
Text('${gameSession.playerScores[playerIndex]} '
|
||||
'${AppLocalizations.of(context).points}')
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).rounds,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: gameSession.roundNumber,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(
|
||||
'${AppLocalizations.of(context).round} ${index + 1}',
|
||||
),
|
||||
trailing:
|
||||
index + 1 != gameSession.roundNumber ||
|
||||
gameSession.isGameFinished == true
|
||||
? (const Text('\u{2705}',
|
||||
style: TextStyle(fontSize: 22)))
|
||||
: const Text('\u{23F3}',
|
||||
style: TextStyle(fontSize: 22)),
|
||||
onTap: () async {
|
||||
_openRoundView(index + 1);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).scoring_history,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => GraphView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).point_overview,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => PointsView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: !gameSession.isPointsLimitEnabled,
|
||||
child: CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished
|
||||
? const TextStyle(color: Colors.white)
|
||||
: const TextStyle(color: Colors.white30),
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
if (gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished) {
|
||||
_showEndGameDialog();
|
||||
}
|
||||
}),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
_showDeleteGameDialog().then((value) {
|
||||
if (value) {
|
||||
_removeGameSession(gameSession);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.new_game_same_settings,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => CreateGameView(
|
||||
gameTitle: gameSession.gameTitle,
|
||||
gameMode: widget.gameSession
|
||||
.isPointsLimitEnabled ==
|
||||
true
|
||||
? GameMode.pointLimit
|
||||
: GameMode.unlimited,
|
||||
players: gameSession.players,
|
||||
)));
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).export_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () async {
|
||||
final success = await LocalStorageService
|
||||
.exportSingleGameSession(
|
||||
widget.gameSession);
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context)
|
||||
.export_error_title),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.export_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).ok),
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm ending the game.
|
||||
/// If the user confirms, it calls the `endGame` method on the game manager
|
||||
void _showEndGameDialog() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).end_game_title),
|
||||
content: Text(AppLocalizations.of(context).end_game_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.destructiveRed),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
gameManager.endGame(gameSession.id);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of player indices sorted by their scores in
|
||||
/// ascending order.
|
||||
List<int> _getSortedPlayerIndices() {
|
||||
List<int> playerIndices =
|
||||
List<int>.generate(gameSession.players.length, (index) => index);
|
||||
// Sort the indices based on the summed points
|
||||
playerIndices.sort((a, b) {
|
||||
int scoreA = gameSession.playerScores[a];
|
||||
int scoreB = gameSession.playerScores[b];
|
||||
if (scoreA != scoreB) {
|
||||
return scoreA.compareTo(scoreB);
|
||||
}
|
||||
return a.compareTo(b);
|
||||
});
|
||||
return playerIndices;
|
||||
}
|
||||
|
||||
/// Calculates the dense rank for a player based on their index in the sorted list of players.
|
||||
List<int> _calculateDenseRank(
|
||||
List<int> playerScores, List<int> sortedIndices) {
|
||||
List<int> denseRanks = [];
|
||||
int rank = 1;
|
||||
for (int i = 0; i < sortedIndices.length; i++) {
|
||||
if (i > 0) {
|
||||
int prevScore = playerScores[sortedIndices[i - 1]];
|
||||
int currScore = playerScores[sortedIndices[i]];
|
||||
if (currScore != prevScore) {
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
denseRanks.add(rank);
|
||||
}
|
||||
return denseRanks;
|
||||
}
|
||||
|
||||
/// Returns a text widget representing the placement text based on the given placement number.
|
||||
/// [index] is the index of the player in [players] list,
|
||||
Text _getPlacementTextWidget(int index) {
|
||||
int placement = denseRanks[index];
|
||||
switch (placement) {
|
||||
case 1:
|
||||
return const Text('\u{1F947}', style: TextStyle(fontSize: 22)); // 🥇
|
||||
case 2:
|
||||
return const Text('\u{1F948}', style: TextStyle(fontSize: 22)); // 🥈
|
||||
case 3:
|
||||
return const Text('\u{1F949}', style: TextStyle(fontSize: 22)); // 🥉
|
||||
default:
|
||||
return Text(' $placement.',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold));
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm deleting the game session.
|
||||
Future<bool> _showDeleteGameDialog() 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(gameSession.gameTitle),
|
||||
),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).delete,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.red),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Removes the game session in the game manager and navigates back to the previous screen.
|
||||
/// If the game session does not exist in the game list, it shows an error dialog.
|
||||
Future<void> _removeGameSession(GameSession gameSession) async {
|
||||
if (gameManager.gameExistsInGameList(gameSession.id)) {
|
||||
Navigator.pop(context);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gameManager.removeGameSessionById(gameSession.id);
|
||||
});
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).id_error_title),
|
||||
content: Text(AppLocalizations.of(context).id_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively opens the RoundView for the specified round number.
|
||||
/// It starts with the given [roundNumber] and continues to open the next round
|
||||
/// until the user navigates back or the round number is invalid.
|
||||
void _openRoundView(int roundNumber) async {
|
||||
final val = await Navigator.of(context, rootNavigator: true).push(
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => RoundView(
|
||||
gameSession: gameSession,
|
||||
roundNumber: roundNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (val != null && val >= 0) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
_openRoundView(val);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
416
lib/presentation/views/create_game_view.dart
Normal file
416
lib/presentation/views/create_game_view.dart
Normal file
@@ -0,0 +1,416 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/data/game_session.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/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CreateStatus {
|
||||
noGameTitle,
|
||||
noModeSelected,
|
||||
minPlayers,
|
||||
maxPlayers,
|
||||
noPlayerName,
|
||||
}
|
||||
|
||||
class CreateGameView extends StatefulWidget {
|
||||
final GameMode gameMode;
|
||||
final String? gameTitle;
|
||||
final List<String>? players;
|
||||
|
||||
const CreateGameView({
|
||||
super.key,
|
||||
this.gameTitle,
|
||||
this.players,
|
||||
required this.gameMode,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_CreateGameViewState createState() => _CreateGameViewState();
|
||||
}
|
||||
|
||||
class _CreateGameViewState extends State<CreateGameView> {
|
||||
final List<TextEditingController> _playerNameTextControllers = [
|
||||
TextEditingController()
|
||||
];
|
||||
final TextEditingController _gameTitleTextController =
|
||||
TextEditingController();
|
||||
|
||||
/// Maximum number of players allowed in the game.
|
||||
final int maxPlayers = 5;
|
||||
|
||||
/// Variable to hold the selected game mode.
|
||||
late GameMode gameMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
gameMode = widget.gameMode;
|
||||
|
||||
_gameTitleTextController.text = widget.gameTitle ?? '';
|
||||
|
||||
if (widget.players != null) {
|
||||
_playerNameTextControllers.clear();
|
||||
for (var player in widget.players!) {
|
||||
_playerNameTextControllers.add(TextEditingController(text: player));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: AppLocalizations.of(context).overview,
|
||||
middle: Text(AppLocalizations.of(context).new_game),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
maxLength: 16,
|
||||
prefix: Text(AppLocalizations.of(context).name),
|
||||
textAlign: TextAlign.right,
|
||||
placeholder: AppLocalizations.of(context).game_title,
|
||||
controller: _gameTitleTextController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
readOnly: true,
|
||||
prefix: Text(AppLocalizations.of(context).mode),
|
||||
suffix: Row(
|
||||
children: [
|
||||
Text(
|
||||
gameMode == GameMode.none
|
||||
? AppLocalizations.of(context).no_mode_selected
|
||||
: (gameMode == GameMode.pointLimit
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
const CupertinoListTileChevron(),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final selectedMode = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
gameMode = selectedMode ?? gameMode;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _playerNameTextControllers.length + 2,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < _playerNameTextControllers.length &&
|
||||
newIndex <= _playerNameTextControllers.length) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) newIndex--;
|
||||
final item =
|
||||
_playerNameTextControllers.removeAt(oldIndex);
|
||||
_playerNameTextControllers.insert(newIndex, item);
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
// Create game button
|
||||
if (index == _playerNameTextControllers.length + 1) {
|
||||
return Container(
|
||||
key: const ValueKey('create_game_button'),
|
||||
child: CupertinoButton(
|
||||
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).create_game,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
_checkAllGameAttributes();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// Add player button
|
||||
if (index == _playerNameTextControllers.length) {
|
||||
return Container(
|
||||
key: const ValueKey('add_player_button'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0, horizontal: 10),
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.add_circled_solid,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
AppLocalizations.of(context).add_player,
|
||||
style:
|
||||
TextStyle(color: CustomTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_playerNameTextControllers.length < maxPlayers) {
|
||||
setState(() {
|
||||
_playerNameTextControllers
|
||||
.add(TextEditingController());
|
||||
});
|
||||
} else {
|
||||
showFeedbackDialog(CreateStatus.maxPlayers);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Spieler-Einträge
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circle_fill,
|
||||
color: CupertinoColors.destructiveRed,
|
||||
size: 25,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_playerNameTextControllers[index].dispose();
|
||||
_playerNameTextControllers.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: _playerNameTextControllers[index],
|
||||
maxLength: 12,
|
||||
placeholder:
|
||||
'${AppLocalizations.of(context).player} ${index + 1}',
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).create_game,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_gameTitleTextController.text == '') {
|
||||
showFeedbackDialog(CreateStatus.noGameTitle);
|
||||
return;
|
||||
}
|
||||
if (gameMode == GameMode.none) {
|
||||
showFeedbackDialog(CreateStatus.noModeSelected);
|
||||
return;
|
||||
}
|
||||
if (_playerNameTextControllers.length < 2) {
|
||||
showFeedbackDialog(CreateStatus.minPlayers);
|
||||
return;
|
||||
}
|
||||
if (!everyPlayerHasAName()) {
|
||||
showFeedbackDialog(CreateStatus.noPlayerName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
}
|
||||
|
||||
bool isPointsLimitEnabled = gameMode == GameMode.pointLimit;
|
||||
|
||||
GameSession gameSession = GameSession(
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: players,
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
caboPenalty: ConfigService.getCaboPenalty(),
|
||||
isPointsLimitEnabled: isPointsLimitEnabled,
|
||||
);
|
||||
final index = await gameManager.addGameSession(gameSession);
|
||||
final session = gameManager.gameList[index];
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
ActiveGameView(gameSession: session)));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
))));
|
||||
Future<void> _createGame() async {
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
}
|
||||
GameSession gameSession = GameSession(
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: players,
|
||||
pointLimit: Globals.pointLimit,
|
||||
caboPenalty: Globals.caboPenalty,
|
||||
isPointsLimitEnabled: _isPointsLimitEnabled!,
|
||||
);
|
||||
final index = await gameManager.addGameSession(gameSession);
|
||||
final session = gameManager.gameList[index];
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ActiveGameView(gameSession: session)));
|
||||
}
|
||||
|
||||
/// Displays a feedback dialog based on the [CreateStatus].
|
||||
void showFeedbackDialog(CreateStatus status) {
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the title and message for the dialog based on the [CreateStatus].
|
||||
(String, String) _getDialogContent(CreateStatus status) {
|
||||
switch (status) {
|
||||
case CreateStatus.noGameTitle:
|
||||
return (
|
||||
AppLocalizations.of(context).no_gameTitle_title,
|
||||
AppLocalizations.of(context).no_gameTitle_message
|
||||
);
|
||||
case CreateStatus.noModeSelected:
|
||||
return (
|
||||
AppLocalizations.of(context).no_mode_title,
|
||||
AppLocalizations.of(context).no_mode_message
|
||||
);
|
||||
|
||||
case CreateStatus.minPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).min_players_title,
|
||||
AppLocalizations.of(context).min_players_message
|
||||
);
|
||||
case CreateStatus.maxPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).max_players_title,
|
||||
AppLocalizations.of(context).max_players_message
|
||||
);
|
||||
case CreateStatus.noPlayerName:
|
||||
return (
|
||||
AppLocalizations.of(context).no_name_title,
|
||||
AppLocalizations.of(context).no_name_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if every player has a name.
|
||||
/// Returns true if all players have a name, false otherwise.
|
||||
bool everyPlayerHasAName() {
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
if (controller.text == '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_gameTitleTextController.dispose();
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
132
lib/presentation/views/graph_view.dart
Normal file
132
lib/presentation/views/graph_view.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class GraphView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const GraphView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
/// List of colors for the graph lines.
|
||||
final List<Color> lineColors = [
|
||||
CustomTheme.graphColor1,
|
||||
CustomTheme.graphColor2,
|
||||
CustomTheme.graphColor3,
|
||||
CustomTheme.graphColor4,
|
||||
CustomTheme.graphColor5
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).scoring_history),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: widget.gameSession.roundNumber > 1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: SfCartesianChart(
|
||||
enableAxisAnimation: true,
|
||||
legend: const Legend(
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
isVisible: true,
|
||||
position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
labelAlignment: LabelAlignment.center,
|
||||
labelPosition: ChartDataLabelPosition.inside,
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
axisLabelFormatter: (AxisLabelRenderDetails details) {
|
||||
if (details.value == 0) {
|
||||
return ChartAxisLabel('', const TextStyle());
|
||||
}
|
||||
return ChartAxisLabel(
|
||||
'${details.value.toInt()}', const TextStyle());
|
||||
},
|
||||
),
|
||||
series: getCumulativeScores(),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Center(
|
||||
child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).empty_graph_text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns a list of LineSeries representing the cumulative scores of each player.
|
||||
/// Each series contains data points for each round, showing the cumulative score up to that round.
|
||||
/// The x-axis represents the round number, and the y-axis represents the cumulative score.
|
||||
List<LineSeries<(int, num), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
for (var round in rounds) {
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
runningTotals[i] += round.scoreUpdates[i];
|
||||
cumulativeScores[i].add(runningTotals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const double jitterStep = 0.03;
|
||||
|
||||
/// Create a list of LineSeries for each player
|
||||
/// Each series contains data points for each round
|
||||
return List.generate(playerCount, (i) {
|
||||
final data = List.generate(
|
||||
cumulativeScores[i].length + 1,
|
||||
(j) => (
|
||||
j,
|
||||
j == 0 || cumulativeScores[i][j - 1] == 0
|
||||
? 0 // 0 points at the start of the game or when the value is 0 (don't subtract jitter step)
|
||||
|
||||
// Adds a small jitter to the cumulative scores to prevent overlapping data points in the graph.
|
||||
// The jitter is centered around zero by subtracting playerCount ~/ 2 from the player index i.
|
||||
: cumulativeScores[i][j - 1] + (i - playerCount ~/ 2) * jitterStep
|
||||
),
|
||||
);
|
||||
|
||||
/// Create a LineSeries for the player
|
||||
/// The xValueMapper maps the round number, and the yValueMapper maps the cumulative score.
|
||||
return LineSeries<(int, num), int>(
|
||||
name: playerNames[i],
|
||||
dataSource: data,
|
||||
xValueMapper: (record, _) => record.$1,
|
||||
yValueMapper: (record, _) => record.$2,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: lineColors[i],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
372
lib/presentation/views/main_menu_view.dart
Normal file
372
lib/presentation/views/main_menu_view.dart
Normal file
@@ -0,0 +1,372 @@
|
||||
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: _isLoading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: gameManager.gameList.isEmpty
|
||||
? 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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:
|
||||
session.isGameFinished == true
|
||||
? Text(
|
||||
'\u{1F947} ${session.winner}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14),
|
||||
)
|
||||
: Text(
|
||||
'${AppLocalizations.of(context).mode}: ${_translateGameMode(session.isPointsLimitEnabled)}',
|
||||
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(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
77
lib/presentation/views/mode_selection_view.dart
Normal file
77
lib/presentation/views/mode_selection_view.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
enum GameMode {
|
||||
none,
|
||||
pointLimit,
|
||||
unlimited,
|
||||
}
|
||||
|
||||
class ModeSelectionMenu extends StatelessWidget {
|
||||
final int pointLimit;
|
||||
final bool showDeselection;
|
||||
const ModeSelectionMenu(
|
||||
{super.key, required this.pointLimit, required this.showDeselection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).select_game_mode),
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text('$pointLimit ${AppLocalizations.of(context).points}',
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)
|
||||
.point_limit_description(pointLimit),
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.pointLimit);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).unlimited,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).unlimited_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.unlimited);
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: showDeselection,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).no_default_mode,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).no_default_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.none);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
141
lib/presentation/views/points_view.dart
Normal file
141
lib/presentation/views/points_view.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PointsView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const PointsView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<PointsView> createState() => _PointsViewState();
|
||||
}
|
||||
|
||||
class _PointsViewState extends State<PointsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).point_overview),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: DataTable(
|
||||
dataRowMinHeight: 60,
|
||||
dataRowMaxHeight: 60,
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: 20,
|
||||
columns: [
|
||||
const DataColumn(
|
||||
numeric: true,
|
||||
headingRowAlignment: MainAxisAlignment.center,
|
||||
label: Text(
|
||||
'#',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
columnWidth: IntrinsicColumnWidth(flex: 0.5)),
|
||||
...widget.gameSession.players.map(
|
||||
(player) => DataColumn(
|
||||
label: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Text(
|
||||
player,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
headingRowAlignment: MainAxisAlignment.center,
|
||||
columnWidth: const IntrinsicColumnWidth(flex: 1)),
|
||||
),
|
||||
],
|
||||
rows: [
|
||||
...List<DataRow>.generate(
|
||||
widget.gameSession.roundList.length,
|
||||
(roundIndex) {
|
||||
final round = widget.gameSession.roundList[roundIndex];
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${roundIndex + 1}',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
)),
|
||||
...List.generate(widget.gameSession.players.length,
|
||||
(playerIndex) {
|
||||
final int score = round.scores[playerIndex];
|
||||
final int update = round.scoreUpdates[playerIndex];
|
||||
final bool saidCabo =
|
||||
round.caboPlayerIndex == playerIndex;
|
||||
return DataCell(
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: update <= 0
|
||||
? CustomTheme.pointLossColor
|
||||
: CustomTheme.pointGainColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${update >= 0 ? '+' : ''}$update',
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('$score',
|
||||
style: TextStyle(
|
||||
fontWeight: saidCabo
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
DataRow(
|
||||
cells: [
|
||||
const DataCell(Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Σ',
|
||||
style:
|
||||
TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
),
|
||||
)),
|
||||
...widget.gameSession.playerScores.map(
|
||||
(score) => DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'$score',
|
||||
style: const TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
534
lib/presentation/views/round_view.dart
Normal file
534
lib/presentation/views/round_view.dart
Normal file
@@ -0,0 +1,534 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class RoundView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
final int roundNumber;
|
||||
const RoundView(
|
||||
{super.key, required this.roundNumber, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_RoundViewState createState() => _RoundViewState();
|
||||
}
|
||||
|
||||
class _RoundViewState extends State<RoundView> {
|
||||
/// The current game session.
|
||||
late GameSession gameSession = widget.gameSession;
|
||||
|
||||
/// Index of the player who said CABO.
|
||||
int _caboPlayerIndex = 0;
|
||||
|
||||
/// Index of the player who has Kamikaze.
|
||||
/// Default is null (no Kamikaze player).
|
||||
int? _kamikazePlayerIndex;
|
||||
|
||||
/// List of text controllers for the score text fields.
|
||||
late final List<TextEditingController> _scoreControllerList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => TextEditingController(),
|
||||
);
|
||||
|
||||
/// List of focus nodes for the score text fields.
|
||||
late final List<FocusNode> _focusNodeList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => FocusNode(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print('=== Runde ${widget.roundNumber} geöffnet ===');
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished == true) {
|
||||
print(
|
||||
'Diese wurde bereits gespielt, deshalb werden die alten Punktestaende angezeigt');
|
||||
|
||||
// If the current round has already been played, the text fields
|
||||
// are filled with the scores from this round
|
||||
for (int i = 0; i < _scoreControllerList.length; i++) {
|
||||
_scoreControllerList[i].text =
|
||||
gameSession.roundList[widget.roundNumber - 1].scores[i].toString();
|
||||
}
|
||||
_caboPlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].caboPlayerIndex;
|
||||
_kamikazePlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].kamikazePlayerIndex;
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final rotatedPlayers = _getRotatedPlayers();
|
||||
final originalIndices = _getOriginalIndices();
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: true,
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
trailing: Visibility(
|
||||
visible: widget.gameSession.isGameFinished,
|
||||
child: const Icon(
|
||||
CupertinoIcons.lock,
|
||||
size: 25,
|
||||
))),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 100 + bottomInset),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).round} ${widget.roundNumber}',
|
||||
style: CustomTheme.roundTitle),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppLocalizations.of(context).who_said_cabo,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.gameSession.players.length > 3 ? 5 : 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
unselectedColor:
|
||||
CustomTheme.mainElementBackgroundColor,
|
||||
selectedColor: CustomTheme.primaryColor,
|
||||
groupValue: _caboPlayerIndex,
|
||||
children: Map.fromEntries(widget.gameSession.players
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 8,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_caboPlayerIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: rotatedPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final originalIndex = originalIndices[index];
|
||||
final name = rotatedPlayers[index];
|
||||
bool shouldShowMedal =
|
||||
index == 0 && widget.roundNumber > 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 20),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CustomTheme.playerTileColor,
|
||||
title: Row(children: [
|
||||
Expanded(
|
||||
child: Row(children: [
|
||||
Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const SizedBox(width: 10),
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const Icon(FontAwesomeIcons.crown,
|
||||
size: 15))
|
||||
]))
|
||||
]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[originalIndex]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: SizedBox(
|
||||
width: 100,
|
||||
child: CupertinoTextField(
|
||||
maxLength: 3,
|
||||
focusNode: _focusNodeList[originalIndex],
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
signed: true,
|
||||
decimal: false,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textInputAction: index ==
|
||||
widget.gameSession.players.length - 1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.next,
|
||||
controller:
|
||||
_scoreControllerList[originalIndex],
|
||||
placeholder:
|
||||
AppLocalizations.of(context).points,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) =>
|
||||
_focusNextTextfield(originalIndex),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Center(
|
||||
heightFactor: 1,
|
||||
child: CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CustomTheme.buttonBackgroundColor,
|
||||
onPressed: () async {
|
||||
if (await _showKamikazeSheet(context)) {
|
||||
if (!context.mounted) return;
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).kamikaze,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomInset,
|
||||
child: KeyboardVisibilityBuilder(builder: (context, visible) {
|
||||
if (!visible) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
color: CustomTheme.mainElementBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, false);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
if (!widget.gameSession.isGameFinished)
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the index of the player who won the previous round.
|
||||
int _getPreviousRoundWinnerIndex() {
|
||||
if (widget.roundNumber == 1) {
|
||||
return 0; // If it's the first round, there's no previous round, so return 0.
|
||||
}
|
||||
|
||||
final previousRound = widget.gameSession.roundList[widget.roundNumber - 2];
|
||||
final scores = previousRound.scoreUpdates;
|
||||
|
||||
// Find the index of the player with the minimum score
|
||||
int minScore = scores[0];
|
||||
int winnerIndex = 0;
|
||||
|
||||
// Iterate through the scores to find the player with the minimum score
|
||||
for (int i = 1; i < scores.length; i++) {
|
||||
if (scores[i] < minScore) {
|
||||
minScore = scores[i];
|
||||
winnerIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return winnerIndex;
|
||||
}
|
||||
|
||||
/// Rotates the players list based on the previous round's winner.
|
||||
List<String> _getRotatedPlayers() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
widget.gameSession.players[winnerIndex],
|
||||
...widget.gameSession.players.sublist(winnerIndex + 1),
|
||||
...widget.gameSession.players.sublist(0, winnerIndex)
|
||||
];
|
||||
}
|
||||
|
||||
/// Gets the original indices of the players by recalculating it from the rotated list.
|
||||
List<int> _getOriginalIndices() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
winnerIndex,
|
||||
...List.generate(widget.gameSession.players.length - winnerIndex - 1,
|
||||
(i) => winnerIndex + i + 1),
|
||||
...List.generate(winnerIndex, (i) => i)
|
||||
];
|
||||
}
|
||||
|
||||
/// Shows a Cupertino action sheet to select the player who has Kamikaze.
|
||||
/// It returns true if a player was selected, false if the action was cancelled.
|
||||
Future<bool> _showKamikazeSheet(BuildContext context) async {
|
||||
return await showCupertinoModalPopup<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(AppLocalizations.of(context).kamikaze),
|
||||
message: Text(AppLocalizations.of(context).who_has_kamikaze),
|
||||
actions: widget.gameSession.players.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
_kamikazePlayerIndex = index;
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(name),
|
||||
);
|
||||
}).toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
isDestructiveAction: true,
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Focuses the next text field in the list of text fields.
|
||||
/// [index] is the index of the current text field.
|
||||
void _focusNextTextfield(int index) {
|
||||
final originalIndices = _getOriginalIndices();
|
||||
final currentPos = originalIndices.indexOf(index);
|
||||
|
||||
if (currentPos < originalIndices.length - 1) {
|
||||
FocusScope.of(context)
|
||||
.requestFocus(_focusNodeList[originalIndices[currentPos + 1]]);
|
||||
} else {
|
||||
_focusNodeList[index].unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the inputs for the round are valid.
|
||||
/// Returns true if the inputs are valid, false otherwise.
|
||||
/// Round Inputs are valid if every player has a score or
|
||||
/// kamikaze is selected for a player
|
||||
bool _areRoundInputsValid() {
|
||||
if (_areTextFieldsEmpty() && _kamikazePlayerIndex == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if any of the text fields for the players points are empty.
|
||||
/// Returns true if any of the text fields is empty, false otherwise.
|
||||
bool _areTextFieldsEmpty() {
|
||||
for (TextEditingController t in _scoreControllerList) {
|
||||
if (t.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Finishes the current round.
|
||||
/// It first determines, ifCalls the [_calculateScoredPoints()] method to calculate the points for
|
||||
/// every player. If the round is the highest round played in this game,
|
||||
/// it expands the player score lists. At the end it updates the score
|
||||
/// array for the game.
|
||||
List<int> _finishRound() {
|
||||
print('====================================');
|
||||
print('Runde ${widget.roundNumber} beendet');
|
||||
// The shown round is smaller than the newest round
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber) {
|
||||
print('Da diese Runde bereits gespielt wurde, werden die alten '
|
||||
'Punktestaende ueberschrieben');
|
||||
}
|
||||
if (_kamikazePlayerIndex != null) {
|
||||
print('${widget.gameSession.players[_kamikazePlayerIndex!]} hat Kamikaze '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen 50 Punkte');
|
||||
widget.gameSession
|
||||
.applyKamikaze(widget.roundNumber, _kamikazePlayerIndex!);
|
||||
} else {
|
||||
List<int> roundScores = [];
|
||||
for (TextEditingController c in _scoreControllerList) {
|
||||
if (c.text.isNotEmpty) roundScores.add(int.parse(c.text));
|
||||
}
|
||||
widget.gameSession.calculateScoredPoints(
|
||||
widget.roundNumber, roundScores, _caboPlayerIndex);
|
||||
}
|
||||
List<int> bonusPlayers = widget.gameSession.updatePoints();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
print('Das Spiel ist beendet');
|
||||
} else if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
/// Shows a popup dialog with the information which player received the bonus points.
|
||||
Future<void> _showBonusPopup(
|
||||
BuildContext context, List<int> bonusPlayers) async {
|
||||
int pointLimit = widget.gameSession.pointLimit;
|
||||
int bonusPoints = (pointLimit / 2).round();
|
||||
|
||||
String resultText =
|
||||
_getBonusPopupMessageString(pointLimit, bonusPoints, bonusPlayers);
|
||||
|
||||
await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).bonus_points_title),
|
||||
content: Text(resultText),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates the message string for the bonus popup.
|
||||
/// It takes the [pointLimit], [bonusPoints] and the list of [bonusPlayers]
|
||||
/// and returns a formatted string.
|
||||
String _getBonusPopupMessageString(
|
||||
int pointLimit, int bonusPoints, List<int> bonusPlayers) {
|
||||
List<String> nameList =
|
||||
bonusPlayers.map((i) => widget.gameSession.players[i]).toList();
|
||||
String resultText = '';
|
||||
if (nameList.length == 1) {
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length, nameList.first, pointLimit, bonusPoints);
|
||||
} else {
|
||||
resultText = nameList.length == 2
|
||||
? '${nameList[0]} & ${nameList[1]}'
|
||||
: '${nameList.sublist(0, nameList.length - 1).join(', ')} & ${nameList.last}';
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length,
|
||||
resultText,
|
||||
pointLimit,
|
||||
bonusPoints,
|
||||
);
|
||||
}
|
||||
return resultText;
|
||||
}
|
||||
|
||||
/// Handles the navigation for the end of the round.
|
||||
/// It checks for bonus players and shows a popup, saves the game session,
|
||||
/// and navigates to the next round or back to the previous screen.
|
||||
/// It takes the BuildContext [context] and a boolean [navigateToNextRound] to determine
|
||||
/// if it should navigate to the next round or not.
|
||||
Future<void> _endOfRoundNavigation(
|
||||
BuildContext context, bool navigateToNextRound) async {
|
||||
List<int> bonusPlayersIndices = _finishRound();
|
||||
if (bonusPlayersIndices.isNotEmpty) {
|
||||
await _showBonusPopup(context, bonusPlayersIndices);
|
||||
}
|
||||
|
||||
LocalStorageService.saveGameSessions();
|
||||
|
||||
if (context.mounted) {
|
||||
// If the game is finished, pop the context and return to the previous screen.
|
||||
if (widget.gameSession.isGameFinished) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is false, pop the context and return to the previous screen.
|
||||
if (!navigateToNextRound) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is true and the game isn't finished yet,
|
||||
// pop the context and navigate to the next round.
|
||||
Navigator.pop(context, widget.roundNumber + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _scoreControllerList) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final focusNode in _focusNodeList) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
304
lib/presentation/views/settings_view.dart
Normal file
304
lib/presentation/views/settings_view.dart
Normal file
@@ -0,0 +1,304 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_form_row.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_stepper.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
UniqueKey _stepperKey1 = UniqueKey();
|
||||
UniqueKey _stepperKey2 = UniqueKey();
|
||||
GameMode defaultMode = ConfigService.getGameMode();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).settings),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).cabo_penalty,
|
||||
prefixIcon: CupertinoIcons.bolt_fill,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey1,
|
||||
initialValue: ConfigService.getCaboPenalty(),
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).point_limit,
|
||||
prefixIcon: FontAwesomeIcons.bullseye,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: ConfigService.getPointLimit(),
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).standard_mode,
|
||||
prefixIcon: CupertinoIcons.square_stack,
|
||||
suffixWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
defaultMode == GameMode.none
|
||||
? AppLocalizations.of(context).no_default_mode
|
||||
: (defaultMode == GameMode.pointLimit
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
const CupertinoListTileChevron()
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final selectedMode = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
defaultMode = selectedMode ?? GameMode.none;
|
||||
});
|
||||
ConfigService.setGameMode(defaultMode);
|
||||
},
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText:
|
||||
AppLocalizations.of(context).reset_to_default,
|
||||
prefixIcon: CupertinoIcons.arrow_counterclockwise,
|
||||
onPressed: () {
|
||||
ConfigService.resetConfig();
|
||||
setState(() {
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
defaultMode = ConfigService.getGameMode();
|
||||
});
|
||||
},
|
||||
)
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game_data,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).import_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_down,
|
||||
onPressed: () async {
|
||||
final status =
|
||||
await LocalStorageService.importJsonFile();
|
||||
showFeedbackDialog(status);
|
||||
},
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).export_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_up,
|
||||
onPressed: () => LocalStorageService.exportGameData(),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).delete_data,
|
||||
prefixIcon: CupertinoIcons.trash,
|
||||
onPressed: () => _deleteAllGames(),
|
||||
),
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).wiki,
|
||||
prefixIcon: CupertinoIcons.book,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubWikiLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).privacy_policy,
|
||||
prefixIcon: CupertinoIcons.doc_append,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kPrivacyPolicyLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).error_found,
|
||||
prefixIcon: FontAwesomeIcons.github,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubIssuesLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).app_version,
|
||||
prefixIcon: CupertinoIcons.tag,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getVersion(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).build,
|
||||
prefixIcon: CupertinoIcons.number,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getBuildNumber(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
])),
|
||||
const SizedBox(height: 50)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm the deletion of all game data.
|
||||
/// When confirmed, it deletes all game data from local storage.
|
||||
void _deleteAllGames() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_data_title),
|
||||
content: Text(AppLocalizations.of(context).delete_data_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
onPressed: () {
|
||||
LocalStorageService.deleteAllGames();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showFeedbackDialog(ImportStatus status) {
|
||||
if (status == ImportStatus.canceled) return;
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
(String, String) _getDialogContent(ImportStatus status) {
|
||||
switch (status) {
|
||||
case ImportStatus.success:
|
||||
return (
|
||||
AppLocalizations.of(context).import_success_title,
|
||||
AppLocalizations.of(context).import_success_message
|
||||
);
|
||||
case ImportStatus.validationError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_validation_error_title,
|
||||
AppLocalizations.of(context).import_validation_error_message
|
||||
);
|
||||
|
||||
case ImportStatus.formatError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_format_error_title,
|
||||
AppLocalizations.of(context).import_format_error_message
|
||||
);
|
||||
case ImportStatus.genericError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_generic_error_title,
|
||||
AppLocalizations.of(context).import_generic_error_message
|
||||
);
|
||||
case ImportStatus.canceled:
|
||||
return ('', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/presentation/views/tab_view.dart
Normal file
48
lib/presentation/views/tab_view.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/about_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/main_menu_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class TabView extends StatefulWidget {
|
||||
const TabView({super.key});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_TabViewState createState() => _TabViewState();
|
||||
}
|
||||
|
||||
class _TabViewState extends State<TabView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
backgroundColor: CustomTheme.mainElementBackgroundColor,
|
||||
iconSize: 27,
|
||||
height: 55,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
CupertinoIcons.house_fill,
|
||||
),
|
||||
label: AppLocalizations.of(context).home,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
CupertinoIcons.info,
|
||||
),
|
||||
label: AppLocalizations.of(context).about,
|
||||
),
|
||||
]),
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CupertinoTabView(builder: (BuildContext context) {
|
||||
if (index == 0) {
|
||||
return const MainMenuView();
|
||||
} else {
|
||||
return const AboutView();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user