From 4c5ba92b066560e9de76ac14a491d789453617d8 Mon Sep 17 00:00:00 2001 From: Connor Frank Date: Mon, 1 Apr 2024 17:43:00 -0400 Subject: Initial Commit --- lib/game_data.dart | 28 ++++ lib/main.dart | 18 +++ lib/main_menu.dart | 95 +++++++++++ lib/scorekeeper.dart | 441 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 582 insertions(+) create mode 100644 lib/game_data.dart create mode 100644 lib/main.dart create mode 100644 lib/main_menu.dart create mode 100644 lib/scorekeeper.dart (limited to 'lib') diff --git a/lib/game_data.dart b/lib/game_data.dart new file mode 100644 index 0000000..3c7c77b --- /dev/null +++ b/lib/game_data.dart @@ -0,0 +1,28 @@ +class GameData { + List playerNames; + Map pars; + Map> scores; + DateTime date; + + GameData({ + this.playerNames = const [], + this.pars = const {}, + this.scores = const {}, + required this.date, + }); + + Map toJson() => { + 'playerNames': playerNames, + 'pars': pars.map((key, value) => MapEntry(key.toString(), value)), + 'scores': scores.map((key, value) => MapEntry(key.toString(), value)), + 'date': date.toIso8601String(), + // No need to serialize 'winner' as it's derived from scores + }; + + factory GameData.fromJson(Map json) => GameData( + playerNames: List.from(json['playerNames']), + pars: Map.from(json['pars'].map((key, value) => MapEntry(int.parse(key), value))), + scores: Map>.from(json['scores'].map((key, value) => MapEntry(int.parse(key), List.from(value)))), + date: DateTime.parse(json['date']), + ); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..0d6a66c --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:mini_golf/main_menu.dart'; + +void main() { + runApp(const MiniGolfScoreApp()); +} + +class MiniGolfScoreApp extends StatelessWidget { + const MiniGolfScoreApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Mini Golf Score App', + home: MainMenu(), + ); + } +} diff --git a/lib/main_menu.dart b/lib/main_menu.dart new file mode 100644 index 0000000..f9fa267 --- /dev/null +++ b/lib/main_menu.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:mini_golf/scorekeeper.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class MainMenu extends StatelessWidget { + const MainMenu({super.key}); + + void _showLoadGameDialog(BuildContext context) async { + final prefs = await SharedPreferences.getInstance(); + final gameKeys = prefs.getKeys().where((key) => key.startsWith('game_')).toList(); + + if (gameKeys.isEmpty) { + // No games found, show a SnackBar instead + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No saved games found.')), + ); + return; + } + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Select a Game to Load'), + content: SingleChildScrollView( + child: ListBody( + children: gameKeys.map((key) { + return ListTile( + title: Text(key.substring(5).substring(0,16).replaceAll("T", " ")), + onTap: () { + _loadGame(context, key); + }, + ); + }).toList(), + ), + ), + ); + }, + ); + } + + Future _loadGame(BuildContext context, String gameKey) async { + final prefs = await SharedPreferences.getInstance(); + final gameDataJson = prefs.getString(gameKey); + if (gameDataJson != null) { + final gameData = jsonDecode(gameDataJson); + // Assuming gameData structure. Convert data types as necessary. + Navigator.of(context).pop(); // Close the selection dialog + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ScoreKeeper( + isNewGame: false, + playerNames: List.from(gameData['playerNames']), + pars: Map.from(gameData['pars']).map((k, v) => MapEntry(int.parse(k), v)), + scores: Map.from(gameData['scores']).map((k, v) => MapEntry(int.parse(k), List.from(v))), + gameCreationTime: DateTime.parse(gameData['creationTime']), + ), + ), + ); + } + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Mini Golf Main Menu'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + child: const Text('Start New Game'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ScoreKeeper(isNewGame: true)), + ); + }, + ), + const SizedBox(height: 16), // Space between the buttons + ElevatedButton( + child: const Text('Load Game'), + onPressed: () => _showLoadGameDialog(context), + ), + ], + ), + ), + ); + } +} diff --git a/lib/scorekeeper.dart b/lib/scorekeeper.dart new file mode 100644 index 0000000..57cd1ed --- /dev/null +++ b/lib/scorekeeper.dart @@ -0,0 +1,441 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class ScoreKeeper extends StatefulWidget { + final bool isNewGame; + final List? playerNames; + final Map? pars; + final Map>? scores; + final DateTime? gameCreationTime; + + const ScoreKeeper({ + super.key, + this.isNewGame = false, + this.playerNames, + this.pars, + this.scores, + this.gameCreationTime, + }); + + @override + _ScoreKeeperState createState() => _ScoreKeeperState(); +} + +class _ScoreKeeperState extends State { + late List playerNames; + late Map pars; + late Map> scores; + late DateTime gameCreationTime; + late int _numberOfHoles; + + @override + void initState() { + super.initState(); + if (widget.isNewGame) { + _numberOfHoles = 1; + playerNames = []; + pars = {1: 0}; + scores = { + 1: List.generate(playerNames.length, (_) => 0, growable: true), + }; + gameCreationTime = DateTime.now(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _askForPlayerNames(); + }); + } + else { + // Use loaded data or initialize with default values + playerNames = widget.playerNames ?? []; + pars = widget.pars ?? {1: 0}; + scores = widget.scores ?? { + 1: List.generate(playerNames.length, (_) => 0, growable: true), + }; + gameCreationTime = widget.gameCreationTime ?? DateTime.now(); + _numberOfHoles = scores.length; + } + } + + Future _askForPlayerNames() async { + TextEditingController controller = TextEditingController(); + return showDialog( + context: context, + barrierDismissible: false, // User must tap a button to close the dialog + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Enter Player Names'), + content: SingleChildScrollView( + child: ListBody( + children: [ + const Text('Please enter player names, separated by commas.'), + TextField( + controller: controller, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Submit'), + onPressed: () { + // Splitting the input text by commas to get individual names + setState(() { + playerNames = controller.text + .split(',') + .map((name) => name.trim()) + .toList(); + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + Future _saveGame() async { + final prefs = await SharedPreferences.getInstance(); + final String timestamp = gameCreationTime.toIso8601String(); + final String gameKey = 'game_$timestamp'; // Unique key for each game + + // Convert maps with int keys to maps with String keys for JSON encoding + final Map parsAsStringKeys = pars.map((k, v) => MapEntry(k.toString(), v)); + final Map> scoresAsStringKeys = scores.map((k, v) => MapEntry(k.toString(), v)); + + final Map gameData = { + 'playerNames': playerNames, + 'pars': parsAsStringKeys, + 'scores': scoresAsStringKeys, + 'creationTime': timestamp, + }; + + await prefs.setString(gameKey, jsonEncode(gameData)); + } + + void _confirmDeleteGame(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Delete Game"), + content: const Text("Are you sure you want to delete this game? This action cannot be undone."), + actions: [ + TextButton( + child: const Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + TextButton( + child: const Text("Delete"), + onPressed: () { + _deleteGame(); + Navigator.of(context).pop(); // Close the dialog + Navigator.of(context).pop(); // Return to the main menu + }, + ), + ], + ); + }, + ); + } + + Future _deleteGame() async { + final prefs = await SharedPreferences.getInstance(); + final String timestamp = gameCreationTime.toIso8601String(); + final String gameKey = 'game_$timestamp'; // Unique key for each game + await prefs.remove(gameKey); // Assuming game data is saved with this key + } + + void _showHoleDetailsDialog(int holeNumber) { + // Initialize text editing controllers for par and scores + TextEditingController parController = + TextEditingController(text: pars[holeNumber]?.toString()); + Map scoreControllers = {}; + for (var playerName in playerNames) { + int playerIndex = playerNames.indexOf(playerName); + scoreControllers[playerName] = TextEditingController( + text: scores[holeNumber] != null && + scores[holeNumber]!.length > playerIndex + ? scores[holeNumber]![playerIndex].toString() + : ''); + } + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Details for Hole $holeNumber'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: parController, + decoration: const InputDecoration(labelText: "Par"), + keyboardType: TextInputType.number, + ), + ...playerNames.map((name) => TextField( + controller: scoreControllers[name]!, + decoration: InputDecoration(labelText: "$name's Score"), + keyboardType: TextInputType.number, + )), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Save'), + onPressed: () { + // Save the entered par and scores + setState(() { + pars[holeNumber] = int.tryParse(parController.text) ?? 0; + scores[holeNumber] = playerNames + .map((name) => + int.tryParse(scoreControllers[name]!.text) ?? 0) + .toList(); + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + Future _editPlayerNames() async { + TextEditingController nameController = TextEditingController( + text: playerNames.join(', '), + ); + + await showDialog( + context: context, + barrierDismissible: false, // User must tap a button to close the dialog + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Edit Player Names'), + content: TextField( + controller: nameController, + decoration: const InputDecoration(hintText: 'Player 1, Player 2'), + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + final List newNames = nameController.text + .split(',') + .map((name) => name.trim()) + .toList(); + setState(() { + // Detect new players and add them + for (String newName in newNames) { + if (!playerNames.contains(newName)) { + playerNames.add(newName); // Add new player name + // Add a zero score for the new player in each hole + scores.forEach((hole, playerScores) { + // Ensure the scores list for each hole is growable + List growableScores = List.from(playerScores); + growableScores.add( + 0); // Initialize with zero score for new player + scores[hole] = + growableScores; // Update with the modified scores list + }); + } + } + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + void _deleteLastHole() { + if (scores.isEmpty) return; // No holes to delete + print(_numberOfHoles); + + final lastHoleScores = scores[_numberOfHoles - 1]; + final isAllZeros = + lastHoleScores != null && lastHoleScores.every((score) => score == 0); + + if (isAllZeros) { + // If all scores are zeros, delete the last hole without confirmation + setState(() { + _numberOfHoles--; + pars.remove(_numberOfHoles + 1); + scores.remove(_numberOfHoles + 1); + }); + } else { + // If any score is not zero, ask for confirmation + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Last Hole?'), + content: const Text( + 'This hole has scores. Are you sure you want to delete it?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: const Text('Delete'), + onPressed: () { + setState(() { + _numberOfHoles--; + pars.remove(_numberOfHoles + 1); + scores.remove(_numberOfHoles + 1); + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + } + + Widget _buildHoleDetails(int holeNumber) { + String scoreText; + + if (scores[holeNumber] != null) { + scoreText = 'Par: ${pars[holeNumber]}'; + List scoreDetails = []; + for (int i = 0; i < playerNames.length; i++) { + final playerName = playerNames[i]; + final playerScore = scores[holeNumber]!.length > i + ? scores[holeNumber]![i].toString() + : 'N/A'; + scoreDetails.add('$playerName: $playerScore'); + } + scoreText += ' ${scoreDetails.join(' ')}'; + } else { + scoreText = ' Tap to add scores'; + } + + return ListTile( + title: Text('Hole $holeNumber'), + subtitle: Text(scoreText), + onTap: () => _showHoleDetailsDialog(holeNumber), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Mini Golf Score Keeper'), + actions: [ + IconButton( + icon: const Icon(Icons.save), + onPressed: _saveGame, + tooltip: 'Save Game', + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _confirmDeleteGame(context), + tooltip: 'Delete Game', + ), + ], + ), + body: ListView.builder( + itemCount: _numberOfHoles, + itemBuilder: (context, index) { + int holeNumber = index + 1; + return _buildHoleDetails(holeNumber); + }, + ), + floatingActionButton: Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + // Adjust padding as needed + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: "deleteHole", + onPressed: _deleteLastHole, + tooltip: 'Delete Last Hole', + backgroundColor: Colors.red, + child: const Icon( + Icons.remove), // Optional: different color for delete + ), + const SizedBox(height: 16), // Space between the buttons + FloatingActionButton( + heroTag: "addHole", + onPressed: () { + setState(() { + _numberOfHoles++; // Increment the number of holes + // Initialize par and scores for the new hole with defaults + pars[_numberOfHoles] = 0; // Assuming 0 as default par + scores[_numberOfHoles] = List.filled(playerNames.length, 0); // Initialize scores with 0 + }); + }, + tooltip: 'Add New Hole', + backgroundColor: Colors.green, + child: const Icon(Icons.add), + ), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: Colors.blueGrey[100], + height: 60.0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Total Par Display + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + 'Total Par: ${pars.values.fold(0, (prev, par) => prev + par)}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + // Divider between Total Par and Players' Scores + const VerticalDivider(color: Colors.black), + // Players' Scores + ...playerNames.map((name) { + // Calculate total score for each player + int totalScore = + scores.values.fold(0, (previousValue, holeScores) { + final index = playerNames.indexOf(name); + return previousValue + + (holeScores.length > index ? holeScores[index] : 0); + }); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(name), + Text('Score: $totalScore'), + ], + ), + ); + }), + // Add an IconButton for editing player names + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + _editPlayerNames(); + }, + tooltip: 'Edit Player Names', + ), + ], + ), + ), + ), + ); + } +} -- cgit v1.2.3