Merge pull request #172 from flixcoo/develop
Some checks failed
Some checks failed
Beta-Version 0.6.2
This commit is contained in:
40
.github/workflows/pull_request.yml
vendored
Normal file
40
.github/workflows/pull_request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Pull Request Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Check Formatting
|
||||
run: flutter analyze
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: false
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run Tests
|
||||
run: flutter test
|
||||
@@ -1,38 +1,81 @@
|
||||
name: Flutter
|
||||
name: Push Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "main"
|
||||
pull_request:
|
||||
- "develop"
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: macos-latest
|
||||
generate_licenses:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v1.5.3
|
||||
with:
|
||||
flutter-version: '3.32.1'
|
||||
channel: "stable"
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate oss_licenses.dart
|
||||
run: flutter pub run flutter_oss_licenses:generate -o lib/presentation/views/about/licenses/oss_licenses.dart
|
||||
|
||||
- name: Escape dollar signs in licenses
|
||||
run: |
|
||||
sed -i 's/\$/\\$/g' lib/presentation/views/about/licenses/oss_licenses.dart
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit Changes
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
git add .
|
||||
git commit -m "Actions: Licenses updated [skip ci]"
|
||||
git push
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: generate_licenses
|
||||
if: always()
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Check Formatting
|
||||
run: flutter analyze
|
||||
|
||||
format:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
if: ${{ failure() && needs.lint.result == 'failure' && github.event_name == 'push' && github.ref == 'refs/heads/develop'}}
|
||||
if: ${{ failure() && needs.lint.result == 'failure'}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Get & upgrade dependencies
|
||||
@@ -64,8 +107,8 @@ jobs:
|
||||
git push
|
||||
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
if: false
|
||||
needs: [lint, format]
|
||||
|
||||
steps:
|
||||
@@ -74,15 +117,18 @@ jobs:
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run Tests
|
||||
run: flutter test
|
||||
|
||||
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: false # skips job
|
||||
needs: [lint, format, test]
|
||||
|
||||
@@ -92,10 +138,10 @@ jobs:
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
flutter-version: '3.32.1'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Analyze project source
|
||||
@@ -15,11 +15,32 @@
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"playerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"gameId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalScore": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"playerId",
|
||||
"gameId",
|
||||
"name",
|
||||
"position",
|
||||
"totalScore"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -41,192 +62,9 @@
|
||||
"roundNumber": {
|
||||
"type": "integer"
|
||||
},
|
||||
"playerScores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"roundList": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -284,7 +122,6 @@
|
||||
"isGameFinished",
|
||||
"winner",
|
||||
"roundNumber",
|
||||
"playerScores",
|
||||
"roundList"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,15 +16,41 @@
|
||||
},
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
"items": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"playerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"gameId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalScore": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"playerId",
|
||||
"gameId",
|
||||
"name",
|
||||
"position",
|
||||
"totalScore"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "number"
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPenalty": {
|
||||
"type": "number"
|
||||
"type": "integer"
|
||||
},
|
||||
"isPointsLimitEnabled": {
|
||||
"type": "boolean"
|
||||
@@ -36,51 +62,59 @@
|
||||
"type": "string"
|
||||
},
|
||||
"roundNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"playerScores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
"type": "integer"
|
||||
},
|
||||
"roundList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "number"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "number"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": ["number", "null"]
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
"items": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
}
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"gameTitle",
|
||||
"players",
|
||||
@@ -90,7 +124,6 @@
|
||||
"isGameFinished",
|
||||
"winner",
|
||||
"roundNumber",
|
||||
"playerScores",
|
||||
"roundList"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
import 'package:rate_my_app/rate_my_app.dart';
|
||||
|
||||
/// A utility class that holds constant values and configuration settings
|
||||
/// used throughout the application, such as external links, email addresses,
|
||||
/// and timing parameters for UI elements.
|
||||
///
|
||||
/// This class also provides an instance of [RateMyApp] for managing
|
||||
/// in-app rating prompts.
|
||||
class Constants {
|
||||
/// Indicates the current development phase of the app
|
||||
static const String appDevPhase = 'Beta';
|
||||
|
||||
/// Links to various social media profiles and resources related to the app.
|
||||
/// URL to my Instagram profile
|
||||
static const String kInstagramLink = 'https://instagram.felixkirchner.de';
|
||||
static const String kGithubLink = 'https://github.felixkirchner.de';
|
||||
static const String kGithubIssuesLink =
|
||||
'https://cabocounter-issues.felixkirchner.de';
|
||||
static const String kGithubWikiLink =
|
||||
'https://cabocounter-wiki.felixkirchner.de';
|
||||
static const String kEmail = 'cabocounter@felixkirchner.de';
|
||||
static const String kPrivacyPolicyLink =
|
||||
'https://www.privacypolicies.com/live/1b3759d4-b2f1-4511-8e3b-21bb1626be68';
|
||||
static const String kImprintLink = 'https://imprint.felixkirchner.de';
|
||||
|
||||
/// URL to my GitHub profile
|
||||
static const String kGithubLink = 'https://github.felixkirchner.de';
|
||||
|
||||
/// URL to the GitHub issues page for reporting bugs or requesting features.
|
||||
static const String kGithubIssuesLink =
|
||||
'https://cabo-counter-issues.felixkirchner.de';
|
||||
|
||||
/// URL to the GitHub wiki for additional documentation and guides.
|
||||
static const String kGithubWikiLink =
|
||||
'https://cabo-counter-wiki.felixkirchner.de';
|
||||
|
||||
/// Official email address for user inquiries and support.
|
||||
static const String kEmail = 'cabocounter@felixkirchner.de';
|
||||
|
||||
/// URL to the app's privacy policy page.
|
||||
static const String kPrivacyPolicyLink =
|
||||
'https://cabo-counter-datenschutz.felixkirchner.de';
|
||||
|
||||
/// URL to the app's imprint page, containing legal information.
|
||||
static const String kLegalLink = 'https://impressum.felixkirchner.de';
|
||||
|
||||
/// Instance of [RateMyApp] configured to prompt users for app store ratings.
|
||||
static RateMyApp rateMyApp = RateMyApp(
|
||||
appStoreIdentifier: '6747105718',
|
||||
minDays: 15,
|
||||
@@ -32,4 +54,7 @@ class Constants {
|
||||
|
||||
/// Duration in milliseconds for the keyboard to fully disappear.
|
||||
static const int kKeyboardDelay = 300;
|
||||
|
||||
/// Minimum duration in milliseconds that the skeleton screen should be displayed.
|
||||
static const int kMinimumSkeletonScreenDuration = 500;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CustomTheme {
|
||||
/// Main Theme of the App
|
||||
/// Primary white color mainly used for text
|
||||
static Color white = CupertinoColors.white;
|
||||
|
||||
/// Red color, typically used for destructive actions or error states
|
||||
static Color red = CupertinoColors.destructiveRed;
|
||||
|
||||
/// Primary color of the app, used for buttons, highlights, and interactive elements
|
||||
static Color primaryColor = CupertinoColors.systemGreen;
|
||||
|
||||
/// Background color for the main app scaffold and views
|
||||
static Color backgroundColor = const Color(0xFF101010);
|
||||
|
||||
/// Background color for main UI elements like cards or containers.
|
||||
static Color mainElementBackgroundColor = CupertinoColors.darkBackgroundGray;
|
||||
|
||||
/// Background color for player tiles in lists.
|
||||
static Color playerTileColor = CupertinoColors.secondaryLabel;
|
||||
|
||||
/// Background color for buttons and interactive controls.
|
||||
static Color buttonBackgroundColor = const Color(0xFF202020);
|
||||
|
||||
/// Color used to highlight the kamikaze button and players
|
||||
static Color kamikazeColor = CupertinoColors.systemYellow;
|
||||
|
||||
// Line Colors for GraphView
|
||||
@@ -18,25 +34,33 @@ class CustomTheme {
|
||||
static final Color graphColor5 = primaryColor;
|
||||
|
||||
// Colors for PointsView
|
||||
/// Color used to indicate a loss of points in the UI.
|
||||
static Color pointLossColor = primaryColor;
|
||||
|
||||
/// Color used to indicate a gain of points in the UI.
|
||||
static const Color pointGainColor = Color(0xFFF44336);
|
||||
|
||||
// Text Styles
|
||||
/// Text style for mode titles, typically used in headers or section titles.
|
||||
static TextStyle modeTitle = TextStyle(
|
||||
color: primaryColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
/// Default text style for mode descriptions.
|
||||
static const TextStyle modeDescription = TextStyle(
|
||||
fontSize: 16,
|
||||
);
|
||||
|
||||
/// Text style for titles of sections of [CupertinoListTile].
|
||||
static TextStyle rowTitle = TextStyle(
|
||||
fontSize: 20,
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
/// Text style for round titles, used for prominent display of the round title
|
||||
static TextStyle roundTitle = TextStyle(
|
||||
fontSize: 60,
|
||||
color: white,
|
||||
|
||||
177
lib/data/dao/game_session_dao.dart
Normal file
177
lib/data/dao/game_session_dao.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:cabo_counter/data/dto/round.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'game_session_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GameSessionTable])
|
||||
class GameSessionDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$GameSessionDaoMixin {
|
||||
GameSessionDao(super.db);
|
||||
|
||||
/// Inserts a new game session into the database.
|
||||
/// This method takes a [GameSession] object as input and inserts its details
|
||||
/// into the [gameSessionTable]. It also inserts associated players and rounds
|
||||
/// using the respective DAOs.
|
||||
/// [gameSession] The [GameSession] object to insert into the database.
|
||||
Future<void> insertGameSession(GameSession gameSession) async {
|
||||
await into(gameSessionTable).insert(
|
||||
GameSessionTableCompanion.insert(
|
||||
gameId: gameSession.gameId,
|
||||
createdAt: gameSession.createdAt,
|
||||
gameTitle: gameSession.gameTitle,
|
||||
pointLimit: gameSession.pointLimit,
|
||||
caboPenalty: gameSession.caboPenalty,
|
||||
isPointsLimitEnabled: gameSession.isPointsLimitEnabled,
|
||||
isGameFinished: gameSession.isGameFinished,
|
||||
winner: Value(gameSession.winner),
|
||||
roundNumber: gameSession.roundNumber,
|
||||
),
|
||||
);
|
||||
|
||||
db.playerDao.insertPlayers(gameSession.gameId, gameSession.players);
|
||||
|
||||
db.roundsDao.insertMultipleRounds(
|
||||
gameSession.gameId, gameSession.roundList, gameSession.players);
|
||||
}
|
||||
|
||||
/// Retrieves all game sessions from the database.
|
||||
/// This method fetches all entries from the `gameSessionTable`,
|
||||
/// along with associated players and rounds for each session from their respective DAOs.
|
||||
/// It constructs and returns a list of `GameSession` objects containing all relevant data.
|
||||
/// Returns an empty list if no game sessions are found.
|
||||
/// Returns a [List] of [GameSession] objects.
|
||||
Future<List<GameSession>> getAllGameSessions() async {
|
||||
final query = select(gameSessionTable);
|
||||
final gameSessionResults = await query.get();
|
||||
|
||||
List<GameSession> gameSessions = await Future.wait(
|
||||
gameSessionResults.map((row) async {
|
||||
List<Player> playerList =
|
||||
await db.playerDao.getPlayersByGameId(row.gameId);
|
||||
List<Round> roundList =
|
||||
await db.roundsDao.getRoundsByGameId(row.gameId);
|
||||
|
||||
return GameSession(
|
||||
gameId: row.gameId,
|
||||
createdAt: row.createdAt,
|
||||
gameTitle: row.gameTitle,
|
||||
players: playerList,
|
||||
pointLimit: row.pointLimit,
|
||||
caboPenalty: row.caboPenalty,
|
||||
isPointsLimitEnabled: row.isPointsLimitEnabled,
|
||||
isGameFinished: row.isGameFinished,
|
||||
winner: row.winner ?? '',
|
||||
roundNumber: row.roundNumber,
|
||||
roundList: roundList,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return gameSessions;
|
||||
}
|
||||
|
||||
/// Retrieves a game session by its ID.
|
||||
/// This method fetches the game session details from the `gameSessionTable`,
|
||||
/// along with associated players and rounds from their respective DAOs.
|
||||
/// It constructs and returns a `GameSession` object containing all relevant data.
|
||||
/// [gameId] The ID of the game session to retrieve.
|
||||
Future<GameSession> getGameSession(String gameId) async {
|
||||
final query = select(gameSessionTable)
|
||||
..where((tbl) => tbl.gameId.equals(gameId));
|
||||
final gameSessionResult = await query.getSingle();
|
||||
|
||||
List<Player> playerList = await db.playerDao.getPlayersByGameId(gameId);
|
||||
List<Round> roundList = await db.roundsDao.getRoundsByGameId(gameId);
|
||||
|
||||
GameSession gameSession = GameSession(
|
||||
gameId: gameSessionResult.gameId,
|
||||
createdAt: gameSessionResult.createdAt,
|
||||
gameTitle: gameSessionResult.gameTitle,
|
||||
players: playerList,
|
||||
pointLimit: gameSessionResult.pointLimit,
|
||||
caboPenalty: gameSessionResult.caboPenalty,
|
||||
isPointsLimitEnabled: gameSessionResult.isPointsLimitEnabled,
|
||||
isGameFinished: gameSessionResult.isGameFinished,
|
||||
winner: gameSessionResult.winner ?? '',
|
||||
roundNumber: gameSessionResult.roundNumber,
|
||||
roundList: roundList);
|
||||
|
||||
return gameSession;
|
||||
}
|
||||
|
||||
/// Deletes a game session and its associated data from the database.
|
||||
/// This method removes the game session with the given [gameId] from the [gameSessionTable].
|
||||
/// It also deletes all related players and rounds because of foreign key constraints.
|
||||
/// [gameId] The ID of the game session to delete.
|
||||
Future<void> deleteGameSession(String gameId) async {
|
||||
await (delete(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
/// Deletes all game sessions from the database.
|
||||
/// This method removes all entries from the [gameSessionTable].
|
||||
void deleteAllGames() {
|
||||
delete(gameSessionTable).go();
|
||||
}
|
||||
|
||||
/// Updates the game finish status of a specific game session.
|
||||
/// This method updates the [isGameFinished] field in the [gameSessionTable]
|
||||
/// for the game session with the given [gameId].
|
||||
/// [gameId] The ID of the game session to update.
|
||||
/// [isFinished] The new finish status to set.
|
||||
Future<void> setGameFinishStatus(String gameId, bool isFinished) async {
|
||||
await (update(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.write(GameSessionTableCompanion(
|
||||
isGameFinished: Value(isFinished),
|
||||
));
|
||||
}
|
||||
|
||||
/// Updates the round number of a specific game session.
|
||||
/// This method updates the [roundNumber] field in the [gameSessionTable]
|
||||
/// for the game session with the given [gameId].
|
||||
/// [gameId] The ID of the game session to update.
|
||||
/// [roundNumber] The new round number to set.
|
||||
Future<void> setRoundNumber(String gameId, int roundNumber) async {
|
||||
await (update(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.write(GameSessionTableCompanion(
|
||||
roundNumber: Value(roundNumber),
|
||||
));
|
||||
}
|
||||
|
||||
/// Updates the winner of a specific game session.
|
||||
/// This method updates the [winner] field in the [gameSessionTable]
|
||||
/// for the game session with the given [gameId].
|
||||
/// [gameId] The ID of the game session to update.
|
||||
/// [winner] The name of the winner(s) to set.
|
||||
Future<void> setWinner(String gameId, String winner) async {
|
||||
await (update(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.write(GameSessionTableCompanion(
|
||||
winner: Value(winner),
|
||||
));
|
||||
}
|
||||
|
||||
/// Ends a game session by marking it as finished and adjusting the round number.
|
||||
/// This method updates the [isGameFinished] field to true and decrements the [roundNumber]
|
||||
/// by 1 for the game session with the given [gameId].
|
||||
/// [gameId] The ID of the game session to end.
|
||||
Future<void> endGame(String gameId) async {
|
||||
await (update(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.write(const GameSessionTableCompanion(
|
||||
isGameFinished: Value(true),
|
||||
));
|
||||
|
||||
int currentRoundNumber = await (select(gameSessionTable)
|
||||
..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.map((row) => row.roundNumber)
|
||||
.getSingle();
|
||||
|
||||
await (update(gameSessionTable)..where((tbl) => tbl.gameId.equals(gameId)))
|
||||
.write(GameSessionTableCompanion(
|
||||
roundNumber: Value(currentRoundNumber - 1),
|
||||
));
|
||||
}
|
||||
}
|
||||
9
lib/data/dao/game_session_dao.g.dart
Normal file
9
lib/data/dao/game_session_dao.g.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'game_session_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$GameSessionDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GameSessionTableTable get gameSessionTable =>
|
||||
attachedDatabase.gameSessionTable;
|
||||
}
|
||||
73
lib/data/dao/player_dao.dart
Normal file
73
lib/data/dao/player_dao.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/db/tables/player_table.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'player_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [PlayerTable])
|
||||
class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
PlayerDao(super.db);
|
||||
|
||||
/// Retrieves all players from a game by gameId
|
||||
Future<List<Player>> getPlayersByGameId(String gameId) async {
|
||||
final query = select(playerTable)
|
||||
..where((tbl) => tbl.gameId.equals(gameId));
|
||||
final playerResults = await query.get();
|
||||
|
||||
return playerResults.map((row) {
|
||||
return Player(
|
||||
playerId: row.playerId,
|
||||
gameId: row.gameId,
|
||||
name: row.name,
|
||||
position: row.position,
|
||||
totalScore: row.totalScore,
|
||||
);
|
||||
}).toList()
|
||||
..sort((a, b) => a.position.compareTo(b.position));
|
||||
}
|
||||
|
||||
/// Retrieves a players position by its id
|
||||
Future<int> getPositionByPlayerId(String playerId) async {
|
||||
final query = select(playerTable)
|
||||
..where((tbl) => tbl.playerId.equals(playerId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
return result.position;
|
||||
}
|
||||
|
||||
/// Inserts a new player into the database.
|
||||
Future<void> insertPlayers(String gameId, List<Player> players) async {
|
||||
await batch((batch) {
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
batch.insert(
|
||||
playerTable,
|
||||
PlayerTableCompanion.insert(
|
||||
playerId: players[i].playerId,
|
||||
gameId: gameId,
|
||||
name: players[i].name,
|
||||
position: i,
|
||||
totalScore: players[i].totalScore,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates the total scores of multiple players in a batch operation.
|
||||
Future<void> updatePlayerScores(List<Player> players) async {
|
||||
batch((batch) {
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
final player = players[i];
|
||||
final updatedScore = players[i].totalScore;
|
||||
batch.update(
|
||||
playerTable,
|
||||
PlayerTableCompanion(
|
||||
totalScore: Value(updatedScore),
|
||||
),
|
||||
where: (tbl) => tbl.playerId.equals(player.playerId),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
10
lib/data/dao/player_dao.g.dart
Normal file
10
lib/data/dao/player_dao.g.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'player_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GameSessionTableTable get gameSessionTable =>
|
||||
attachedDatabase.gameSessionTable;
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
}
|
||||
59
lib/data/dao/round_scores_dao.dart
Normal file
59
lib/data/dao/round_scores_dao.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/db/tables/round_scores_table.dart';
|
||||
import 'package:cabo_counter/data/dto/round_score.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'round_scores_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [RoundScoresTable])
|
||||
class RoundScoresDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$RoundScoresDaoMixin {
|
||||
RoundScoresDao(super.db);
|
||||
|
||||
/// Retrieves all scores for a specific round by its ID.
|
||||
/// This method returns a list of [RoundScore] objects sorted by player
|
||||
/// position in the corresponding gameSession
|
||||
Future<List<RoundScore>> _getRoundScoresByRoundId(String roundId) async {
|
||||
final query = select(roundScoresTable)
|
||||
..where((tbl) => tbl.roundId.equals(roundId));
|
||||
|
||||
final result = await query.get();
|
||||
|
||||
// Get positions for each player
|
||||
final scoresWithPosition = await Future.wait(result.map((row) async {
|
||||
final position = await db.playerDao.getPositionByPlayerId(row.playerId);
|
||||
return MapEntry(row, position);
|
||||
}));
|
||||
|
||||
// Sort rows by position
|
||||
scoresWithPosition.sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
return scoresWithPosition.map((entry) {
|
||||
final row = entry.key;
|
||||
return RoundScore(
|
||||
roundId: roundId,
|
||||
playerId: row.playerId,
|
||||
score: row.score,
|
||||
scoreUpdate: row.scoreUpdate,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Retrieves all scores for a specific round by its ID.
|
||||
/// This method returns a list of scores sorted by player position in the
|
||||
/// corresponding gameSession.
|
||||
Future<List<int>> getScoresByRoundId(String roundId) async {
|
||||
List<RoundScore> roundScores = await _getRoundScoresByRoundId(roundId);
|
||||
|
||||
return roundScores.map((score) => score.score).toList();
|
||||
}
|
||||
|
||||
/// Retrieves all score updates for a specific round by its ID.
|
||||
/// This method returns a list of score updates sorted by player position in
|
||||
/// the corresponding gameSession.
|
||||
Future<List<int>> getScoreUpdatesByRoundId(String roundId) async {
|
||||
List<RoundScore> roundScores = await _getRoundScoresByRoundId(roundId);
|
||||
|
||||
return roundScores.map((score) => score.scoreUpdate).toList();
|
||||
}
|
||||
}
|
||||
13
lib/data/dao/round_scores_dao.g.dart
Normal file
13
lib/data/dao/round_scores_dao.g.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'round_scores_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$RoundScoresDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GameSessionTableTable get gameSessionTable =>
|
||||
attachedDatabase.gameSessionTable;
|
||||
$RoundsTableTable get roundsTable => attachedDatabase.roundsTable;
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$RoundScoresTableTable get roundScoresTable =>
|
||||
attachedDatabase.roundScoresTable;
|
||||
}
|
||||
167
lib/data/dao/rounds_dao.dart
Normal file
167
lib/data/dao/rounds_dao.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/db/tables/round_scores_table.dart';
|
||||
import 'package:cabo_counter/data/db/tables/rounds_table.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:cabo_counter/data/dto/round.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'rounds_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [RoundsTable, RoundScoresTable])
|
||||
class RoundsDao extends DatabaseAccessor<AppDatabase> with _$RoundsDaoMixin {
|
||||
RoundsDao(super.db);
|
||||
|
||||
/// Retrieves all rounds for a specific game session by its ID.
|
||||
Future<List<Round>> getRoundsByGameId(String gameId) async {
|
||||
final query = select(roundsTable)
|
||||
..where((tbl) => tbl.gameId.equals(gameId));
|
||||
|
||||
final roundResult = await query.get();
|
||||
|
||||
final roundList = await Future.wait(
|
||||
roundResult.map((row) async {
|
||||
final scores = await db.roundScoresDao.getScoresByRoundId(row.roundId);
|
||||
final roundScores =
|
||||
await db.roundScoresDao.getScoreUpdatesByRoundId(row.roundId);
|
||||
|
||||
return Round(
|
||||
roundId: row.roundId,
|
||||
gameId: row.gameId,
|
||||
roundNum: row.roundNumber,
|
||||
caboPlayerIndex: row.caboPlayerIndex,
|
||||
kamikazePlayerIndex: row.kamikazePlayerIndex,
|
||||
scores: scores,
|
||||
scoreUpdates: roundScores,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return roundList;
|
||||
}
|
||||
|
||||
/// Retrieves a specific round by its [gameId] and [roundNumber].
|
||||
/// Returns null if the round does not exist.
|
||||
Future<Round?> getRoundByGameIdAndRoundNumber(
|
||||
String gameId, int roundNumber) async {
|
||||
final query = select(roundsTable)
|
||||
..where((tbl) =>
|
||||
tbl.gameId.equals(gameId) & tbl.roundNumber.equals(roundNumber));
|
||||
final roundResult = await query.getSingleOrNull();
|
||||
if (roundResult == null) return null;
|
||||
|
||||
final scoreResult = await Future.wait([
|
||||
db.roundScoresDao.getScoresByRoundId(roundResult.roundId),
|
||||
db.roundScoresDao.getScoreUpdatesByRoundId(roundResult.roundId),
|
||||
]);
|
||||
|
||||
return Round(
|
||||
roundId: roundResult.roundId,
|
||||
gameId: roundResult.gameId,
|
||||
roundNum: roundResult.roundNumber,
|
||||
caboPlayerIndex: roundResult.caboPlayerIndex,
|
||||
kamikazePlayerIndex: roundResult.kamikazePlayerIndex,
|
||||
scores: scoreResult[0],
|
||||
scoreUpdates: scoreResult[1],
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a new round into the database.
|
||||
/// This method creates a new round with a unique ID and inserts it
|
||||
/// along with the scores for each player in the round.
|
||||
/// [gameId] is the ID of the game session this round belongs to.
|
||||
/// [round] is the round data to be inserted.
|
||||
/// [players] is the list of players in the game session.
|
||||
Future<void> insertOneRound(
|
||||
String gameId, Round round, List<Player> players) async {
|
||||
final roundEntry = RoundsTableCompanion.insert(
|
||||
roundId: round.roundId,
|
||||
gameId: gameId,
|
||||
roundNumber: round.roundNum,
|
||||
caboPlayerIndex: round.caboPlayerIndex,
|
||||
kamikazePlayerIndex: Value(round.kamikazePlayerIndex),
|
||||
);
|
||||
|
||||
await into(roundsTable).insert(roundEntry);
|
||||
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
final player = players[i];
|
||||
final roundScoreEntry = RoundScoresTableCompanion.insert(
|
||||
roundId: round.roundId,
|
||||
playerId: player.playerId,
|
||||
score: round.scores[i],
|
||||
scoreUpdate: round.scoreUpdates[i],
|
||||
);
|
||||
await into(roundScoresTable).insert(roundScoreEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces an already existing round with a new one.
|
||||
/// [gameId] is the ID of the game session this round belongs to.
|
||||
/// [round] is the round data to be inserted.
|
||||
/// [players] is the list of players in the game session.
|
||||
Future<void> replaceRound(
|
||||
String gameId, Round round, List<Player> players) async {
|
||||
await deleteRound(gameId, round.roundNum);
|
||||
|
||||
await insertOneRound(gameId, round, players);
|
||||
}
|
||||
|
||||
/// Inserts multiple rounds into the database.
|
||||
/// This method uses a batch operation to insert all rounds and their scores
|
||||
/// in a single transaction.
|
||||
/// [gameId] is the ID of the game session these rounds belong to.
|
||||
/// [rounds] is the list of rounds to be inserted.
|
||||
/// [players] is the list of players in the game session.
|
||||
Future<void> insertMultipleRounds(
|
||||
String gameId, List<Round> rounds, List<Player> players) async {
|
||||
var uuid = const Uuid();
|
||||
|
||||
await batch((batch) {
|
||||
final roundEntries = <RoundsTableCompanion>[];
|
||||
final roundScoreEntries = <RoundScoresTableCompanion>[];
|
||||
|
||||
for (final round in rounds) {
|
||||
final roundId = uuid.v4();
|
||||
roundEntries.add(RoundsTableCompanion.insert(
|
||||
roundId: roundId,
|
||||
gameId: gameId,
|
||||
roundNumber: round.roundNum,
|
||||
caboPlayerIndex: round.caboPlayerIndex,
|
||||
kamikazePlayerIndex: Value(round.kamikazePlayerIndex),
|
||||
));
|
||||
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
roundScoreEntries.add(RoundScoresTableCompanion.insert(
|
||||
roundId: roundId,
|
||||
playerId: players[i].playerId,
|
||||
score: round.scores[i],
|
||||
scoreUpdate: round.scoreUpdates[i],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
batch.insertAll(roundsTable, roundEntries);
|
||||
batch.insertAll(roundScoresTable, roundScoreEntries);
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes a specific round by its [gameId] and [roundNumber].
|
||||
/// Returns true if the round was found and deleted, false otherwise.
|
||||
/// Also deletes all associated scores due to foreign key constraints.
|
||||
/// [gameId] is the ID of the game session this round belongs to.
|
||||
/// [roundNumber] is the number of the round to be deleted.
|
||||
Future<bool> deleteRound(String gameId, int roundNumber) async {
|
||||
final query = select(roundsTable)
|
||||
..where((tbl) =>
|
||||
tbl.gameId.equals(gameId) & tbl.roundNumber.equals(roundNumber));
|
||||
final roundResult = await query.getSingleOrNull();
|
||||
if (roundResult == null) return false;
|
||||
|
||||
final deleteQuery = delete(roundsTable)
|
||||
..where((tbl) =>
|
||||
tbl.gameId.equals(gameId) & tbl.roundNumber.equals(roundNumber));
|
||||
await deleteQuery.go();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
13
lib/data/dao/rounds_dao.g.dart
Normal file
13
lib/data/dao/rounds_dao.g.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'rounds_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$RoundsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$GameSessionTableTable get gameSessionTable =>
|
||||
attachedDatabase.gameSessionTable;
|
||||
$RoundsTableTable get roundsTable => attachedDatabase.roundsTable;
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$RoundScoresTableTable get roundScoresTable =>
|
||||
attachedDatabase.roundScoresTable;
|
||||
}
|
||||
41
lib/data/db/database.dart
Normal file
41
lib/data/db/database.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:cabo_counter/data/dao/game_session_dao.dart';
|
||||
import 'package:cabo_counter/data/dao/player_dao.dart';
|
||||
import 'package:cabo_counter/data/dao/round_scores_dao.dart';
|
||||
import 'package:cabo_counter/data/dao/rounds_dao.dart';
|
||||
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
|
||||
import 'package:cabo_counter/data/db/tables/player_table.dart';
|
||||
import 'package:cabo_counter/data/db/tables/round_scores_table.dart';
|
||||
import 'package:cabo_counter/data/db/tables/rounds_table.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [GameSessionTable, PlayerTable, RoundScoresTable, RoundsTable],
|
||||
daos: [GameSessionDao, PlayerDao, RoundsDao, RoundScoresDao])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
}
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
name: 'cabo-counter_database',
|
||||
native: const DriftNativeOptions(
|
||||
databaseDirectory: getApplicationSupportDirectory,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final db = AppDatabase();
|
||||
2941
lib/data/db/database.g.dart
Normal file
2941
lib/data/db/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
16
lib/data/db/tables/game_session_table.dart
Normal file
16
lib/data/db/tables/game_session_table.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class GameSessionTable extends Table {
|
||||
late final gameId = text()();
|
||||
late final createdAt = dateTime()();
|
||||
late final gameTitle = text()();
|
||||
late final pointLimit = integer()();
|
||||
late final caboPenalty = integer()();
|
||||
late final isPointsLimitEnabled = boolean()();
|
||||
late final isGameFinished = boolean()();
|
||||
late final winner = text().nullable()();
|
||||
late final roundNumber = integer()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {gameId};
|
||||
}
|
||||
14
lib/data/db/tables/player_table.dart
Normal file
14
lib/data/db/tables/player_table.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class PlayerTable extends Table {
|
||||
late final playerId = text()();
|
||||
late final gameId = text()
|
||||
.references(GameSessionTable, #gameId, onDelete: KeyAction.cascade)();
|
||||
late final totalScore = integer()();
|
||||
late final position = integer()();
|
||||
late final name = text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {playerId};
|
||||
}
|
||||
15
lib/data/db/tables/round_scores_table.dart
Normal file
15
lib/data/db/tables/round_scores_table.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:cabo_counter/data/db/tables/player_table.dart';
|
||||
import 'package:cabo_counter/data/db/tables/rounds_table.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class RoundScoresTable extends Table {
|
||||
late final roundId =
|
||||
text().references(RoundsTable, #roundId, onDelete: KeyAction.cascade)();
|
||||
late final playerId =
|
||||
text().references(PlayerTable, #playerId, onDelete: KeyAction.cascade)();
|
||||
late final score = integer()();
|
||||
late final scoreUpdate = integer()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {roundId, playerId};
|
||||
}
|
||||
14
lib/data/db/tables/rounds_table.dart
Normal file
14
lib/data/db/tables/rounds_table.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class RoundsTable extends Table {
|
||||
late final roundId = text()();
|
||||
late final gameId = text()
|
||||
.references(GameSessionTable, #gameId, onDelete: KeyAction.cascade)();
|
||||
late final roundNumber = integer()();
|
||||
late final caboPlayerIndex = integer()();
|
||||
late final kamikazePlayerIndex = integer().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {roundId};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@@ -18,60 +18,66 @@ class GameManager extends ChangeNotifier {
|
||||
gameList.add(session);
|
||||
gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
db.gameSessionDao.insertGameSession(session);
|
||||
return gameList.indexOf(session);
|
||||
}
|
||||
|
||||
/// Adds a game session from the database to the list and sorts it by creation date.
|
||||
/// Takes a [GameSession] object as input. It adds the session to the [gameList],
|
||||
/// sorts the list in descending order based on the creation date, and notifies listeners of the change.
|
||||
/// This method is used during the start of the app.
|
||||
void addGameSessionFromDataBase(GameSession session) {
|
||||
session.addListener(() {
|
||||
notifyListeners();
|
||||
});
|
||||
gameList.add(session);
|
||||
gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Retrieves a game session by its id.
|
||||
/// Takes a String [id] as input. It searches the `gameList` for a session
|
||||
/// with a matching id and returns it if found.
|
||||
/// If no session is found, it returns null.
|
||||
GameSession? getGameSessionById(String id) {
|
||||
return gameList.firstWhereOrNull((session) => session.id == id);
|
||||
}
|
||||
|
||||
/// Removes a game session from the list and sorts it by creation date.
|
||||
/// Takes a [index] as input. It then removes the session at the specified index from the `gameList`,
|
||||
/// sorts the list in descending order based on the creation date, and notifies listeners of the change.
|
||||
/// It also saves the updated game sessions to local storage.
|
||||
void removeGameSessionByIndex(int index) {
|
||||
gameList[index].removeListener(notifyListeners);
|
||||
gameList.removeAt(index);
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
return gameList.firstWhereOrNull((session) => session.gameId == id);
|
||||
}
|
||||
|
||||
/// Removes a game session by its ID.
|
||||
/// Takes a String [id] as input. It finds the index of the game session with the matching ID
|
||||
/// in the `gameList`, and then calls `removeGameSessionByIndex` with that index.
|
||||
void removeGameSessionById(String id) {
|
||||
final int index =
|
||||
gameList.indexWhere((session) => session.id.toString() == id);
|
||||
if (index == -1) return;
|
||||
removeGameSessionByIndex(index);
|
||||
/// Takes a String [id] as input. It removes the game session with the matching id
|
||||
/// from the `gameList`, deletes it from the database, and notifies listeners of the change.
|
||||
/// If no session with the given ID exists, the method does nothing.
|
||||
void deleteGameById(String id) {
|
||||
gameList.removeWhere((session) => session.gameId == id);
|
||||
db.gameSessionDao.deleteGameSession(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteAllGames() {
|
||||
gameList.clear();
|
||||
db.gameSessionDao.deleteAllGames();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Retrieves a game session by its ID.
|
||||
/// Takes a String [id] as input. It finds the game session with the matching id
|
||||
bool gameExistsInGameList(String id) {
|
||||
return gameList.any((session) => session.id.toString() == id);
|
||||
return gameList.any((session) => session.gameId.toString() == id);
|
||||
}
|
||||
|
||||
/// Ends a game session if its in unlimited mode.
|
||||
/// Takes a String [id] as input. It finds the index of the game
|
||||
/// Takes a String [gameId] as input. It finds the index of the game
|
||||
/// session with the matching ID marks it as finished,
|
||||
void endGame(String id) {
|
||||
void endGame(String gameId) {
|
||||
final int index =
|
||||
gameList.indexWhere((session) => session.id.toString() == id);
|
||||
gameList.indexWhere((session) => session.gameId.toString() == gameId);
|
||||
|
||||
// Game session not found or not in unlimited mode
|
||||
if (index == -1 || gameList[index].isPointsLimitEnabled == true) return;
|
||||
|
||||
gameList[index].roundNumber--;
|
||||
gameList[index].isGameFinished = true;
|
||||
gameList[index].setWinner();
|
||||
gameList[index].endGame();
|
||||
db.gameSessionDao.endGame(gameId);
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:cabo_counter/data/round.dart';
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:cabo_counter/data/dto/round.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -13,81 +15,70 @@ import 'package:uuid/uuid.dart';
|
||||
/// [isGameFinished] is a boolean indicating if the game has ended yet.
|
||||
/// [winner] is the name of the player who won the game.
|
||||
class GameSession extends ChangeNotifier {
|
||||
final String id;
|
||||
final String gameId;
|
||||
final DateTime createdAt;
|
||||
final String gameTitle;
|
||||
final List<String> players;
|
||||
final List<Player> players;
|
||||
final int pointLimit;
|
||||
final int caboPenalty;
|
||||
final bool isPointsLimitEnabled;
|
||||
bool isGameFinished = false;
|
||||
String winner = '';
|
||||
int roundNumber = 1;
|
||||
late List<int> playerScores;
|
||||
List<Round> roundList = [];
|
||||
bool isGameFinished;
|
||||
String winner;
|
||||
int roundNumber;
|
||||
List<Round> roundList;
|
||||
|
||||
GameSession({
|
||||
required this.id,
|
||||
required this.gameId,
|
||||
required this.createdAt,
|
||||
required this.gameTitle,
|
||||
required this.players,
|
||||
required this.pointLimit,
|
||||
required this.caboPenalty,
|
||||
required this.isPointsLimitEnabled,
|
||||
}) {
|
||||
playerScores = List.filled(players.length, 0);
|
||||
}
|
||||
this.isGameFinished = false,
|
||||
this.winner = '',
|
||||
this.roundNumber = 1,
|
||||
List<Round>? roundList,
|
||||
}) : roundList = roundList ?? [];
|
||||
|
||||
@override
|
||||
toString() {
|
||||
return ('GameSession: [id: $id, createdAt: $createdAt, gameTitle: $gameTitle, '
|
||||
return 'GameSession: [id: $gameId, createdAt: $createdAt, gameTitle: $gameTitle, '
|
||||
'isPointsLimitEnabled: $isPointsLimitEnabled, pointLimit: $pointLimit, caboPenalty: $caboPenalty,'
|
||||
' players: $players, playerScores: $playerScores, roundList: $roundList, winner: $winner]');
|
||||
' players: $players, roundList: $roundList, winner: $winner]';
|
||||
}
|
||||
|
||||
/// Converts the GameSession object to a JSON map.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'id': gameId,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'gameTitle': gameTitle,
|
||||
'players': players,
|
||||
'players': players.map((p) => p.toJson()).toList(),
|
||||
'pointLimit': pointLimit,
|
||||
'caboPenalty': caboPenalty,
|
||||
'isPointsLimitEnabled': isPointsLimitEnabled,
|
||||
'isGameFinished': isGameFinished,
|
||||
'winner': winner,
|
||||
'roundNumber': roundNumber,
|
||||
'playerScores': playerScores,
|
||||
'roundList': roundList.map((e) => e.toJson()).toList()
|
||||
};
|
||||
|
||||
/// Creates a GameSession object from a JSON map.
|
||||
GameSession.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'] ?? const Uuid().v1(),
|
||||
: gameId = json['id'] ?? const Uuid().v4(),
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
gameTitle = json['gameTitle'],
|
||||
players = List<String>.from(json['players']),
|
||||
players =
|
||||
(json['players'] as List).map((e) => Player.fromJson(e)).toList(),
|
||||
pointLimit = json['pointLimit'],
|
||||
caboPenalty = json['caboPenalty'],
|
||||
isPointsLimitEnabled = json['isPointsLimitEnabled'],
|
||||
isGameFinished = json['isGameFinished'],
|
||||
winner = json['winner'],
|
||||
roundNumber = json['roundNumber'],
|
||||
playerScores = List<int>.from(json['playerScores']),
|
||||
roundList =
|
||||
(json['roundList'] as List).map((e) => Round.fromJson(e)).toList();
|
||||
|
||||
/// Returns the length of the longest player name.
|
||||
int getMaxLengthOfPlayerNames() {
|
||||
int length = 0;
|
||||
for (String player in players) {
|
||||
if (player.length >= length) {
|
||||
length = player.length;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
/// Assigns 50 points to all players except the kamikaze player.
|
||||
/// [kamikazePlayerIndex] is the index of the kamikaze player.
|
||||
void applyKamikaze(int roundNum, int kamikazePlayerIndex) {
|
||||
@@ -116,32 +107,15 @@ class GameSession extends ChangeNotifier {
|
||||
/// Every other player gets their round score.
|
||||
void calculateScoredPoints(
|
||||
int roundNum, List<int> roundScores, int caboPlayerIndex) {
|
||||
print('Spieler: $players');
|
||||
print('Punkte: $roundScores');
|
||||
print('${players[caboPlayerIndex]} hat mit ${roundScores[caboPlayerIndex]} '
|
||||
'Punkten CABO gesagt');
|
||||
|
||||
/// List of the index of the player(s) with the lowest score
|
||||
List<int> lowestScoreIndex = _getLowestScoreIndex(roundScores);
|
||||
print('Folgende Spieler haben die niedrigsten Punte:');
|
||||
for (int i in lowestScoreIndex) {
|
||||
print('${players[i]} (${roundScores[i]} Punkte)');
|
||||
}
|
||||
// The player who said CABO is one of the players which have the
|
||||
// fewest points.
|
||||
|
||||
if (lowestScoreIndex.contains(caboPlayerIndex)) {
|
||||
print('${players[caboPlayerIndex]} hat CABO gesagt '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen ihre Punkte');
|
||||
// The player who said CABO is one of the players which have the
|
||||
// fewest points.
|
||||
_assignPoints(roundNum, roundScores, caboPlayerIndex, [caboPlayerIndex]);
|
||||
} else {
|
||||
// A player other than the one who said CABO has the fewest points.
|
||||
print('${players[caboPlayerIndex]} hat CABO gesagt, '
|
||||
'jedoch nicht die wenigsten Punkte.');
|
||||
print('Folgende:r Spieler haben die wenigsten Punkte:');
|
||||
for (int i in lowestScoreIndex) {
|
||||
print('${players[i]}: ${roundScores[i]} Punkte');
|
||||
}
|
||||
_assignPoints(roundNum, roundScores, caboPlayerIndex, lowestScoreIndex,
|
||||
caboPlayerIndex);
|
||||
}
|
||||
@@ -185,23 +159,13 @@ class GameSession extends ChangeNotifier {
|
||||
[int? loserIndex]) {
|
||||
/// List of the updates for every player score
|
||||
List<int> scoreUpdates = [...roundScores];
|
||||
print('Folgende Punkte wurden aus der Runde übernommen:');
|
||||
for (int i = 0; i < scoreUpdates.length; i++) {
|
||||
print('${players[i]}: ${scoreUpdates[i]}');
|
||||
}
|
||||
|
||||
for (int i in winnerIndex) {
|
||||
print('${players[i]} hat gewonnen und bekommt 0 Punkte');
|
||||
scoreUpdates[i] = 0;
|
||||
}
|
||||
if (loserIndex != null) {
|
||||
print('${players[loserIndex]} bekommt 5 Fehlerpunkte');
|
||||
scoreUpdates[loserIndex] += 5;
|
||||
}
|
||||
print('Aktualisierte Punkte:');
|
||||
for (int i = 0; i < scoreUpdates.length; i++) {
|
||||
print('${players[i]}: ${scoreUpdates[i]}');
|
||||
}
|
||||
print('scoreUpdates: $scoreUpdates, roundScores: $roundScores');
|
||||
addRoundScoresToList(roundNum, roundScores, scoreUpdates, caboPlayerIndex);
|
||||
}
|
||||
|
||||
@@ -217,7 +181,10 @@ class GameSession extends ChangeNotifier {
|
||||
int caboPlayerIndex, [
|
||||
int? kamikazePlayerIndex,
|
||||
]) {
|
||||
const uuid = Uuid();
|
||||
Round newRound = Round(
|
||||
roundId: uuid.v4(),
|
||||
gameId: gameId,
|
||||
roundNum: roundNum,
|
||||
caboPlayerIndex: caboPlayerIndex,
|
||||
kamikazePlayerIndex: kamikazePlayerIndex,
|
||||
@@ -226,9 +193,12 @@ class GameSession extends ChangeNotifier {
|
||||
);
|
||||
if (roundNum > roundList.length) {
|
||||
roundList.add(newRound);
|
||||
db.roundsDao.insertOneRound(gameId, newRound, players);
|
||||
} else {
|
||||
roundList[roundNum - 1] = newRound;
|
||||
db.roundsDao.replaceRound(gameId, newRound, players);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -242,8 +212,8 @@ class GameSession extends ChangeNotifier {
|
||||
/// It then checks if any player has exceeded 100 points. If so, it sets
|
||||
/// isGameFinished to true and calls the _setWinner() method to determine
|
||||
/// the winner.
|
||||
/// It returns a list of players indices who reached 100 points in the current
|
||||
/// round for the [RoundView] to show a popup
|
||||
/// It returns a list of players indices who reached 100 points (bonus player)
|
||||
/// in the current round for the [RoundView] to show a popup
|
||||
List<int> updatePoints() {
|
||||
List<int> bonusPlayers = [];
|
||||
_sumPoints();
|
||||
@@ -252,12 +222,10 @@ class GameSession extends ChangeNotifier {
|
||||
bonusPlayers = _checkHundredPointsReached();
|
||||
bool limitExceeded = false;
|
||||
|
||||
for (int i = 0; i < playerScores.length; i++) {
|
||||
if (playerScores[i] > pointLimit) {
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (players[i].totalScore > pointLimit) {
|
||||
isGameFinished = true;
|
||||
limitExceeded = true;
|
||||
print('${players[i]} hat die 100 Punkte ueberschritten, '
|
||||
'deswegen wurde das Spiel beendet');
|
||||
setWinner();
|
||||
}
|
||||
}
|
||||
@@ -265,6 +233,7 @@ class GameSession extends ChangeNotifier {
|
||||
isGameFinished = false;
|
||||
}
|
||||
}
|
||||
db.gameSessionDao.setGameFinishStatus(gameId, isGameFinished);
|
||||
notifyListeners();
|
||||
return bonusPlayers;
|
||||
}
|
||||
@@ -276,11 +245,12 @@ class GameSession extends ChangeNotifier {
|
||||
/// playerScores list.
|
||||
void _sumPoints() {
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
playerScores[i] = 0;
|
||||
players[i].totalScore = 0;
|
||||
for (int j = 0; j < roundList.length; j++) {
|
||||
playerScores[i] += roundList[j].scoreUpdates[i];
|
||||
players[i].totalScore += roundList[j].scoreUpdates[i];
|
||||
}
|
||||
}
|
||||
db.playerDao.updatePlayerScores(players);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -290,10 +260,8 @@ class GameSession extends ChangeNotifier {
|
||||
List<int> _checkHundredPointsReached() {
|
||||
List<int> bonusPlayers = [];
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (playerScores[i] == pointLimit) {
|
||||
if (players[i].totalScore == pointLimit) {
|
||||
bonusPlayers.add(i);
|
||||
print('${players[i]} hat genau 100 Punkte erreicht und bekommt '
|
||||
'deswegen ${(pointLimit / 2).round()} Punkte abgezogen');
|
||||
roundList[roundNumber - 1].scoreUpdates[i] -= (pointLimit / 2).round();
|
||||
}
|
||||
}
|
||||
@@ -301,15 +269,23 @@ class GameSession extends ChangeNotifier {
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
List<int> getPlayerScoresAsList() {
|
||||
return players.map((player) => player.totalScore).toList();
|
||||
}
|
||||
|
||||
List<String> getPlayerNamesAsList() {
|
||||
return players.map((player) => player.name).toList();
|
||||
}
|
||||
|
||||
/// Determines the winner of the game session.
|
||||
/// It iterates through the player scores and finds the player
|
||||
/// with the lowest score.
|
||||
void setWinner() {
|
||||
int minScore = playerScores.reduce((a, b) => a < b ? a : b);
|
||||
int minScore = getPlayerScoresAsList().reduce((a, b) => a < b ? a : b);
|
||||
List<String> lowestPlayers = [];
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (playerScores[i] == minScore) {
|
||||
lowestPlayers.add(players[i]);
|
||||
if (players[i].totalScore == minScore) {
|
||||
lowestPlayers.add(players[i].name);
|
||||
}
|
||||
}
|
||||
if (lowestPlayers.length > 1) {
|
||||
@@ -318,14 +294,24 @@ class GameSession extends ChangeNotifier {
|
||||
} else {
|
||||
winner = lowestPlayers.first;
|
||||
}
|
||||
db.gameSessionDao.setWinner(gameId, winner);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Increases the round number by 1.
|
||||
void increaseRound() {
|
||||
roundNumber++;
|
||||
print('roundNumber erhöht: $roundNumber — Hash: ${identityHashCode(this)}');
|
||||
db.gameSessionDao.setRoundNumber(gameId, roundNumber);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Ends the game if it is in unlimited mode.
|
||||
/// It decreases the round number by 1, sets isGameFinished to true,
|
||||
/// and calls the setWinner() method to determine the winner.
|
||||
void endGame() {
|
||||
roundNumber--;
|
||||
isGameFinished = true;
|
||||
setWinner();
|
||||
}
|
||||
}
|
||||
34
lib/data/dto/player.dart
Normal file
34
lib/data/dto/player.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
class Player {
|
||||
final String playerId;
|
||||
final String gameId;
|
||||
final String name;
|
||||
final int position;
|
||||
int totalScore;
|
||||
|
||||
Player(
|
||||
{required this.playerId,
|
||||
required this.gameId,
|
||||
required this.name,
|
||||
required this.position,
|
||||
this.totalScore = 0});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Player: [playerId: $playerId, gameId: $gameId, name: $name, position: $position]';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'playerId': playerId,
|
||||
'gameId': gameId,
|
||||
'name': name,
|
||||
'position': position,
|
||||
'totalScore': totalScore
|
||||
};
|
||||
|
||||
Player.fromJson(Map<String, dynamic> json)
|
||||
: playerId = json['playerId'],
|
||||
gameId = json['gameId'],
|
||||
name = json['name'],
|
||||
position = json['position'],
|
||||
totalScore = json['totalScore'];
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
|
||||
/// This class represents a single round in the game.
|
||||
/// It is stored within the [GameSession] class.
|
||||
/// [roundNum] is the number of the round its reppresenting.
|
||||
@@ -8,6 +6,8 @@ import 'package:cabo_counter/data/game_session.dart';
|
||||
/// [kamikazePlayerIndex] is the index of the player who got kamikaze. If no one got
|
||||
/// kamikaze, this value is null.
|
||||
class Round {
|
||||
final String roundId;
|
||||
final String gameId;
|
||||
final int roundNum;
|
||||
final int caboPlayerIndex;
|
||||
final int? kamikazePlayerIndex;
|
||||
@@ -15,11 +15,13 @@ class Round {
|
||||
final List<int> scoreUpdates;
|
||||
|
||||
Round({
|
||||
required this.roundId,
|
||||
required this.gameId,
|
||||
required this.roundNum,
|
||||
required this.caboPlayerIndex,
|
||||
this.kamikazePlayerIndex,
|
||||
required this.scores,
|
||||
required this.scoreUpdates,
|
||||
this.kamikazePlayerIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -31,6 +33,8 @@ class Round {
|
||||
|
||||
/// Converts the Round object to a JSON map.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'roundId': roundId,
|
||||
'gameId': gameId,
|
||||
'roundNum': roundNum,
|
||||
'caboPlayerIndex': caboPlayerIndex,
|
||||
'kamikazePlayerIndex': kamikazePlayerIndex,
|
||||
@@ -40,7 +44,9 @@ class Round {
|
||||
|
||||
/// Creates a Round object from a JSON map.
|
||||
Round.fromJson(Map<String, dynamic> json)
|
||||
: roundNum = json['roundNum'],
|
||||
: roundId = json['roundId'],
|
||||
gameId = json['gameId'],
|
||||
roundNum = json['roundNum'],
|
||||
caboPlayerIndex = json['caboPlayerIndex'],
|
||||
kamikazePlayerIndex = json['kamikazePlayerIndex'],
|
||||
scores = List<int>.from(json['scores']),
|
||||
12
lib/data/dto/round_score.dart
Normal file
12
lib/data/dto/round_score.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
class RoundScore {
|
||||
final String roundId;
|
||||
final String playerId;
|
||||
final int score;
|
||||
final int scoreUpdate;
|
||||
|
||||
RoundScore(
|
||||
{required this.roundId,
|
||||
required this.playerId,
|
||||
required this.score,
|
||||
required this.scoreUpdate});
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
"licenses": "Lizenzen",
|
||||
"license_details": "Lizenzdetails",
|
||||
"no_license_text": "Keine Lizenz verfügbar",
|
||||
"imprint": "Impressum",
|
||||
"legal_notice": "Impressum",
|
||||
|
||||
"empty_text_1": "Ganz schön leer hier...",
|
||||
"empty_text_2": "Füge über den Button oben rechts eine neue Runde hinzu",
|
||||
@@ -162,6 +162,6 @@
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App-Version",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"loading": "Lädt...",
|
||||
"loading_games": "Lade Spiele ...",
|
||||
"build": "Build-Nr."
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"about": "About",
|
||||
"licenses": "Licenses",
|
||||
"license_details": "License Details",
|
||||
"imprint": "Imprint",
|
||||
"legal_notice": "Legal Notice",
|
||||
"no_license_text": "No license available",
|
||||
|
||||
"empty_text_1": "Pretty empty here...",
|
||||
@@ -162,6 +162,6 @@
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App Version",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"loading": "Loading...",
|
||||
"loading_games": "Loading Games ...",
|
||||
"build": "Build No."
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ import 'app_localizations_en.dart';
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
@@ -86,16 +86,16 @@ abstract class AppLocalizations {
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('de'),
|
||||
Locale('en')
|
||||
Locale('en'),
|
||||
];
|
||||
|
||||
/// No description provided for @app_name.
|
||||
@@ -224,11 +224,11 @@ abstract class AppLocalizations {
|
||||
/// **'Keine Lizenz verfügbar'**
|
||||
String get no_license_text;
|
||||
|
||||
/// No description provided for @imprint.
|
||||
/// No description provided for @legal_notice.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Impressum'**
|
||||
String get imprint;
|
||||
String get legal_notice;
|
||||
|
||||
/// No description provided for @empty_text_1.
|
||||
///
|
||||
@@ -487,7 +487,11 @@ abstract class AppLocalizations {
|
||||
/// In de, this message translates to:
|
||||
/// **'{playerCount, plural, =1{{names} hat exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommt deshalb {bonusPoints} Punkte abgezogen!} other{{names} haben exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommen deshalb jeweils {bonusPoints} Punkte abgezogen!}}'**
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints);
|
||||
int playerCount,
|
||||
String names,
|
||||
int pointLimit,
|
||||
int bonusPoints,
|
||||
);
|
||||
|
||||
/// No description provided for @end_of_game_title.
|
||||
///
|
||||
@@ -735,11 +739,11 @@ abstract class AppLocalizations {
|
||||
/// **'Datenschutzerklärung'**
|
||||
String get privacy_policy;
|
||||
|
||||
/// No description provided for @loading.
|
||||
/// No description provided for @loading_games.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lädt...'**
|
||||
String get loading;
|
||||
/// **'Lade Spiele ...'**
|
||||
String get loading_games;
|
||||
|
||||
/// No description provided for @build.
|
||||
///
|
||||
@@ -775,8 +779,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.');
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get no_license_text => 'Keine Lizenz verfügbar';
|
||||
|
||||
@override
|
||||
String get imprint => 'Impressum';
|
||||
String get legal_notice => 'Impressum';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Ganz schön leer hier...';
|
||||
@@ -214,7 +214,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
int playerCount,
|
||||
String names,
|
||||
int pointLimit,
|
||||
int bonusPoints,
|
||||
) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
@@ -366,7 +370,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get privacy_policy => 'Datenschutzerklärung';
|
||||
|
||||
@override
|
||||
String get loading => 'Lädt...';
|
||||
String get loading_games => 'Lade Spiele ...';
|
||||
|
||||
@override
|
||||
String get build => 'Build-Nr.';
|
||||
|
||||
@@ -72,7 +72,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get no_license_text => 'No license available';
|
||||
|
||||
@override
|
||||
String get imprint => 'Imprint';
|
||||
String get legal_notice => 'Legal Notice';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Pretty empty here...';
|
||||
@@ -211,7 +211,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
int playerCount,
|
||||
String names,
|
||||
int pointLimit,
|
||||
int bonusPoints,
|
||||
) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
@@ -363,7 +367,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get privacy_policy => 'Privacy Policy';
|
||||
|
||||
@override
|
||||
String get loading => 'Loading...';
|
||||
String get loading_games => 'Loading Games ...';
|
||||
|
||||
@override
|
||||
String get build => 'Build No.';
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/tab_view.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:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Ensure the app runs in portrait mode only
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||
// Initialize services
|
||||
await ConfigService.initConfig();
|
||||
await VersionService.init();
|
||||
runApp(const App());
|
||||
await ConfigService.setMigrationDone(false);
|
||||
runApp(
|
||||
Provider<AppDatabase>(
|
||||
create: (_) => db,
|
||||
child: const App(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@@ -24,31 +33,8 @@ class App extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
LocalStorageService.loadGameSessions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
LocalStorageService.saveGameSessions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LocalStorageService.loadGameSessions();
|
||||
|
||||
return CupertinoApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: const [
|
||||
|
||||
@@ -55,8 +55,8 @@ class AboutView extends StatelessWidget {
|
||||
CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(AppLocalizations.of(context).imprint),
|
||||
onPressed: () => launchUrl(Uri.parse(Constants.kImprintLink)),
|
||||
child: Text(AppLocalizations.of(context).legal_notice),
|
||||
onPressed: () => launchUrl(Uri.parse(Constants.kLegalLink)),
|
||||
),
|
||||
CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
|
||||
@@ -3,15 +3,24 @@ import 'package:cabo_counter/l10n/generated/app_localizations.dart'
|
||||
show AppLocalizations;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// A view that displays the details of a specific open source software license.
|
||||
/// It shows the title and the full license text in a scrollable view.
|
||||
/// Displays the details of a specific open source software license in a Cupertino-style view.
|
||||
///
|
||||
/// This view presents the license title and its full text in a scrollable layout.
|
||||
///
|
||||
/// Required parameters:
|
||||
/// - [title]: The name of the license.
|
||||
/// - [license]: The full license text to display.
|
||||
class LicenseDetailView extends StatelessWidget {
|
||||
final String title, license;
|
||||
final String title, description, license;
|
||||
const LicenseDetailView(
|
||||
{super.key, required this.title, required this.license});
|
||||
{super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.license});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print(description);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(
|
||||
@@ -40,12 +49,22 @@ class LicenseDetailView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 8),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.buttonBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Text(
|
||||
license,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
|
||||
@@ -6,8 +6,14 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// A view that displays a list of the open source software licenses used in the app.
|
||||
/// It allows users to tap on a license to view its details in a separate screen.
|
||||
/// Displays a list of open source software licenses used in the app.
|
||||
///
|
||||
/// Users can tap on a license to view its details on a separate screen.
|
||||
/// This view uses a Cupertino design and supports localization.
|
||||
///
|
||||
/// See also:
|
||||
/// - [LicenseDetailView] for displaying license details.
|
||||
/// - [ossLicenses] for the list of licenses.
|
||||
class LicenseView extends StatelessWidget {
|
||||
const LicenseView({super.key});
|
||||
|
||||
@@ -24,7 +30,7 @@ class LicenseView extends StatelessWidget {
|
||||
itemCount: ossLicenses.length,
|
||||
itemBuilder: (_, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
@@ -38,6 +44,7 @@ class LicenseView extends StatelessWidget {
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => LicenseDetailView(
|
||||
title: ossLicenses[index].name,
|
||||
description: ossLicenses[index].description,
|
||||
license: ossLicenses[index].license ??
|
||||
AppLocalizations.of(context).no_license_text,
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
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/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/graph_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/mode_selection_view.dart';
|
||||
@@ -9,12 +9,19 @@ import 'package:cabo_counter/presentation/views/home/active_game/points_view.dar
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/round_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/create_game_view.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/services/data_transfer_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Displays the active game view, showing game details, player rankings, rounds, and statistics.
|
||||
///
|
||||
/// This view allows users to interact with an ongoing game session, including viewing player scores,
|
||||
/// navigating through rounds, ending or deleting the game, exporting game data, and starting a new game
|
||||
/// with the same settings. It also provides visual feedback such as confetti animation when the game ends.
|
||||
///
|
||||
/// The widget listens to changes in the provided [GameSession] and updates the UI accordingly.
|
||||
class ActiveGameView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
const ActiveGameView({super.key, required this.gameSession});
|
||||
@@ -55,7 +62,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
builder: (context, _) {
|
||||
sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
denseRanks = _calculateDenseRank(
|
||||
gameSession.playerScores, sortedPlayerIndices);
|
||||
gameSession.getPlayerScoresAsList(), sortedPlayerIndices);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: AppLocalizations.of(context).games,
|
||||
@@ -110,7 +117,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
_getPlacementTextWidget(index),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
gameSession.players[playerIndex],
|
||||
gameSession.players[playerIndex].name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -120,7 +127,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${gameSession.playerScores[playerIndex]} '
|
||||
'${gameSession.getPlayerScoresAsList()[playerIndex]} '
|
||||
'${AppLocalizations.of(context).points}')
|
||||
],
|
||||
),
|
||||
@@ -251,15 +258,14 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => CreateGameView(
|
||||
gameTitle:
|
||||
gameSession.gameTitle,
|
||||
gameMode: widget.gameSession
|
||||
.isPointsLimitEnabled ==
|
||||
true
|
||||
? GameMode.pointLimit
|
||||
: GameMode.unlimited,
|
||||
players: gameSession.players,
|
||||
)));
|
||||
gameTitle: gameSession.gameTitle,
|
||||
gameMode: widget.gameSession
|
||||
.isPointsLimitEnabled ==
|
||||
true
|
||||
? GameMode.pointLimit
|
||||
: GameMode.unlimited,
|
||||
players: gameSession
|
||||
.getPlayerNamesAsList())));
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
@@ -269,7 +275,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () async {
|
||||
final success = await LocalStorageService
|
||||
final success = await DataTransferService
|
||||
.exportSingleGameSession(
|
||||
widget.gameSession);
|
||||
if (!success && context.mounted) {
|
||||
@@ -344,7 +350,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
gameManager.endGame(gameSession.id);
|
||||
gameManager.endGame(gameSession.gameId);
|
||||
_playFinishAnimation(context);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
@@ -367,8 +373,8 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
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];
|
||||
int scoreA = gameSession.getPlayerScoresAsList()[a];
|
||||
int scoreB = gameSession.getPlayerScoresAsList()[b];
|
||||
if (scoreA != scoreB) {
|
||||
return scoreA.compareTo(scoreB);
|
||||
}
|
||||
@@ -448,11 +454,11 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
/// 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)) {
|
||||
if (gameManager.gameExistsInGameList(gameSession.gameId)) {
|
||||
Navigator.pop(context);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gameManager.removeGameSessionById(gameSession.id);
|
||||
gameManager.deleteGameById(gameSession.gameId);
|
||||
});
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
@@ -508,7 +514,8 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
/// Plays the confetti animation and shows a dialog with the winner's information.
|
||||
Future<void> _playFinishAnimation(BuildContext context) async {
|
||||
String winner = widget.gameSession.winner;
|
||||
int winnerPoints = widget.gameSession.playerScores.min;
|
||||
|
||||
int winnerPoints = widget.gameSession.getPlayerScoresAsList().min;
|
||||
int winnerAmount = winner.contains('&') ? 2 : 1;
|
||||
|
||||
confettiController.play();
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
/// A widget that displays the cumulative scoring history of a game session as a line graph.
|
||||
///
|
||||
/// The [GraphView] visualizes the progression of each player's score over multiple rounds
|
||||
/// using a line chart. It supports dynamic coloring for each player, axis formatting,
|
||||
/// and handles cases where insufficient data is available to render the graph.
|
||||
class GraphView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
@@ -89,7 +94,7 @@ class _GraphViewState extends State<GraphView> {
|
||||
List<LineSeries<(int, num), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
final playerNames = widget.gameSession.getPlayerNamesAsList();
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
@@ -8,6 +8,12 @@ enum GameMode {
|
||||
unlimited,
|
||||
}
|
||||
|
||||
/// A stateless widget that displays a menu for selecting the game mode.
|
||||
///
|
||||
/// The [ModeSelectionMenu] allows the user to choose between different game modes:
|
||||
/// - Point limit mode with a specified [pointLimit]
|
||||
/// - Unlimited mode
|
||||
/// - Optionally, no default mode if [showDeselection] is true
|
||||
class ModeSelectionMenu extends StatelessWidget {
|
||||
final int pointLimit;
|
||||
final bool showDeselection;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Displays an overview of points for each player and round in the current game session.
|
||||
///
|
||||
/// The [PointsView] widget shows a table with all rounds and player scores,
|
||||
/// including score updates and highlights for players who said "Cabo".
|
||||
/// It uses a Cupertino-style layout and adapts to the number of players.
|
||||
///
|
||||
/// Requires a [GameSession] to provide player and round data.
|
||||
class PointsView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
@@ -61,7 +68,7 @@ class _PointsViewState extends State<PointsView> {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
player,
|
||||
player.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -113,7 +120,7 @@ class _PointsViewState extends State<PointsView> {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8),
|
||||
child: Text(
|
||||
player,
|
||||
player.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -229,18 +236,20 @@ class _PointsViewState extends State<PointsView> {
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
)),
|
||||
...widget.gameSession.playerScores.map(
|
||||
(score) => DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'$score',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
...widget.gameSession
|
||||
.getPlayerScoresAsList()
|
||||
.map(
|
||||
(score) => DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'$score',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_button.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';
|
||||
|
||||
/// A view for displaying and managing a single round
|
||||
///
|
||||
/// This widget allows users to input and review scores for each player in a round,
|
||||
/// select the player who called CABO, and handle special cases such as Kamikaze rounds.
|
||||
/// It manages the round state, validates input, and coordinates navigation between rounds.
|
||||
///
|
||||
/// Features:
|
||||
/// - Rotates player order based on the previous round's winner.
|
||||
/// - Supports Kamikaze rounds with dedicated UI and logic.
|
||||
/// - Handles score input, validation, and updates to the game session.
|
||||
/// - Displays bonus point popups when applicable.
|
||||
///
|
||||
/// Requires a [GameSession] and the current [roundNumber].
|
||||
class RoundView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
final int roundNumber;
|
||||
@@ -46,12 +58,8 @@ class _RoundViewState extends State<RoundView> {
|
||||
|
||||
@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++) {
|
||||
@@ -85,7 +93,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
//LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context, -1)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
@@ -133,7 +141,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
final player = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
@@ -144,7 +152,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
name,
|
||||
player.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
@@ -197,7 +205,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
]))
|
||||
]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[originalIndex]}'
|
||||
'${widget.gameSession.getPlayerScoresAsList()[originalIndex]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: SizedBox(
|
||||
width: 100,
|
||||
@@ -316,10 +324,11 @@ class _RoundViewState extends State<RoundView> {
|
||||
/// Rotates the players list based on the previous round's winner.
|
||||
List<String> _getRotatedPlayers() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
final playerList = widget.gameSession.getPlayerNamesAsList();
|
||||
return [
|
||||
widget.gameSession.players[winnerIndex],
|
||||
...widget.gameSession.players.sublist(winnerIndex + 1),
|
||||
...widget.gameSession.players.sublist(0, winnerIndex)
|
||||
playerList[winnerIndex],
|
||||
...playerList.sublist(winnerIndex + 1),
|
||||
...playerList.sublist(0, winnerIndex)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -345,14 +354,14 @@ class _RoundViewState extends State<RoundView> {
|
||||
message: Text(AppLocalizations.of(context).who_has_kamikaze),
|
||||
actions: widget.gameSession.players.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
final player = entry.value;
|
||||
return CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
_kamikazePlayerIndex = index;
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(
|
||||
name,
|
||||
player.name,
|
||||
style: TextStyle(color: CustomTheme.kamikazeColor),
|
||||
),
|
||||
);
|
||||
@@ -421,17 +430,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
/// 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 {
|
||||
@@ -443,9 +442,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
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) {
|
||||
if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
return bonusPlayers;
|
||||
@@ -481,7 +478,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
String _getBonusPopupMessageString(
|
||||
int pointLimit, int bonusPoints, List<int> bonusPlayers) {
|
||||
List<String> nameList =
|
||||
bonusPlayers.map((i) => widget.gameSession.players[i]).toList();
|
||||
bonusPlayers.map((i) => widget.gameSession.players[i].name).toList();
|
||||
String resultText = '';
|
||||
if (nameList.length == 1) {
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
@@ -512,7 +509,7 @@ class _RoundViewState extends State<RoundView> {
|
||||
await _showBonusPopup(context, bonusPlayersIndices);
|
||||
}
|
||||
|
||||
LocalStorageService.saveGameSessions();
|
||||
//LocalStorageService.saveGameSessions();
|
||||
|
||||
if (context.mounted) {
|
||||
// If the game is finished, pop the context and return to the previous screen.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/active_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/mode_selection_view.dart';
|
||||
@@ -20,6 +21,12 @@ enum CreateStatus {
|
||||
noPlayerName,
|
||||
}
|
||||
|
||||
/// A view for creating a new game session in the Cabo Counter app.
|
||||
///
|
||||
/// The [CreateGameView] allows users to input a game title, select a game mode,
|
||||
/// add and reorder player names, and validate all required fields before
|
||||
/// starting a new game. It provides feedback dialogs for missing or invalid
|
||||
/// input and navigates to the active game view upon successful creation.
|
||||
class CreateGameView extends StatefulWidget {
|
||||
final GameMode gameMode;
|
||||
final String? gameTitle;
|
||||
@@ -442,26 +449,40 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
/// It then adds the game session to the game manager and navigates to the active game view.
|
||||
void _createGame() {
|
||||
var uuid = const Uuid();
|
||||
final String id = uuid.v1();
|
||||
final String gameId = uuid.v4();
|
||||
|
||||
List<String> players = [];
|
||||
// Collect player names from the text controllers.
|
||||
List<String> playerNames = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
playerNames.add(controller.text);
|
||||
}
|
||||
|
||||
// Create a list of Player objects with unique IDs and the corresponding attributes
|
||||
List<Player> playerList = [];
|
||||
for (int i = 0; i < playerNames.length; i++) {
|
||||
String playerId = uuid.v4();
|
||||
playerList.add(Player(
|
||||
playerId: playerId,
|
||||
gameId: gameId,
|
||||
name: playerNames[i],
|
||||
position: i,
|
||||
));
|
||||
}
|
||||
|
||||
bool isPointsLimitEnabled = gameMode == GameMode.pointLimit;
|
||||
|
||||
GameSession gameSession = GameSession(
|
||||
id: id,
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: players,
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
caboPenalty: ConfigService.getCaboPenalty(),
|
||||
isPointsLimitEnabled: isPointsLimitEnabled,
|
||||
);
|
||||
gameId: gameId,
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: playerList,
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
caboPenalty: ConfigService.getCaboPenalty(),
|
||||
isPointsLimitEnabled: isPointsLimitEnabled,
|
||||
isGameFinished: false);
|
||||
|
||||
gameManager.addGameSession(gameSession);
|
||||
final session = gameManager.getGameSessionById(id) ?? gameSession;
|
||||
final session = gameManager.getGameSessionById(gameId) ?? gameSession;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/db/database.dart';
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/active_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/create_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/settings_view.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/main_menu_skeleton.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/services/data_migration_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -16,6 +18,11 @@ enum PreRatingDialogDecision { yes, no, cancel }
|
||||
|
||||
enum BadRatingDialogDecision { email, cancel }
|
||||
|
||||
/// Home screen of the app that displays a list of game sessions.
|
||||
///
|
||||
/// The [MainMenuView] is the main entry point for the app's home screen.
|
||||
/// It displays a list of existing game sessions, allows users to create new games,
|
||||
/// access settings, and handles user feedback dialogs for app rating and support.
|
||||
class MainMenuView extends StatefulWidget {
|
||||
const MainMenuView({super.key});
|
||||
|
||||
@@ -26,16 +33,26 @@ class MainMenuView extends StatefulWidget {
|
||||
|
||||
class _MainMenuViewState extends State<MainMenuView> {
|
||||
bool _isLoading = true;
|
||||
late Map<String, dynamic> migrationStatus;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
LocalStorageService.loadGameSessions().then((_) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
db.gameSessionDao.getAllGameSessions().then((gameSessions) {
|
||||
for (final session in gameSessions) {
|
||||
gameManager.addGameSessionFromDataBase(session);
|
||||
}
|
||||
return Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_migrateData();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catchError((error) {
|
||||
print('[MainMenuView] $error');
|
||||
});
|
||||
gameManager.addListener(_updateView);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
precacheImage(
|
||||
@@ -44,7 +61,8 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
|
||||
if (Constants.rateMyApp.shouldOpenDialog &&
|
||||
Constants.appDevPhase != 'Beta') {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
await Future.delayed(const Duration(
|
||||
milliseconds: Constants.kMinimumSkeletonScreenDuration));
|
||||
if (!mounted) return;
|
||||
_handleFeedbackDialog(context);
|
||||
}
|
||||
@@ -107,7 +125,7 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
listenable: session,
|
||||
builder: (context, _) {
|
||||
return Dismissible(
|
||||
key: Key(session.id),
|
||||
key: Key(session.gameId),
|
||||
background: Container(
|
||||
color: CustomTheme.red,
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -123,7 +141,7 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
context, session.gameTitle);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
gameManager.removeGameSessionById(session.id);
|
||||
gameManager.deleteGameById(session.gameId);
|
||||
},
|
||||
dismissThresholds: const {
|
||||
DismissDirection.startToEnd: 0.6
|
||||
@@ -139,11 +157,13 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
visible: session.isGameFinished,
|
||||
replacement: Text(
|
||||
'${AppLocalizations.of(context).mode}: ${_translateGameMode(session)}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
style:
|
||||
const TextStyle(fontSize: 14.5),
|
||||
),
|
||||
child: Text(
|
||||
'\u{1F947} ${session.winner}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
style:
|
||||
const TextStyle(fontSize: 14.5),
|
||||
)),
|
||||
trailing: Row(
|
||||
children: [
|
||||
@@ -211,7 +231,7 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Center(child: CupertinoActivityIndicator()),
|
||||
child: const MainMenuSkeleton(),
|
||||
),
|
||||
)));
|
||||
});
|
||||
@@ -366,6 +386,57 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
BadRatingDialogDecision.cancel;
|
||||
}
|
||||
|
||||
void _migrateData() async {
|
||||
if (!ConfigService.isMigrationDone()) {
|
||||
migrationStatus = await DataMigrationService.loadOldGameData();
|
||||
final success = migrationStatus['success'] ?? 0;
|
||||
if (success == 1) {
|
||||
ConfigService.setMigrationDone(true);
|
||||
|
||||
if (mounted) {
|
||||
final int migratedGames = migrationStatus['gameCount'] ?? 0;
|
||||
await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: const Text('Migration erfolgreich'),
|
||||
content: Text(
|
||||
'$migratedGames Spiele konnten aus den gefundenen Spieldaten migriert werden.'),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (success == -1) {
|
||||
ConfigService.setMigrationDone(true);
|
||||
|
||||
if (mounted) {
|
||||
await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: const Text('Migration fehlgeschlagen'),
|
||||
content: const Text(
|
||||
'Deine alten Spieldaten konnten leider nicht migriert werden.'),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print('[MainMenuView] Data migration already completed. Skipping.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
gameManager.removeListener(_updateView);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/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/data_transfer_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';
|
||||
|
||||
/// Settings and information page for the app.
|
||||
///
|
||||
/// [SettingsView] is a settings page for the app, allowing users to configure game options,
|
||||
/// manage game data (import, export, delete), and view app information.
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@@ -151,7 +156,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
prefixIcon: CupertinoIcons.square_arrow_down,
|
||||
onPressed: () async {
|
||||
final status =
|
||||
await LocalStorageService.importJsonFile();
|
||||
await DataTransferService.importJsonFile();
|
||||
showFeedbackDialog(status);
|
||||
},
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
@@ -159,7 +164,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).export_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_up,
|
||||
onPressed: () => LocalStorageService.exportGameData(),
|
||||
onPressed: () => DataTransferService.exportGameData(),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
@@ -238,7 +243,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
onPressed: () {
|
||||
LocalStorageService.deleteAllGames();
|
||||
gameManager.deleteAllGames();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,15 @@ import 'package:cabo_counter/presentation/views/about/about_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/home/main_menu_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// A view that provides a tabbed interface for navigating between the main menu and the about section.
|
||||
/// TabBar for navigating between the main menu and about section.
|
||||
///
|
||||
/// [TabView] is a [StatefulWidget] that provides a tabbed interface for navigating
|
||||
/// between the main menu and the about section of the app. It uses a
|
||||
/// [CupertinoTabScaffold] with two tabs:
|
||||
/// - Home (MainMenuView)
|
||||
/// - About (AboutView)
|
||||
///
|
||||
/// The tab labels are provided via localization.
|
||||
class TabView extends StatefulWidget {
|
||||
const TabView({super.key});
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// A customizable button widget using Cupertino style.
|
||||
///
|
||||
/// Displays a button with a child widget and optional callback.
|
||||
/// The button uses a medium size, rounded corners, and a custom background color.
|
||||
class CustomButton extends StatelessWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@@ -2,6 +2,11 @@ import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_stepper.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// A customizable form row widget with a prefix icon, text, and optional suffix widget.
|
||||
///
|
||||
/// Displays a row with an icon and text on the left side.
|
||||
/// Optionally, a suffix widget (e.g. a stepper) can be shown on the right side.
|
||||
/// The row is styled as a [CupertinoButton] and can react to taps.
|
||||
class CustomFormRow extends StatefulWidget {
|
||||
final String prefixText;
|
||||
final IconData prefixIcon;
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart'; // Für iOS-Style
|
||||
|
||||
/// A custom stepper widget for incrementing and decrementing a value.
|
||||
///
|
||||
/// The [CustomStepper] widget allows increasing and decreasing a value
|
||||
/// within a defined range ([minValue] to [maxValue]) in fixed steps.
|
||||
///
|
||||
/// Properties:
|
||||
/// - [minValue]: The minimum value.
|
||||
/// - [maxValue]: The maximum value.
|
||||
/// - [initialValue]: The initial value (optional, defaults to [minValue]).
|
||||
/// - [step]: The step size.
|
||||
/// - [onChanged]: Callback triggered when the value changes.
|
||||
class CustomStepper extends StatefulWidget {
|
||||
final int minValue;
|
||||
final int maxValue;
|
||||
|
||||
92
lib/presentation/widgets/main_menu_skeleton.dart
Normal file
92
lib/presentation/widgets/main_menu_skeleton.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class MainMenuSkeleton extends StatelessWidget {
|
||||
const MainMenuSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: CupertinoColors.systemGrey2.withValues(alpha: 0.5),
|
||||
highlightColor: CupertinoColors.systemGrey2.withValues(alpha: 2),
|
||||
child: ListView.separated(
|
||||
itemCount: 9,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
indent: 50,
|
||||
endIndent: 50,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: CupertinoListTile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
title: Container(
|
||||
width: 170,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
subtitle: Container(
|
||||
width: 120,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Container(
|
||||
width: 15,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Container(
|
||||
width: 25,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Container(
|
||||
width: 15,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Container(
|
||||
width: 25,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/mode_selection_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// This class handles the configuration settings for the app.
|
||||
/// It uses SharedPreferences to store and retrieve the personal configuration of the app.
|
||||
/// Currently it provides methods to initialize, get, and set the point limit and cabo penalty.
|
||||
/// A service class for managing and persisting app configuration settings using `SharedPreferences`.
|
||||
///
|
||||
/// Provides methods to initialize, retrieve, update, and reset configuration values such as point limit,
|
||||
/// cabo penalty, and game mode. Ensures that user preferences are stored locally and persist across app restarts.
|
||||
class ConfigService {
|
||||
// Keys for the stored values
|
||||
static const String _keyPointLimit = 'pointLimit';
|
||||
static const String _keyCaboPenalty = 'caboPenalty';
|
||||
static const String _keyGameMode = 'gameMode';
|
||||
static const String _keyMigrationDone = 'migrationDone';
|
||||
// Actual values used in the app
|
||||
static int _pointLimit = 100;
|
||||
static int _caboPenalty = 5;
|
||||
static int _gameMode = -1;
|
||||
static bool _migrationDone = false;
|
||||
// Default values
|
||||
static const int _defaultPointLimit = 100;
|
||||
static const int _defaultCaboPenalty = 5;
|
||||
static const int _defaultGameMode = -1;
|
||||
static const bool _defaultMigrationDone = false;
|
||||
|
||||
static Future<void> initConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -25,11 +30,13 @@ class ConfigService {
|
||||
_pointLimit = prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
|
||||
_caboPenalty = prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
|
||||
_gameMode = prefs.getInt(_keyGameMode) ?? _defaultGameMode;
|
||||
_migrationDone = prefs.getBool(_keyMigrationDone) ?? _defaultMigrationDone;
|
||||
|
||||
// Save the initial values to SharedPreferences
|
||||
prefs.setInt(_keyPointLimit, _pointLimit);
|
||||
prefs.setInt(_keyCaboPenalty, _caboPenalty);
|
||||
prefs.setInt(_keyGameMode, _gameMode);
|
||||
prefs.setBool(_keyMigrationDone, _migrationDone);
|
||||
}
|
||||
|
||||
/// Retrieves the current game mode.
|
||||
@@ -97,6 +104,16 @@ class ConfigService {
|
||||
_caboPenalty = newCaboPenalty;
|
||||
}
|
||||
|
||||
static bool isMigrationDone() => _migrationDone;
|
||||
|
||||
/// Setter for the migration done flag.
|
||||
/// [done] is the new value to be set.
|
||||
static Future<void> setMigrationDone(bool done) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_keyMigrationDone, done);
|
||||
_migrationDone = done;
|
||||
}
|
||||
|
||||
/// Resets the configuration to default values.
|
||||
static Future<void> resetConfig() async {
|
||||
ConfigService._pointLimit = _defaultPointLimit;
|
||||
|
||||
158
lib/services/data_migration_service.dart
Normal file
158
lib/services/data_migration_service.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/services/data_transfer_service.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class DataMigrationService {
|
||||
static const String _fileName = 'game_data.json';
|
||||
|
||||
/// Returns the path to the local JSON file.
|
||||
static Future<File> _getFilePath() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final path = '${directory.path}/$_fileName';
|
||||
print(path);
|
||||
return File(path);
|
||||
}
|
||||
|
||||
/// Loads old game data from a local JSON file, migrates it to the new format,
|
||||
/// validates it, and adds it to the game manager.
|
||||
/// Returns a map with the result of the operation:
|
||||
/// - 'success': 1 if successful, 0 if no file found, -1 if error
|
||||
/// - 'gameCount': number of games loaded (only if successful)
|
||||
/// If no file is found, returns {'success': 0}.
|
||||
/// If an error occurs, returns {'success': -1}.
|
||||
static Future<Map<String, dynamic>> loadOldGameData() async {
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
|
||||
if (!await file.exists()) {
|
||||
return {'success': 0};
|
||||
}
|
||||
|
||||
final jsonString = await file.readAsString();
|
||||
if (jsonString.isEmpty) {
|
||||
return {'success': -1};
|
||||
}
|
||||
final migratedJsonString = _migrateJsonData(jsonString);
|
||||
if (!await DataTransferService.validateJsonSchema(
|
||||
migratedJsonString, true)) {
|
||||
return {'success': -1};
|
||||
}
|
||||
|
||||
final jsonList = json.decode(migratedJsonString) as List<dynamic>;
|
||||
|
||||
final gameList = jsonList
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
for (GameSession session in gameList) {
|
||||
if (gameManager.gameExistsInGameList(session.gameId)) {
|
||||
gameManager.deleteGameById(session.gameId);
|
||||
}
|
||||
gameManager.addGameSession(session);
|
||||
}
|
||||
await _deleteOldGameDataFile();
|
||||
return {'success': 1, 'gameCount': gameList.length};
|
||||
} catch (e, stack) {
|
||||
print('$e\n$stack');
|
||||
return {'success': -1};
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the old game data file after successful migration.
|
||||
static Future<void> _deleteOldGameDataFile() async {
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print('$e\n$stack');
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates old JSON data to the new format.
|
||||
/// This function assumes the old format has:
|
||||
/// - A list of player names in 'players'
|
||||
/// - A list of player scores in 'playerScores'
|
||||
/// - A list of rounds in 'roundList' without unique IDs
|
||||
/// The new format will have:
|
||||
/// - A list of player objects with unique IDs and additional fields
|
||||
/// - Each round will have a unique 'roundId' and 'gameId'
|
||||
/// - The 'playerScores' field will be removed
|
||||
/// Returns the migrated JSON string.
|
||||
static String _migrateJsonData(String jsonString) {
|
||||
final List<dynamic> oldGameList = jsonDecode(jsonString);
|
||||
|
||||
const uuid = Uuid();
|
||||
final migrated = oldGameList.map((game) {
|
||||
final gameId = game['id'] ?? uuid.v4();
|
||||
final players = _migratePlayers(game, gameId);
|
||||
final migratedRounds = _migrateRounds(game, gameId);
|
||||
|
||||
final newGame = Map<String, dynamic>.from(game);
|
||||
newGame['players'] = players;
|
||||
newGame['roundList'] = migratedRounds;
|
||||
newGame.remove('playerScores');
|
||||
return newGame;
|
||||
}).toList();
|
||||
|
||||
final migratedJsonString =
|
||||
const JsonEncoder.withIndent(' ').convert(migrated);
|
||||
|
||||
return migratedJsonString;
|
||||
}
|
||||
|
||||
/// Migrates old player data to include unique IDs and gameId.
|
||||
/// If a player already has a 'playerId' and 'gameId', they are
|
||||
/// kept. Otherwise, new IDs are generated.
|
||||
/// Returns the list of migrated players.
|
||||
static List<Map<String, Object>> _migratePlayers(
|
||||
dynamic game, String gameId) {
|
||||
const uuid = Uuid();
|
||||
final playerNames = List<String>.from(game['players']);
|
||||
final playerScores = List<int>.from(game['playerScores']);
|
||||
|
||||
final players = List.generate(
|
||||
playerNames.length,
|
||||
(i) => {
|
||||
'playerId': uuid.v4(),
|
||||
'gameId': gameId,
|
||||
'name': playerNames[i],
|
||||
'position': i,
|
||||
'totalScore': playerScores[i],
|
||||
},
|
||||
);
|
||||
return players;
|
||||
}
|
||||
|
||||
/// Migrates old rounds to include unique IDs and gameId.
|
||||
/// If a round already has a 'roundId' and 'gameId', they are kept.
|
||||
/// Otherwise, new IDs are generated.
|
||||
/// Returns the list of migrated rounds.
|
||||
static List<Map<String, dynamic>> _migrateRounds(
|
||||
dynamic game, String gameId) {
|
||||
const uuid = Uuid();
|
||||
final oldRounds = (game['roundList'] as List<dynamic>? ?? []);
|
||||
|
||||
final migratedRounds = oldRounds.map((round) {
|
||||
final newRound = Map<String, dynamic>.from(round ?? {});
|
||||
newRound['roundId'] = (newRound['roundId'] is String &&
|
||||
(newRound['roundId'] as String).isNotEmpty)
|
||||
? newRound['roundId']
|
||||
: uuid.v4();
|
||||
newRound['gameId'] = (newRound['gameId'] is String &&
|
||||
(newRound['gameId'] as String).isNotEmpty)
|
||||
? newRound['gameId']
|
||||
: gameId;
|
||||
return newRound;
|
||||
}).toList();
|
||||
|
||||
return migratedRounds;
|
||||
}
|
||||
}
|
||||
155
lib/services/data_transfer_service.dart
Normal file
155
lib/services/data_transfer_service.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cabo_counter/data/dto/game_manager.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:json_schema/json_schema.dart';
|
||||
|
||||
enum ImportStatus {
|
||||
success,
|
||||
canceled,
|
||||
validationError,
|
||||
formatError,
|
||||
genericError
|
||||
}
|
||||
|
||||
class DataTransferService {
|
||||
/// Writes the game session list to a JSON file and returns it as string.
|
||||
static String _getGameDataAsJsonFile() {
|
||||
final jsonFile =
|
||||
gameManager.gameList.map((session) => session.toJson()).toList();
|
||||
return json.encode(jsonFile);
|
||||
}
|
||||
|
||||
/// Opens the file picker to export game data as a JSON file.
|
||||
/// This method will export the given [jsonString] as a JSON file. It opens
|
||||
/// the file picker with the choosen [fileName].
|
||||
static Future<bool> _exportJsonData(
|
||||
String jsonString,
|
||||
String fileName,
|
||||
) async {
|
||||
try {
|
||||
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
await FileSaver.instance.saveAs(
|
||||
name: fileName,
|
||||
bytes: bytes,
|
||||
ext: 'json',
|
||||
mimeType: MimeType.json,
|
||||
);
|
||||
return true;
|
||||
} catch (e, stack) {
|
||||
print('[DataTransferService] $e');
|
||||
print(stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to export all game sessions as a JSON file.
|
||||
static Future<bool> exportGameData() async {
|
||||
String jsonString = _getGameDataAsJsonFile();
|
||||
String fileName = 'cabo_counter-game_data';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to save a single game session as a JSON file.
|
||||
static Future<bool> exportSingleGameSession(GameSession session) async {
|
||||
String jsonString = json.encode(session.toJson());
|
||||
String fileName = 'cabo_counter-game_${session.gameId.substring(0, 7)}';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to import a JSON file and loads the game data from it.
|
||||
static Future<ImportStatus> importJsonFile() async {
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
|
||||
if (path == null) {
|
||||
return ImportStatus.canceled;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonString = await _readFileContent(path.files.single);
|
||||
|
||||
// Checks if the JSON String is in the gameList format
|
||||
if (await validateJsonSchema(jsonString, true)) {
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
List<GameSession> importedList = jsonData
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
for (GameSession s in importedList) {
|
||||
_importSession(s);
|
||||
}
|
||||
} else if (await validateJsonSchema(jsonString, false)) {
|
||||
// Checks if the JSON String is in the single game format
|
||||
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
|
||||
_importSession(GameSession.fromJson(jsonData));
|
||||
} else {
|
||||
return ImportStatus.validationError;
|
||||
}
|
||||
|
||||
return ImportStatus.success;
|
||||
} on FormatException catch (e, stack) {
|
||||
print('[DataTransferService] $e');
|
||||
print(stack);
|
||||
return ImportStatus.formatError;
|
||||
} on Exception catch (e, stack) {
|
||||
print('[DataTransferService] $e');
|
||||
print(stack);
|
||||
return ImportStatus.genericError;
|
||||
}
|
||||
}
|
||||
|
||||
/// Imports a single game session into the gameList.
|
||||
static Future<void> _importSession(GameSession session) async {
|
||||
if (gameManager.gameExistsInGameList(session.gameId)) {
|
||||
gameManager.deleteGameById(session.gameId);
|
||||
}
|
||||
gameManager.addGameSession(session);
|
||||
}
|
||||
|
||||
/// Helper method to read file content from either bytes or path
|
||||
static Future<String> _readFileContent(PlatformFile file) async {
|
||||
if (file.bytes != null) return utf8.decode(file.bytes!);
|
||||
if (file.path != null) return await File(file.path!).readAsString();
|
||||
|
||||
throw Exception('Die Datei hat keinen lesbaren Inhalt');
|
||||
}
|
||||
|
||||
/// Validates the JSON data against the schema.
|
||||
/// This method checks if the provided [jsonString] is valid against the
|
||||
/// JSON schema. It takes a boolean [isGameList] to determine
|
||||
/// which schema to use (game list or single game).
|
||||
static Future<bool> validateJsonSchema(
|
||||
String jsonString, bool isGameList) async {
|
||||
final String schemaString;
|
||||
|
||||
if (isGameList) {
|
||||
schemaString =
|
||||
await rootBundle.loadString('assets/game_list-schema.json');
|
||||
} else {
|
||||
schemaString = await rootBundle.loadString('assets/game-schema.json');
|
||||
}
|
||||
|
||||
try {
|
||||
final schema = JsonSchema.create(json.decode(schemaString));
|
||||
final jsonData = json.decode(jsonString);
|
||||
final result = schema.validate(jsonData);
|
||||
|
||||
if (result.isValid) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e, stack) {
|
||||
print('[DataTransferService] $e');
|
||||
print(stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:json_schema/json_schema.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
enum ImportStatus {
|
||||
success,
|
||||
canceled,
|
||||
validationError,
|
||||
formatError,
|
||||
genericError
|
||||
}
|
||||
|
||||
class LocalStorageService {
|
||||
static const String _fileName = 'game_data.json';
|
||||
|
||||
/// Writes the game session list to a JSON file and returns it as string.
|
||||
static String _getGameDataAsJsonFile() {
|
||||
final jsonFile =
|
||||
gameManager.gameList.map((session) => session.toJson()).toList();
|
||||
return json.encode(jsonFile);
|
||||
}
|
||||
|
||||
/// Returns the path to the local JSON file.
|
||||
static Future<File> _getFilePath() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final path = '${directory.path}/$_fileName';
|
||||
return File(path);
|
||||
}
|
||||
|
||||
/// Saves the game sessions to a local JSON file.
|
||||
static Future<void> saveGameSessions() async {
|
||||
print('[local_storage_service.dart] Versuche, Daten zu speichern...');
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
final jsonFile = _getGameDataAsJsonFile();
|
||||
await file.writeAsString(jsonFile);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden zwischengespeichert.');
|
||||
} catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Speichern der Spieldaten. Exception: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the game data from a local JSON file.
|
||||
static Future<bool> loadGameSessions() async {
|
||||
print('[local_storage_service.dart] Versuche, Daten zu laden...');
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
|
||||
if (!await file.exists()) {
|
||||
print(
|
||||
'[local_storage_service.dart] Es existiert noch keine Datei mit Spieldaten');
|
||||
return false;
|
||||
}
|
||||
|
||||
print(
|
||||
'[local_storage_service.dart] Es existiert bereits eine Datei mit Spieldaten');
|
||||
final jsonString = await file.readAsString();
|
||||
|
||||
if (jsonString.isEmpty) {
|
||||
print('[local_storage_service.dart] Die gefundene Datei ist leer');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await _validateJsonSchema(jsonString, true)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei konnte nicht validiert werden');
|
||||
gameManager.gameList = [];
|
||||
return false;
|
||||
}
|
||||
print('[local_storage_service.dart] Die gefundene Datei hat Inhalt');
|
||||
print(
|
||||
'[local_storage_service.dart] Die gefundene Datei wurde erfolgreich validiert');
|
||||
final jsonList = json.decode(jsonString) as List<dynamic>;
|
||||
|
||||
gameManager.gameList = jsonList
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
for (GameSession session in gameManager.gameList) {
|
||||
print(
|
||||
'[local_storage_service.dart] Geladene Session: ${session.gameTitle} - ${session.id}');
|
||||
}
|
||||
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden erfolgreich geladen und verarbeitet');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Laden der Spieldaten:\n$e');
|
||||
gameManager.gameList = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to export game data as a JSON file.
|
||||
/// This method will export the given [jsonString] as a JSON file. It opens
|
||||
/// the file picker with the choosen [fileName].
|
||||
static Future<bool> _exportJsonData(
|
||||
String jsonString,
|
||||
String fileName,
|
||||
) async {
|
||||
try {
|
||||
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
final path = await FileSaver.instance.saveAs(
|
||||
name: fileName,
|
||||
bytes: bytes,
|
||||
ext: 'json',
|
||||
mimeType: MimeType.json,
|
||||
);
|
||||
if (path == null) {
|
||||
print('[local_storage_service.dart]: Export abgebrochen');
|
||||
} else {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden exportiert. Dateipfad: $path');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Exportieren der Spieldaten. Exception: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to export all game sessions as a JSON file.
|
||||
static Future<bool> exportGameData() async {
|
||||
String jsonString = _getGameDataAsJsonFile();
|
||||
String fileName = 'cabo_counter-game_data';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to save a single game session as a JSON file.
|
||||
static Future<bool> exportSingleGameSession(GameSession session) async {
|
||||
String jsonString = json.encode(session.toJson());
|
||||
String fileName = 'cabo_counter-game_${session.id.substring(0, 7)}';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to import a JSON file and loads the game data from it.
|
||||
static Future<ImportStatus> importJsonFile() async {
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
dialogTitle: 'Wähle eine Datei mit Spieldaten aus',
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
|
||||
if (path == null) {
|
||||
print(
|
||||
'[local_storage_service.dart] Der Filepicker-Dialog wurde abgebrochen');
|
||||
return ImportStatus.canceled;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonString = await _readFileContent(path.files.single);
|
||||
|
||||
if (await _validateJsonSchema(jsonString, true)) {
|
||||
// Checks if the JSON String is in the gameList format
|
||||
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
List<GameSession> importedList = jsonData
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
for (GameSession s in importedList) {
|
||||
_importSession(s);
|
||||
}
|
||||
} else if (await _validateJsonSchema(jsonString, false)) {
|
||||
// Checks if the JSON String is in the single game format
|
||||
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
|
||||
_importSession(GameSession.fromJson(jsonData));
|
||||
} else {
|
||||
return ImportStatus.validationError;
|
||||
}
|
||||
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei wurde erfolgreich Importiert');
|
||||
await saveGameSessions();
|
||||
return ImportStatus.success;
|
||||
} on FormatException catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Ungültiges JSON-Format. Exception: $e');
|
||||
return ImportStatus.formatError;
|
||||
} on Exception catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Dateizugriff. Exception: $e');
|
||||
return ImportStatus.genericError;
|
||||
}
|
||||
}
|
||||
|
||||
/// Imports a single game session into the gameList.
|
||||
static Future<void> _importSession(GameSession session) async {
|
||||
if (gameManager.gameExistsInGameList(session.id)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Session mit der ID ${session.id} existiert bereits. Sie wird überschrieben.');
|
||||
gameManager.removeGameSessionById(session.id);
|
||||
}
|
||||
gameManager.addGameSession(session);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Session mit der ID ${session.id} wurde erfolgreich importiert.');
|
||||
}
|
||||
|
||||
/// Helper method to read file content from either bytes or path
|
||||
static Future<String> _readFileContent(PlatformFile file) async {
|
||||
if (file.bytes != null) return utf8.decode(file.bytes!);
|
||||
if (file.path != null) return await File(file.path!).readAsString();
|
||||
|
||||
throw Exception('Die Datei hat keinen lesbaren Inhalt');
|
||||
}
|
||||
|
||||
/// Validates the JSON data against the schema.
|
||||
/// This method checks if the provided [jsonString] is valid against the
|
||||
/// JSON schema. It takes a boolean [isGameList] to determine
|
||||
/// which schema to use (game list or single game).
|
||||
static Future<bool> _validateJsonSchema(
|
||||
String jsonString, bool isGameList) async {
|
||||
final String schemaString;
|
||||
|
||||
if (isGameList) {
|
||||
schemaString =
|
||||
await rootBundle.loadString('assets/game_list-schema.json');
|
||||
} else {
|
||||
schemaString = await rootBundle.loadString('assets/game-schema.json');
|
||||
}
|
||||
|
||||
try {
|
||||
final schema = JsonSchema.create(json.decode(schemaString));
|
||||
final jsonData = json.decode(jsonString);
|
||||
final result = schema.validate(jsonData);
|
||||
|
||||
if (result.isValid) {
|
||||
print(
|
||||
'[local_storage_service.dart] JSON ist erfolgreich validiert. Typ: ${isGameList ? 'Game List' : 'Single Game'}');
|
||||
return true;
|
||||
}
|
||||
print(
|
||||
'[local_storage_service.dart] JSON ist nicht gültig.\nFehler: ${result.errors}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Validieren des JSON-Schemas: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> deleteAllGames() async {
|
||||
try {
|
||||
gameManager.gameList.clear();
|
||||
await saveGameSessions();
|
||||
print(
|
||||
'[local_storage_service.dart] Alle Runden wurden erfolgreich gelöscht.');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Löschen aller Runden: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: cabo_counter
|
||||
description: "Mobile app for the card game Cabo"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.5.8+667
|
||||
version: 0.6.2+792
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -33,12 +33,18 @@ dependencies:
|
||||
confetti: ^0.6.0
|
||||
flutter_oss_licenses: ^2.0.1
|
||||
google_fonts: ^6.3.0
|
||||
drift: ^2.26.1
|
||||
drift_flutter: ^0.2.4
|
||||
provider: ^6.1.5+1
|
||||
shimmer: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
test: ^1.25.15
|
||||
drift_dev: ^2.26.1
|
||||
build_runner: ^2.4.15
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
@@ -1,29 +1,51 @@
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/game_session.dart';
|
||||
import 'package:cabo_counter/data/dto/player.dart';
|
||||
import 'package:flutter_test/flutter_test.dart' as flutter_test;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
flutter_test.TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late GameSession session;
|
||||
final testPlayers = ['Alice', 'Bob', 'Charlie'];
|
||||
final testPlayers = [
|
||||
Player(
|
||||
name: 'Alice',
|
||||
totalScore: 0,
|
||||
playerId: '0',
|
||||
gameId: 'abc',
|
||||
position: 0),
|
||||
Player(
|
||||
name: 'Bobby',
|
||||
totalScore: 0,
|
||||
playerId: '1',
|
||||
gameId: 'abc',
|
||||
position: 1),
|
||||
Player(
|
||||
name: 'Charlie',
|
||||
totalScore: 0,
|
||||
playerId: '2',
|
||||
gameId: 'abc',
|
||||
position: 2)
|
||||
];
|
||||
final testDate = DateTime(2023, 1, 1);
|
||||
const testTitle = 'Test Game';
|
||||
|
||||
setUp(() {
|
||||
session = GameSession(
|
||||
id: '1',
|
||||
createdAt: testDate,
|
||||
gameTitle: testTitle,
|
||||
players: testPlayers,
|
||||
pointLimit: 100,
|
||||
caboPenalty: 5,
|
||||
isPointsLimitEnabled: true,
|
||||
);
|
||||
gameId: '1',
|
||||
createdAt: testDate,
|
||||
gameTitle: testTitle,
|
||||
players: testPlayers,
|
||||
pointLimit: 100,
|
||||
caboPenalty: 5,
|
||||
isPointsLimitEnabled: true,
|
||||
isGameFinished: false);
|
||||
});
|
||||
|
||||
group('Initialization & JSON', () {
|
||||
test('Initialization', () {
|
||||
expect(session.gameTitle, testTitle);
|
||||
expect(session.players, testPlayers);
|
||||
expect(session.playerScores, [0, 0, 0]);
|
||||
expect(session.getPlayerScoresAsList(), [0, 0, 0]);
|
||||
expect(session.roundNumber, 1);
|
||||
expect(session.isGameFinished, isFalse);
|
||||
expect(session.winner, isEmpty);
|
||||
@@ -34,8 +56,8 @@ void main() {
|
||||
session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
|
||||
session.addRoundScoresToList(2, [15, 25, 35], [5, 5, 5], 1);
|
||||
|
||||
final json = session.toJson();
|
||||
final fromJsonSession = GameSession.fromJson(json);
|
||||
final jsonFile = session.toJson();
|
||||
final fromJsonSession = GameSession.fromJson(jsonFile);
|
||||
|
||||
expect(fromJsonSession.gameTitle, testTitle);
|
||||
expect(fromJsonSession.players, testPlayers);
|
||||
@@ -47,7 +69,7 @@ void main() {
|
||||
() => GameSession.fromJson({
|
||||
'createdAt': testDate.toIso8601String(),
|
||||
'gameTitle': null, // Invalid
|
||||
'players': testPlayers,
|
||||
'players': session.players.map((p) => p.toJson()).toList(),
|
||||
'pointLimit': 100,
|
||||
'caboPenalty': 50,
|
||||
'isPointsLimitEnabled': true,
|
||||
@@ -62,10 +84,6 @@ void main() {
|
||||
});
|
||||
|
||||
group('Helper Functions', () {
|
||||
test('getMaxLengthOfPlayerNames', () {
|
||||
expect(session.getMaxLengthOfPlayerNames(), equals(7)); // Charlie (7)
|
||||
});
|
||||
|
||||
test('increaseRound', () {
|
||||
expect(session.roundNumber, 1);
|
||||
session.increaseRound();
|
||||
@@ -145,14 +163,14 @@ void main() {
|
||||
session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
|
||||
session.addRoundScoresToList(2, [5, 5, 5], [5, 5, 5], 1);
|
||||
session.testingSumPoints();
|
||||
expect(session.playerScores, [15, 25, 35]);
|
||||
expect(session.getPlayerScoresAsList(), [15, 25, 35]);
|
||||
});
|
||||
|
||||
test('_checkHundredPointsReached via updatePoints', () {
|
||||
session.addRoundScoresToList(1, [50, 5, 15], [50, 0, 15], 1);
|
||||
session.addRoundScoresToList(2, [50, 5, 15], [50, 0, 15], 1);
|
||||
session.updatePoints();
|
||||
expect(session.playerScores, equals([50, 0, 30]));
|
||||
expect(session.getPlayerScoresAsList(), equals([50, 0, 30]));
|
||||
});
|
||||
|
||||
test('_setWinner via updatePoints', () {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:cabo_counter/data/round.dart';
|
||||
import 'package:cabo_counter/data/dto/round.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
@@ -8,9 +8,13 @@ void main() {
|
||||
const testKamikazePlayerIndex = 1;
|
||||
const testScores = [10, 20, 30];
|
||||
const testScoreUpdates = [5, 15, 25];
|
||||
const testRoundId = 'testRoundId';
|
||||
const testGameId = 'testGameId';
|
||||
|
||||
setUp(() {
|
||||
round = Round(
|
||||
roundId: 'testRoundId',
|
||||
gameId: 'testGameId',
|
||||
roundNum: testRoundNum,
|
||||
caboPlayerIndex: testCaboPlayerIndex,
|
||||
kamikazePlayerIndex: testKamikazePlayerIndex,
|
||||
@@ -30,6 +34,8 @@ void main() {
|
||||
|
||||
test('Constructor with null kamikazePlayerIndex', () {
|
||||
final roundWithoutKamikaze = Round(
|
||||
roundId: testRoundId,
|
||||
gameId: testGameId,
|
||||
roundNum: testRoundNum,
|
||||
caboPlayerIndex: testCaboPlayerIndex,
|
||||
kamikazePlayerIndex: null,
|
||||
@@ -45,6 +51,8 @@ void main() {
|
||||
test('toJson() returns correct map', () {
|
||||
final jsonMap = round.toJson();
|
||||
|
||||
expect(jsonMap['roundId'], equals(testRoundId));
|
||||
expect(jsonMap['gameId'], equals(testGameId));
|
||||
expect(jsonMap['roundNum'], equals(testRoundNum));
|
||||
expect(jsonMap['caboPlayerIndex'], equals(testCaboPlayerIndex));
|
||||
expect(jsonMap['kamikazePlayerIndex'], equals(testKamikazePlayerIndex));
|
||||
@@ -54,6 +62,8 @@ void main() {
|
||||
|
||||
test('fromJson() creates correct Round object', () {
|
||||
final jsonMap = {
|
||||
'roundId': testRoundId,
|
||||
'gameId': testGameId,
|
||||
'roundNum': testRoundNum,
|
||||
'caboPlayerIndex': testCaboPlayerIndex,
|
||||
'kamikazePlayerIndex': testKamikazePlayerIndex,
|
||||
@@ -72,6 +82,8 @@ void main() {
|
||||
|
||||
test('fromJson() with null kamikazePlayerIndex', () {
|
||||
final jsonMap = {
|
||||
'roundId': testRoundId,
|
||||
'gameId': testGameId,
|
||||
'roundNum': testRoundNum,
|
||||
'caboPlayerIndex': testCaboPlayerIndex,
|
||||
'kamikazePlayerIndex': null,
|
||||
@@ -98,6 +110,8 @@ void main() {
|
||||
|
||||
test('toString() with null kamikazePlayerIndex', () {
|
||||
final roundWithoutKamikaze = Round(
|
||||
roundId: 'testRoundId',
|
||||
gameId: 'testGameId',
|
||||
roundNum: testRoundNum,
|
||||
caboPlayerIndex: testCaboPlayerIndex,
|
||||
kamikazePlayerIndex: null,
|
||||
|
||||
Reference in New Issue
Block a user