Merge branch 'develop' into feature/60-implement-drift-database
# Conflicts: # lib/views/active_game_view.dart # lib/views/create_game_view.dart # lib/views/graph_view.dart # lib/views/main_menu_view.dart # lib/views/round_view.dart # pubspec.yaml
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: always()
|
||||
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,7 +107,7 @@ jobs:
|
||||
git push
|
||||
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs: [lint, format]
|
||||
|
||||
@@ -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
|
||||
45
README.md
45
README.md
@@ -1,6 +1,6 @@
|
||||
# CABO Counter
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -12,25 +12,29 @@ A mobile score tracker for the card game Cabo, helping players effortlessly mana
|
||||
|
||||
## 📱 Description
|
||||
|
||||
Cabo Counter is an intuitive Flutter-based mobile application designed to enhance your CABO card game experience. It eliminates manual scorekeeping by automatically calculating points per round.
|
||||
Cabo Counter is an intuitive Flutter-based mobile application designed to enhance your CABO card game experience. It eliminates manual scorekeeping by automatically calculating points per round.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🆕 Create new games with customizable rules
|
||||
- 👥 Support for 2-5 players
|
||||
- ⚖️ Two game modes:
|
||||
- **100 Points Mode** (Standard)
|
||||
- **Infinite Mode** (Casual play)
|
||||
- **Point Limit Mode**: Play until a certain point limit is reached
|
||||
- **Unlimited Mode**: Play without an limit and end the round at any point
|
||||
- 🔢 Automatic score calculation with:
|
||||
- Falsly calling Cabo
|
||||
- Exact 100-point bonus (score halving)
|
||||
- Kamikaze rule handling
|
||||
- Exact 100-point bonus (score halving)
|
||||
- 📊 Round history tracking
|
||||
- 📊 Round history tracking via graph and table
|
||||
- 🎨 Customizable
|
||||
- Change the default settings for point limits and cabo penaltys
|
||||
- Choose a default game mode for every new created game
|
||||
- 💿 Im- and exporting certain games or the whole app data
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Flutter 3.24.5+
|
||||
- Dart 3.5.4+
|
||||
- Flutter 3.32.1+
|
||||
- Dart 3.8.1+
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -43,18 +47,22 @@ flutter run
|
||||
|
||||
## 🎮 Usage
|
||||
|
||||
1. **Start New Game**
|
||||
- Choose game mode (100 Points or Infinite)
|
||||
1. **Start a new game**
|
||||
- Click the "+"-Button
|
||||
- Choose a game title and a game mode
|
||||
- Add 2-5 players
|
||||
|
||||
2. **Gameplay**
|
||||
- Track rounds with automatic scoring
|
||||
- Handle special rules (Kamikaze, exact 100 points)
|
||||
- View real-time standings
|
||||
- Open the first round
|
||||
- Choose the player who called Cabo
|
||||
- Enter the points of every player
|
||||
- If given: Choose a Kamikaze player
|
||||
- Navigate to the next round or back to the overview
|
||||
- Let the app calculate all points for you
|
||||
|
||||
3. **Round Management**
|
||||
- Automatic winner detection
|
||||
- Penalty point calculation
|
||||
3. **Statistics**
|
||||
- View the progress graph for the game
|
||||
- Get a detailed table overview for every points made or lost
|
||||
- Game-over detection (100 Points mode)
|
||||
|
||||
## 🃏 Key Rules Overview
|
||||
@@ -67,7 +75,8 @@ flutter run
|
||||
- Exact 100 points: Score halved
|
||||
|
||||
### Game End
|
||||
- First player ≥101 points triggers final scoring
|
||||
- First player ≥100 points triggers final scoring
|
||||
- In unlimited mode you can end the game via the End Game Button
|
||||
- Lowest total score wins
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -11,6 +11,3 @@ linter:
|
||||
prefer_const_literals_to_create_immutables: true
|
||||
unnecessary_const: true
|
||||
lines_longer_than_80_chars: false
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
291
assets/game-schema.json
Normal file
291
assets/game-schema.json
Normal file
@@ -0,0 +1,291 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Generated schema for a single cabo counter game",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"gameTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPenalty": {
|
||||
"type": "integer"
|
||||
},
|
||||
"isPointsLimitEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isGameFinished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"winner": {
|
||||
"type": "string"
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"gameTitle",
|
||||
"players",
|
||||
"pointLimit",
|
||||
"caboPenalty",
|
||||
"isPointsLimitEnabled",
|
||||
"isGameFinished",
|
||||
"winner",
|
||||
"roundNumber",
|
||||
"playerScores",
|
||||
"roundList"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Generated schema for cabo game data",
|
||||
"title": "Generated schema for the cabo counter game data",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -1,5 +1,6 @@
|
||||
arb-dir: lib/l10n
|
||||
arb-dir: lib/l10n/arb
|
||||
template-arb-file: app_de.arb
|
||||
untranslated-messages-file: lib/l10n/untranslated_messages.json
|
||||
untranslated-messages-file: lib/l10n/arb/untranslated_messages.json
|
||||
nullable-getter: false
|
||||
output-localization-file: app_localizations.dart
|
||||
output-localization-file: app_localizations.dart
|
||||
output-dir: lib/l10n/generated
|
||||
57
lib/core/constants.dart
Normal file
57
lib/core/constants.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
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';
|
||||
|
||||
/// 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,
|
||||
remindDays: 45,
|
||||
minLaunches: 15,
|
||||
remindLaunches: 40);
|
||||
|
||||
/// Delay in milliseconds before a pop-up appears.
|
||||
static const int kPopUpDelay = 300;
|
||||
|
||||
/// Delay in milliseconds before the round view appears after the previous one is closed.
|
||||
static const int kRoundViewDelay = 600;
|
||||
|
||||
/// Duration in milliseconds for the fade-in animation of texts.
|
||||
static const int kFadeInDuration = 300;
|
||||
|
||||
/// Duration in milliseconds for the keyboard to fully disappear.
|
||||
static const int kKeyboardDelay = 300;
|
||||
}
|
||||
69
lib/core/custom_theme.dart
Normal file
69
lib/core/custom_theme.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
static const Color graphColor1 = Color(0xFFF44336);
|
||||
static const Color graphColor2 = Color(0xFF2196F3);
|
||||
static const Color graphColor3 = Color(0xFFFFA726);
|
||||
static const Color graphColor4 = Color(0xFF9C27B0);
|
||||
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,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cabo_counter/data/models/game_session.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class GameManager extends ChangeNotifier {
|
||||
@@ -10,32 +11,68 @@ class GameManager extends ChangeNotifier {
|
||||
/// 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.
|
||||
/// Returns the index of the newly added session in the sorted list.
|
||||
Future<int> addGameSession(GameSession session) async {
|
||||
int addGameSession(GameSession session) {
|
||||
session.addListener(() {
|
||||
notifyListeners(); // Propagate session changes
|
||||
});
|
||||
gameList.add(session);
|
||||
print(
|
||||
'[game_manager.dart] Added game session: ${session.gameTitle} at ${session.createdAt}');
|
||||
gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
print(
|
||||
'[game_manager.dart] Sorted game sessions by creation date. Total sessions: ${gameList.length}');
|
||||
notifyListeners();
|
||||
await LocalStorageService.saveGameSessions();
|
||||
print('[game_manager.dart] Saved game sessions to local storage.');
|
||||
LocalStorageService.saveGameSessions();
|
||||
return gameList.indexOf(session);
|
||||
}
|
||||
|
||||
/// 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 removeGameSession(int index) {
|
||||
void removeGameSessionByIndex(int index) {
|
||||
gameList[index].removeListener(notifyListeners);
|
||||
gameList.removeAt(index);
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Ends a game session if its in unlimited mode.
|
||||
/// Takes a String [id] as input. It finds the index of the game
|
||||
/// session with the matching ID marks it as finished,
|
||||
void endGame(String id) {
|
||||
final int index =
|
||||
gameList.indexWhere((session) => session.id.toString() == id);
|
||||
|
||||
// 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();
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
}
|
||||
}
|
||||
|
||||
final gameManager = GameManager();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cabo_counter/data/models/round.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// This class represents a game session for Cabo game.
|
||||
/// [createdAt] is the timestamp of when the game session was created.
|
||||
@@ -12,6 +13,7 @@ import 'package:flutter/cupertino.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 DateTime createdAt;
|
||||
final String gameTitle;
|
||||
final List<String> players;
|
||||
@@ -25,6 +27,7 @@ class GameSession extends ChangeNotifier {
|
||||
List<Round> roundList = [];
|
||||
|
||||
GameSession({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.gameTitle,
|
||||
required this.players,
|
||||
@@ -37,13 +40,14 @@ class GameSession extends ChangeNotifier {
|
||||
|
||||
@override
|
||||
toString() {
|
||||
return ('GameSession: [createdAt: $createdAt, gameTitle: $gameTitle, '
|
||||
return ('GameSession: [id: $id, createdAt: $createdAt, gameTitle: $gameTitle, '
|
||||
'isPointsLimitEnabled: $isPointsLimitEnabled, pointLimit: $pointLimit, caboPenalty: $caboPenalty,'
|
||||
' players: $players, playerScores: $playerScores, roundList: $roundList, winner: $winner]');
|
||||
}
|
||||
|
||||
/// Converts the GameSession object to a JSON map.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'gameTitle': gameTitle,
|
||||
'players': players,
|
||||
@@ -59,7 +63,8 @@ class GameSession extends ChangeNotifier {
|
||||
|
||||
/// Creates a GameSession object from a JSON map.
|
||||
GameSession.fromJson(Map<String, dynamic> json)
|
||||
: createdAt = DateTime.parse(json['createdAt']),
|
||||
: id = json['id'] ?? const Uuid().v1(),
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
gameTitle = json['gameTitle'],
|
||||
players = List<String>.from(json['players']),
|
||||
pointLimit = json['pointLimit'],
|
||||
@@ -72,15 +77,6 @@ class GameSession extends ChangeNotifier {
|
||||
roundList =
|
||||
(json['roundList'] as List).map((e) => Round.fromJson(e)).toList();
|
||||
|
||||
/// Returns the length of all player names combined.
|
||||
int getLengthOfPlayerNames() {
|
||||
int length = 0;
|
||||
for (String player in players) {
|
||||
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) {
|
||||
@@ -227,7 +223,7 @@ class GameSession extends ChangeNotifier {
|
||||
|
||||
/// This method updates the points of each player after a round.
|
||||
/// It first uses the _sumPoints() method to calculate the total points of each player.
|
||||
/// Then, it checks if any player has reached 100 points. If so, it marks
|
||||
/// Then, it checks if any player has reached 100 points. If so, saves their indices and marks
|
||||
/// that player as having reached 100 points in that corresponding [Round] object.
|
||||
/// If the game has the point limit activated, it first applies the
|
||||
/// _subtractPointsForReachingHundred() method to subtract 50 points
|
||||
@@ -235,21 +231,31 @@ 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.
|
||||
Future<void> updatePoints() async {
|
||||
/// 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();
|
||||
|
||||
if (isPointsLimitEnabled) {
|
||||
_checkHundredPointsReached();
|
||||
bonusPlayers = _checkHundredPointsReached();
|
||||
bool limitExceeded = false;
|
||||
|
||||
for (int i = 0; i < playerScores.length; i++) {
|
||||
if (playerScores[i] > pointLimit) {
|
||||
isGameFinished = true;
|
||||
limitExceeded = true;
|
||||
print('${players[i]} hat die 100 Punkte ueberschritten, '
|
||||
'deswegen wurde das Spiel beendet');
|
||||
_setWinner();
|
||||
setWinner();
|
||||
}
|
||||
}
|
||||
if (!limitExceeded) {
|
||||
isGameFinished = false;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
@@ -270,30 +276,37 @@ class GameSession extends ChangeNotifier {
|
||||
/// Checks if a player has reached 100 points in the current round.
|
||||
/// If so, it updates the [scoreUpdate] List by subtracting 50 points from
|
||||
/// the corresponding round update.
|
||||
void _checkHundredPointsReached() {
|
||||
List<int> _checkHundredPointsReached() {
|
||||
List<int> bonusPlayers = [];
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (playerScores[i] == pointLimit) {
|
||||
bonusPlayers.add(i);
|
||||
print('${players[i]} hat genau 100 Punkte erreicht und bekommt '
|
||||
'deswegen 50 Punkte abgezogen');
|
||||
roundList[roundNumber - 1].scoreUpdates[i] -= 50;
|
||||
'deswegen ${(pointLimit / 2).round()} Punkte abgezogen');
|
||||
roundList[roundNumber - 1].scoreUpdates[i] -= (pointLimit / 2).round();
|
||||
}
|
||||
}
|
||||
_sumPoints();
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
/// Determines the winner of the game session.
|
||||
/// It iterates through the player scores and finds the player
|
||||
/// with the lowest score.
|
||||
void _setWinner() {
|
||||
int score = playerScores[0];
|
||||
String lowestPlayer = players[0];
|
||||
void setWinner() {
|
||||
int minScore = playerScores.reduce((a, b) => a < b ? a : b);
|
||||
List<String> lowestPlayers = [];
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (playerScores[i] < score) {
|
||||
score = playerScores[i];
|
||||
lowestPlayer = players[i];
|
||||
if (playerScores[i] == minScore) {
|
||||
lowestPlayers.add(players[i]);
|
||||
}
|
||||
}
|
||||
winner = lowestPlayer;
|
||||
if (lowestPlayers.length > 1) {
|
||||
winner =
|
||||
'${lowestPlayers.sublist(0, lowestPlayers.length - 1).join(', ')} & ${lowestPlayers.last}';
|
||||
} else {
|
||||
winner = lowestPlayers.first;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
|
||||
"app_name": "Cabo Counter",
|
||||
"round": "Runde",
|
||||
"rounds": "Runden",
|
||||
"mode": "Modus",
|
||||
"points": "Punkte",
|
||||
"unlimited": "Unbegrenzt",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"game": "Spiel",
|
||||
"ok": "OK",
|
||||
"player": "Spieler:in",
|
||||
"players": "Spieler:innen",
|
||||
"name": "Name",
|
||||
"back": "Zurück",
|
||||
|
||||
"home": "Home",
|
||||
"about": "Über",
|
||||
|
||||
"empty_text_1": "Ganz schön leer hier...",
|
||||
"empty_text_2": "Füge über den Button oben rechts eine neue Runde hinzu",
|
||||
"delete_game_title": "Spiel löschen?",
|
||||
"delete_game_message": "Bist du sicher, dass du die Runde {gameTitle} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"@delete_game_message": {
|
||||
"placeholders": {
|
||||
"gameTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overview": "Übersicht",
|
||||
"new_game": "Neues Spiel",
|
||||
"game_title": "Titel des Spiels",
|
||||
"select_mode": "Wähle einen Modus",
|
||||
"add_player": "Spieler:in hinzufügen",
|
||||
"create_game": "Spiel erstellen",
|
||||
"max_players_title": "Maximale Anzahl erreicht",
|
||||
"max_players_message": "Es können maximal 5 Spieler:innen hinzugefügt werden.",
|
||||
"no_gameTitle_title": "Kein Titel",
|
||||
"no_gameTitle_message": "Es muss ein Titel für das Spiel eingegeben werden.",
|
||||
"no_mode_title": "Kein Modus",
|
||||
"no_mode_message": "Es muss ein Spielmodus ausgewählt werden.",
|
||||
"min_players_title": "Zu wenig Spieler:innen",
|
||||
"min_players_message": "Es müssen mindestens 2 Spieler:innen hinzugefügt werden",
|
||||
"no_name_title": "Kein Name",
|
||||
"no_name_message": "Jeder Spieler muss einen Namen haben.",
|
||||
|
||||
"select_game_mode": "Spielmodus auswählen",
|
||||
"point_limit_description": "Es wird so lange gespielt, bis ein:e Spieler:in mehr als {pointLimit} Punkte erreicht",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unlimited_description": "Dem Spiel sind keine Grenzen gesetzt. Es wird so lange gespielt, bis ihr keine Lust mehr habt.",
|
||||
|
||||
"results": "Ergebnisse",
|
||||
"who_said_cabo": "Wer hat CABO gesagt?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"done": "Fertig",
|
||||
"next_round": "Nächste Runde",
|
||||
|
||||
"statistics": "Statistiken",
|
||||
"delete_game": "Spiel löschen",
|
||||
"new_game_same_settings": "Neues Spiel mit gleichen Einstellungen",
|
||||
"export_game": "Spiel exportieren",
|
||||
|
||||
"game_process": "Spielverlauf",
|
||||
|
||||
"settings": "Einstellungen",
|
||||
"cabo_penalty": "Cabo-Strafe",
|
||||
"cabo_penalty_subtitle": "... für falsches Cabo sagen",
|
||||
"point_limit": "Punkte-Limit",
|
||||
"point_limit_subtitle": "... hier ist Schluss",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"game_data": "Spieldaten",
|
||||
"import_data": "Daten importieren",
|
||||
"export_data": "Daten exportieren",
|
||||
"error": "Fehler",
|
||||
"error_import": "Datei konnte nicht importiert werden",
|
||||
"error_export": "Datei konnte nicht exportiert werden",
|
||||
"error_found": "Fehler gefunden?",
|
||||
"create_issue": "Issue erstellen",
|
||||
"app_version": "App-Version",
|
||||
"build": "Build",
|
||||
"load_version": "Lade Version...",
|
||||
|
||||
"about_text": "Hey :) Danke, dass du als eine:r der ersten User meiner ersten eigenen App dabei bist! Ich hab sehr viel Arbeit in dieses Projekt gesteckt und auch, wenn ich (hoffentlich) an vieles Gedacht hab, wird auf jeden Fall noch nicht alles 100% funktionieren. Solltest du also irgendwelche Fehler entdecken oder Feedback zum Design oder der Benutzerfreundlichkeit haben, teile Sie mir gern über die Testflight App oder auf den dir bekannten Wegen mit. Danke! "
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
|
||||
"app_name": "Cabo Counter",
|
||||
"round": "Round",
|
||||
"rounds": "Rounds",
|
||||
"mode": "Mode",
|
||||
"points": "Points",
|
||||
"unlimited": "Unlimited",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"game": "Game",
|
||||
"ok": "OK",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
"name": "Name",
|
||||
"back": "Back",
|
||||
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
|
||||
"empty_text_1": "Pretty empty here...",
|
||||
"empty_text_2": "Add a new round using the button in the top right corner.",
|
||||
"delete_game_title": "Delete game?",
|
||||
"delete_game_message": "Are you sure you want to delete the game {gameTitle}? This action cannot be undone.",
|
||||
"@delete_game_message": {
|
||||
"placeholders": {
|
||||
"gameTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overview": "Overview",
|
||||
"new_game": "New Game",
|
||||
"game_title": "Game Title",
|
||||
"select_mode": "Select a mode",
|
||||
"add_player": "Add Player",
|
||||
"create_game": "Create Game",
|
||||
"max_players_title": "Maximum reached",
|
||||
"max_players_message": "A maximum of 5 players can be added.",
|
||||
"no_gameTitle_title": "No Title",
|
||||
"no_gameTitle_message": "You must enter a title for the game.",
|
||||
"no_mode_title": "No Mode",
|
||||
"no_mode_message": "You must select a game mode.",
|
||||
"min_players_title": "Too few players",
|
||||
"min_players_message": "At least 2 players must be added.",
|
||||
"no_name_title": "No Name",
|
||||
"no_name_message": "Each player must have a name.",
|
||||
|
||||
"select_game_mode": "Select game mode",
|
||||
"point_limit_description": "The game ends when a player reaches more than {pointLimit} points.",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unlimited_description": "There is no limit. The game continues until you decide to stop.",
|
||||
|
||||
"results": "Results",
|
||||
"who_said_cabo": "Who said CABO?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"done": "Done",
|
||||
"next_round": "Next Round",
|
||||
|
||||
"statistics": "Statistics",
|
||||
"delete_game": "Delete Game",
|
||||
"new_game_same_settings": "New Game with same Settings",
|
||||
"export_game": "Export Game",
|
||||
|
||||
"game_process": "Spielverlauf",
|
||||
|
||||
"settings": "Settings",
|
||||
"cabo_penalty": "Cabo Penalty",
|
||||
"cabo_penalty_subtitle": "... for falsely calling Cabo.",
|
||||
"point_limit": "Point Limit",
|
||||
"point_limit_subtitle": "... the game ends here.",
|
||||
"reset_to_default": "Reset to Default",
|
||||
"game_data": "Game Data",
|
||||
"import_data": "Import Data",
|
||||
"export_data": "Export Data",
|
||||
"error": "Error",
|
||||
"error_import": "Could not import file",
|
||||
"error_export": "Could not export file",
|
||||
"error_found": "Found a bug?",
|
||||
"create_issue": "Create Issue",
|
||||
"app_version": "App Version",
|
||||
"load_version": "Loading version...",
|
||||
"build": "Build",
|
||||
|
||||
"about_text": "Hey :) Thanks for being one of the first users of my app! I’ve put a lot of work into this project, and even though I tried to think of everything, it might not work perfectly just yet. So if you discover any bugs or have feedback on the design or usability, please let me know via the TestFlight app or by sending me a message or email. Thank you very much!"
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for German (`de`).
|
||||
class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get app_name => 'Cabo Counter';
|
||||
|
||||
@override
|
||||
String get round => 'Runde';
|
||||
|
||||
@override
|
||||
String get rounds => 'Runden';
|
||||
|
||||
@override
|
||||
String get mode => 'Modus';
|
||||
|
||||
@override
|
||||
String get points => 'Punkte';
|
||||
|
||||
@override
|
||||
String get unlimited => 'Unbegrenzt';
|
||||
|
||||
@override
|
||||
String get delete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get game => 'Spiel';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
|
||||
@override
|
||||
String get player => 'Spieler:in';
|
||||
|
||||
@override
|
||||
String get players => 'Spieler:innen';
|
||||
|
||||
@override
|
||||
String get name => 'Name';
|
||||
|
||||
@override
|
||||
String get back => 'Zurück';
|
||||
|
||||
@override
|
||||
String get home => 'Home';
|
||||
|
||||
@override
|
||||
String get about => 'Über';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Ganz schön leer hier...';
|
||||
|
||||
@override
|
||||
String get empty_text_2 =>
|
||||
'Füge über den Button oben rechts eine neue Runde hinzu';
|
||||
|
||||
@override
|
||||
String get delete_game_title => 'Spiel löschen?';
|
||||
|
||||
@override
|
||||
String delete_game_message(String gameTitle) {
|
||||
return 'Bist du sicher, dass du die Runde $gameTitle löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get overview => 'Übersicht';
|
||||
|
||||
@override
|
||||
String get new_game => 'Neues Spiel';
|
||||
|
||||
@override
|
||||
String get game_title => 'Titel des Spiels';
|
||||
|
||||
@override
|
||||
String get select_mode => 'Wähle einen Modus';
|
||||
|
||||
@override
|
||||
String get add_player => 'Spieler:in hinzufügen';
|
||||
|
||||
@override
|
||||
String get create_game => 'Spiel erstellen';
|
||||
|
||||
@override
|
||||
String get max_players_title => 'Maximale Anzahl erreicht';
|
||||
|
||||
@override
|
||||
String get max_players_message =>
|
||||
'Es können maximal 5 Spieler:innen hinzugefügt werden.';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_title => 'Kein Titel';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_message =>
|
||||
'Es muss ein Titel für das Spiel eingegeben werden.';
|
||||
|
||||
@override
|
||||
String get no_mode_title => 'Kein Modus';
|
||||
|
||||
@override
|
||||
String get no_mode_message => 'Es muss ein Spielmodus ausgewählt werden.';
|
||||
|
||||
@override
|
||||
String get min_players_title => 'Zu wenig Spieler:innen';
|
||||
|
||||
@override
|
||||
String get min_players_message =>
|
||||
'Es müssen mindestens 2 Spieler:innen hinzugefügt werden';
|
||||
|
||||
@override
|
||||
String get no_name_title => 'Kein Name';
|
||||
|
||||
@override
|
||||
String get no_name_message => 'Jeder Spieler muss einen Namen haben.';
|
||||
|
||||
@override
|
||||
String get select_game_mode => 'Spielmodus auswählen';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'Es wird so lange gespielt, bis ein:e Spieler:in mehr als $pointLimit Punkte erreicht';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unlimited_description =>
|
||||
'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange gespielt, bis ihr keine Lust mehr habt.';
|
||||
|
||||
@override
|
||||
String get results => 'Ergebnisse';
|
||||
|
||||
@override
|
||||
String get who_said_cabo => 'Wer hat CABO gesagt?';
|
||||
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get done => 'Fertig';
|
||||
|
||||
@override
|
||||
String get next_round => 'Nächste Runde';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Spiel löschen';
|
||||
|
||||
@override
|
||||
String get new_game_same_settings => 'Neues Spiel mit gleichen Einstellungen';
|
||||
|
||||
@override
|
||||
String get export_game => 'Spiel exportieren';
|
||||
|
||||
@override
|
||||
String get game_process => 'Spielverlauf';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo-Strafe';
|
||||
|
||||
@override
|
||||
String get cabo_penalty_subtitle => '... für falsches Cabo sagen';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Punkte-Limit';
|
||||
|
||||
@override
|
||||
String get point_limit_subtitle => '... hier ist Schluss';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Auf Standard zurücksetzen';
|
||||
|
||||
@override
|
||||
String get game_data => 'Spieldaten';
|
||||
|
||||
@override
|
||||
String get import_data => 'Daten importieren';
|
||||
|
||||
@override
|
||||
String get export_data => 'Daten exportieren';
|
||||
|
||||
@override
|
||||
String get error => 'Fehler';
|
||||
|
||||
@override
|
||||
String get error_import => 'Datei konnte nicht importiert werden';
|
||||
|
||||
@override
|
||||
String get error_export => 'Datei konnte nicht exportiert werden';
|
||||
|
||||
@override
|
||||
String get error_found => 'Fehler gefunden?';
|
||||
|
||||
@override
|
||||
String get create_issue => 'Issue erstellen';
|
||||
|
||||
@override
|
||||
String get app_version => 'App-Version';
|
||||
|
||||
@override
|
||||
String get build => 'Build';
|
||||
|
||||
@override
|
||||
String get load_version => 'Lade Version...';
|
||||
|
||||
@override
|
||||
String get about_text =>
|
||||
'Hey :) Danke, dass du als eine:r der ersten User meiner ersten eigenen App dabei bist! Ich hab sehr viel Arbeit in dieses Projekt gesteckt und auch, wenn ich (hoffentlich) an vieles Gedacht hab, wird auf jeden Fall noch nicht alles 100% funktionieren. Solltest du also irgendwelche Fehler entdecken oder Feedback zum Design oder der Benutzerfreundlichkeit haben, teile Sie mir gern über die Testflight App oder auf den dir bekannten Wegen mit. Danke! ';
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get app_name => 'Cabo Counter';
|
||||
|
||||
@override
|
||||
String get round => 'Round';
|
||||
|
||||
@override
|
||||
String get rounds => 'Rounds';
|
||||
|
||||
@override
|
||||
String get mode => 'Mode';
|
||||
|
||||
@override
|
||||
String get points => 'Points';
|
||||
|
||||
@override
|
||||
String get unlimited => 'Unlimited';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get game => 'Game';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
|
||||
@override
|
||||
String get player => 'Player';
|
||||
|
||||
@override
|
||||
String get players => 'Players';
|
||||
|
||||
@override
|
||||
String get name => 'Name';
|
||||
|
||||
@override
|
||||
String get back => 'Back';
|
||||
|
||||
@override
|
||||
String get home => 'Home';
|
||||
|
||||
@override
|
||||
String get about => 'About';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Pretty empty here...';
|
||||
|
||||
@override
|
||||
String get empty_text_2 =>
|
||||
'Add a new round using the button in the top right corner.';
|
||||
|
||||
@override
|
||||
String get delete_game_title => 'Delete game?';
|
||||
|
||||
@override
|
||||
String delete_game_message(String gameTitle) {
|
||||
return 'Are you sure you want to delete the game $gameTitle? This action cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get overview => 'Overview';
|
||||
|
||||
@override
|
||||
String get new_game => 'New Game';
|
||||
|
||||
@override
|
||||
String get game_title => 'Game Title';
|
||||
|
||||
@override
|
||||
String get select_mode => 'Select a mode';
|
||||
|
||||
@override
|
||||
String get add_player => 'Add Player';
|
||||
|
||||
@override
|
||||
String get create_game => 'Create Game';
|
||||
|
||||
@override
|
||||
String get max_players_title => 'Maximum reached';
|
||||
|
||||
@override
|
||||
String get max_players_message => 'A maximum of 5 players can be added.';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_title => 'No Title';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_message => 'You must enter a title for the game.';
|
||||
|
||||
@override
|
||||
String get no_mode_title => 'No Mode';
|
||||
|
||||
@override
|
||||
String get no_mode_message => 'You must select a game mode.';
|
||||
|
||||
@override
|
||||
String get min_players_title => 'Too few players';
|
||||
|
||||
@override
|
||||
String get min_players_message => 'At least 2 players must be added.';
|
||||
|
||||
@override
|
||||
String get no_name_title => 'No Name';
|
||||
|
||||
@override
|
||||
String get no_name_message => 'Each player must have a name.';
|
||||
|
||||
@override
|
||||
String get select_game_mode => 'Select game mode';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'The game ends when a player reaches more than $pointLimit points.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unlimited_description =>
|
||||
'There is no limit. The game continues until you decide to stop.';
|
||||
|
||||
@override
|
||||
String get results => 'Results';
|
||||
|
||||
@override
|
||||
String get who_said_cabo => 'Who said CABO?';
|
||||
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get done => 'Done';
|
||||
|
||||
@override
|
||||
String get next_round => 'Next Round';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Delete Game';
|
||||
|
||||
@override
|
||||
String get new_game_same_settings => 'New Game with same Settings';
|
||||
|
||||
@override
|
||||
String get export_game => 'Export Game';
|
||||
|
||||
@override
|
||||
String get game_process => 'Spielverlauf';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo Penalty';
|
||||
|
||||
@override
|
||||
String get cabo_penalty_subtitle => '... for falsely calling Cabo.';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Point Limit';
|
||||
|
||||
@override
|
||||
String get point_limit_subtitle => '... the game ends here.';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Reset to Default';
|
||||
|
||||
@override
|
||||
String get game_data => 'Game Data';
|
||||
|
||||
@override
|
||||
String get import_data => 'Import Data';
|
||||
|
||||
@override
|
||||
String get export_data => 'Export Data';
|
||||
|
||||
@override
|
||||
String get error => 'Error';
|
||||
|
||||
@override
|
||||
String get error_import => 'Could not import file';
|
||||
|
||||
@override
|
||||
String get error_export => 'Could not export file';
|
||||
|
||||
@override
|
||||
String get error_found => 'Found a bug?';
|
||||
|
||||
@override
|
||||
String get create_issue => 'Create Issue';
|
||||
|
||||
@override
|
||||
String get app_version => 'App Version';
|
||||
|
||||
@override
|
||||
String get build => 'Build';
|
||||
|
||||
@override
|
||||
String get load_version => 'Loading version...';
|
||||
|
||||
@override
|
||||
String get about_text =>
|
||||
'Hey :) Thanks for being one of the first users of my app! I’ve put a lot of work into this project, and even though I tried to think of everything, it might not work perfectly just yet. So if you discover any bugs or have feedback on the design or usability, please let me know via the TestFlight app or by sending me a message or email. Thank you very much!';
|
||||
}
|
||||
167
lib/l10n/arb/app_de.arb
Normal file
167
lib/l10n/arb/app_de.arb
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
|
||||
"app_name": "Cabo Counter",
|
||||
"round": "Runde",
|
||||
"rounds": "Runden",
|
||||
"mode": "Modus",
|
||||
"points": "Punkte",
|
||||
"unlimited": "Unbegrenzt",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"game": "Spiel",
|
||||
"games": "Spiele",
|
||||
"gamemode": "Spielmodus",
|
||||
"ok": "OK",
|
||||
"player": "Spieler:in",
|
||||
"players": "Spieler:innen",
|
||||
"name": "Name",
|
||||
"back": "Zurück",
|
||||
|
||||
"home": "Home",
|
||||
|
||||
"about": "Über",
|
||||
"licenses": "Lizenzen",
|
||||
"license_details": "Lizenzdetails",
|
||||
"no_license_text": "Keine Lizenz verfügbar",
|
||||
"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",
|
||||
"delete_game_title": "Spiel löschen?",
|
||||
"delete_game_message": "Bist du sicher, dass du das Spiel \"{gameTitle}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"@delete_game_message": {
|
||||
"placeholders": {
|
||||
"gameTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pre_rating_title": "Gefällt dir die App?",
|
||||
"pre_rating_message": "Feedback hilft mir, die App zu verbessern. Vielen Dank!",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"bad_rating_title": "Unzufrieden mit der App?",
|
||||
"bad_rating_message": "Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!",
|
||||
"contact_email": "E-Mail schreiben",
|
||||
"email_subject": "Feedback: Cabo Counter App",
|
||||
"email_body": "Ich habe folgendes Feedback...",
|
||||
|
||||
"overview": "Übersicht",
|
||||
"new_game": "Neues Spiel",
|
||||
"game_title": "Titel des Spiels",
|
||||
"select_mode": "Wähle einen Modus",
|
||||
"add_player": "Spieler:in hinzufügen",
|
||||
"create_game": "Spiel erstellen",
|
||||
"max_players_title": "Maximale Anzahl erreicht",
|
||||
"max_players_message": "Es können maximal 5 Spieler:innen hinzugefügt werden.",
|
||||
"no_gameTitle_title": "Kein Titel",
|
||||
"no_gameTitle_message": "Es muss ein Titel für das Spiel eingegeben werden.",
|
||||
"no_mode_title": "Kein Modus",
|
||||
"no_mode_message": "Es muss ein Spielmodus ausgewählt werden.",
|
||||
"min_players_title": "Zu wenig Spieler:innen",
|
||||
"min_players_message": "Es müssen mindestens 2 Spieler:innen hinzugefügt werden",
|
||||
"no_name_title": "Kein Name",
|
||||
"no_name_message": "Jede:r Spieler:in muss einen Namen haben.",
|
||||
|
||||
"select_game_mode": "Spielmodus auswählen",
|
||||
"no_mode_selected": "Wähle einen Spielmodus",
|
||||
"no_default_mode": "Kein Modus",
|
||||
"no_default_description": "Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.",
|
||||
"point_limit_description": "Es wird so lange gespielt, bis ein:e Spieler:in mehr als {pointLimit} Punkte erreicht",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unlimited_description": "Es wird so lange gespielt, bis ihr keine Lust mehr habt. Das Spiel kann jederzeit manuell beendet werden.",
|
||||
|
||||
"results": "Ergebnisse",
|
||||
"who_said_cabo": "Wer hat CABO gesagt?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"who_has_kamikaze": "Wer hat Kamikaze?",
|
||||
"done": "Fertig",
|
||||
"next_round": "Nächste Runde",
|
||||
"bonus_points_title": "Bonus-Punkte!",
|
||||
"bonus_points_message": "{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!}}",
|
||||
"@bonus_points_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
},
|
||||
"bonusPoints": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"end_of_game_title": "Spiel beendet",
|
||||
"end_of_game_message": "{playerCount, plural, =1{{names} hat das Spiel mit {points} Punkten gewonnen. Glückwunsch!} other{{names} haben das Spiel mit {points} Punkten gewonnen. Glückwunsch!}}",
|
||||
"@end_of_game_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"points": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"end_game": "Spiel beenden",
|
||||
"delete_game": "Spiel löschen",
|
||||
"new_game_same_settings": "Neues Spiel mit gleichen Einstellungen",
|
||||
"export_game": "Spiel exportieren",
|
||||
"id_error_title": "ID Fehler",
|
||||
"id_error_message": "Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.",
|
||||
"end_game_title": "Spiel beenden?",
|
||||
"end_game_message": "Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.",
|
||||
|
||||
"statistics": "Statistiken",
|
||||
"point_overview": "Punktetabelle",
|
||||
"scoring_history": "Spielverlauf",
|
||||
"empty_graph_text": "Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.",
|
||||
|
||||
"settings": "Einstellungen",
|
||||
"cabo_penalty": "Cabo-Strafe",
|
||||
"point_limit": "Punkte-Limit",
|
||||
"standard_mode": "Standard-Modus",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"game_data": "Spieldaten",
|
||||
"import_data": "Spieldaten importieren",
|
||||
"export_data": "Spieldaten exportieren",
|
||||
"delete_data": "Alle Spieldaten löschen",
|
||||
"delete_data_title": "Spieldaten löschen?",
|
||||
"delete_data_message": "Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"app": "App",
|
||||
|
||||
"import_success_title": "Import erfolgreich",
|
||||
"import_success_message":"Die Spieldaten wurden erfolgreich importiert.",
|
||||
"import_validation_error_title": "Validierung fehlgeschlagen",
|
||||
"import_validation_error_message": "Es wurden keine Cabo-Counter Spieldaten gefunden. Bitte stellen Sie sicher, dass es sich um eine gültige Cabo-Counter Exportdatei handelt.",
|
||||
"import_format_error_title": "Falsches Format",
|
||||
"import_format_error_message": "Die Datei ist kein gültiges JSON-Format oder enthält ungültige Daten.",
|
||||
"import_generic_error_title": "Import fehlgeschlagen",
|
||||
"import_generic_error_message": "Der Import ist fehlgeschlagen.",
|
||||
|
||||
"export_error_title": "Fehler",
|
||||
"export_error_message": "Datei konnte nicht exportiert werden",
|
||||
|
||||
"error_found": "Fehler gefunden?",
|
||||
"create_issue": "Issue erstellen",
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App-Version",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"loading": "Lädt...",
|
||||
"build": "Build-Nr."
|
||||
}
|
||||
167
lib/l10n/arb/app_en.arb
Normal file
167
lib/l10n/arb/app_en.arb
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
|
||||
"app_name": "Cabo Counter",
|
||||
"round": "Round",
|
||||
"rounds": "Rounds",
|
||||
"mode": "Mode",
|
||||
"points": "Points",
|
||||
"unlimited": "Unlimited",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"game": "Game",
|
||||
"games": "Games",
|
||||
"gamemode": "Gamemode",
|
||||
"ok": "OK",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
"name": "Name",
|
||||
"back": "Back",
|
||||
|
||||
"home": "Home",
|
||||
|
||||
"about": "About",
|
||||
"licenses": "Licenses",
|
||||
"license_details": "License Details",
|
||||
"legal_notice": "Legal Notice",
|
||||
"no_license_text": "No license available",
|
||||
|
||||
"empty_text_1": "Pretty empty here...",
|
||||
"empty_text_2": "Create a new game using the button in the top right.",
|
||||
"delete_game_title": "Delete game?",
|
||||
"delete_game_message": "Are you sure you want to delete the game \"{gameTitle}\"? This action cannot be undone.",
|
||||
"@delete_game_message": {
|
||||
"placeholders": {
|
||||
"gameTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pre_rating_title": "Do you like the app?",
|
||||
"pre_rating_message": "Feedback helps me to continuously improve the app. Thank you!",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"bad_rating_title": "Not satisfied?",
|
||||
"bad_rating_message": "Feel free to send me an email directly so we can solve your problem!",
|
||||
"contact_email": "Contact via E-Mail",
|
||||
"email_subject": "Feedback: Cabo Counter App",
|
||||
"email_body": "I have the following feedback...",
|
||||
|
||||
"overview": "Overview",
|
||||
"new_game": "New Game",
|
||||
"game_title": "Game Title",
|
||||
"select_mode": "Select a mode",
|
||||
"add_player": "Add Player",
|
||||
"create_game": "Create Game",
|
||||
"max_players_title": "Player Limit Reached",
|
||||
"max_players_message": "You can add a maximum of 5 players.",
|
||||
"no_gameTitle_title": "Missing Game Title",
|
||||
"no_gameTitle_message": "Please enter a title for your game.",
|
||||
"no_mode_title": "Game Mode Required",
|
||||
"no_mode_message": "Please select a game mode to continue",
|
||||
"min_players_title": "Too Few Players",
|
||||
"min_players_message": "At least 2 players are required to start the game.",
|
||||
"no_name_title": "Missing Player Names",
|
||||
"no_name_message": "Each player must have a name.",
|
||||
|
||||
"select_game_mode": "Select game mode",
|
||||
"no_mode_selected": "No mode selected",
|
||||
"no_default_mode": "No default mode",
|
||||
"no_default_description": "The default mode gets reset.",
|
||||
"point_limit_description": "The game ends when a player scores more than {pointLimit} points.",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unlimited_description": "The game continues until you decide to stop playing. The game can be ended manually at any time.",
|
||||
|
||||
"results": "Results",
|
||||
"who_said_cabo": "Who called Cabo?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"who_has_kamikaze": "Who has Kamikaze?",
|
||||
"done": "Done",
|
||||
"next_round": "Next Round",
|
||||
"bonus_points_title": "Bonus-Points!",
|
||||
"bonus_points_message": "{playerCount, plural, =1{{names} has reached exactly the point limit of {pointLimit} points and therefore gets {bonusPoints} points deducted!} other{{names} have reached exactly the point limit of {pointLimit} points and therefore get {bonusPoints} points deducted!}}",
|
||||
"@bonus_points_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
},
|
||||
"bonusPoints": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"end_of_game_title": "End of Game",
|
||||
"end_of_game_message": "{playerCount, plural, =1{{names} won the game with {points} points. Congratulations!} other{{names} won the game with {points} points. Congratulations to everyone!}}",
|
||||
"@end_of_game_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"points": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"end_game": "End Game",
|
||||
"delete_game": "Delete Game",
|
||||
"new_game_same_settings": "New Game with same Settings",
|
||||
"export_game": "Export Game",
|
||||
"id_error_title": "ID Error",
|
||||
"id_error_message": "The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.",
|
||||
"end_game_title": "End the game?",
|
||||
"end_game_message": "Do you want to end the game? The game gets marked as finished and cannot be continued.",
|
||||
|
||||
"statistics": "Statistics",
|
||||
"point_overview": "Point Overview",
|
||||
"scoring_history": "Scoring History",
|
||||
"empty_graph_text": "You must play at least one round for the game progress graph to be displayed.",
|
||||
|
||||
"settings": "Settings",
|
||||
"cabo_penalty": "Cabo Penalty",
|
||||
"point_limit": "Point Limit",
|
||||
"standard_mode": "Default Mode",
|
||||
"reset_to_default": "Reset to Default",
|
||||
"game_data": "Game Data",
|
||||
"import_data": "Import Data",
|
||||
"export_data": "Export Data",
|
||||
"delete_data": "Delete all Game Data",
|
||||
"delete_data_title": "Delete game data?",
|
||||
"delete_data_message": "Are you sure you want to delete all game data? This action cannot be undone.",
|
||||
"app": "App",
|
||||
|
||||
"import_success_title": "Import successful",
|
||||
"import_success_message":"The game data has been successfully imported.",
|
||||
"import_validation_error_title": "Validation failed",
|
||||
"import_validation_error_message": "No Cabo-Counter game data was found. Please make sure that this is a valid Cabo-Counter export file.",
|
||||
"import_format_error_title": "Wrong format",
|
||||
"import_format_error_message": "The file is not a valid JSON format or contains invalid data.",
|
||||
"import_generic_error_title": "Import failed",
|
||||
"import_generic_error_message": "The import has failed.",
|
||||
|
||||
"export_error_title": "Export failed",
|
||||
"export_error_message": "Could not export file",
|
||||
|
||||
"error_found": "Found a bug?",
|
||||
"create_issue": "Create Issue",
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App Version",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"loading": "Loading...",
|
||||
"build": "Build No."
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import 'app_localizations_en.dart';
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
/// import 'generated/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
@@ -152,6 +152,18 @@ abstract class AppLocalizations {
|
||||
/// **'Spiel'**
|
||||
String get game;
|
||||
|
||||
/// No description provided for @games.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiele'**
|
||||
String get games;
|
||||
|
||||
/// No description provided for @gamemode.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spielmodus'**
|
||||
String get gamemode;
|
||||
|
||||
/// No description provided for @ok.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -194,6 +206,30 @@ abstract class AppLocalizations {
|
||||
/// **'Über'**
|
||||
String get about;
|
||||
|
||||
/// No description provided for @licenses.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lizenzen'**
|
||||
String get licenses;
|
||||
|
||||
/// No description provided for @license_details.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lizenzdetails'**
|
||||
String get license_details;
|
||||
|
||||
/// No description provided for @no_license_text.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Keine Lizenz verfügbar'**
|
||||
String get no_license_text;
|
||||
|
||||
/// No description provided for @legal_notice.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Impressum'**
|
||||
String get legal_notice;
|
||||
|
||||
/// No description provided for @empty_text_1.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -215,9 +251,63 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @delete_game_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Bist du sicher, dass du die Runde {gameTitle} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'**
|
||||
/// **'Bist du sicher, dass du das Spiel \"{gameTitle}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'**
|
||||
String delete_game_message(String gameTitle);
|
||||
|
||||
/// No description provided for @pre_rating_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Gefällt dir die App?'**
|
||||
String get pre_rating_title;
|
||||
|
||||
/// No description provided for @pre_rating_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Feedback hilft mir, die App zu verbessern. Vielen Dank!'**
|
||||
String get pre_rating_message;
|
||||
|
||||
/// No description provided for @yes.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Ja'**
|
||||
String get yes;
|
||||
|
||||
/// No description provided for @no.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nein'**
|
||||
String get no;
|
||||
|
||||
/// No description provided for @bad_rating_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Unzufrieden mit der App?'**
|
||||
String get bad_rating_title;
|
||||
|
||||
/// No description provided for @bad_rating_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!'**
|
||||
String get bad_rating_message;
|
||||
|
||||
/// No description provided for @contact_email.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'E-Mail schreiben'**
|
||||
String get contact_email;
|
||||
|
||||
/// No description provided for @email_subject.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Feedback: Cabo Counter App'**
|
||||
String get email_subject;
|
||||
|
||||
/// No description provided for @email_body.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Ich habe folgendes Feedback...'**
|
||||
String get email_body;
|
||||
|
||||
/// No description provided for @overview.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -311,7 +401,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @no_name_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Jeder Spieler muss einen Namen haben.'**
|
||||
/// **'Jede:r Spieler:in muss einen Namen haben.'**
|
||||
String get no_name_message;
|
||||
|
||||
/// No description provided for @select_game_mode.
|
||||
@@ -320,6 +410,24 @@ abstract class AppLocalizations {
|
||||
/// **'Spielmodus auswählen'**
|
||||
String get select_game_mode;
|
||||
|
||||
/// No description provided for @no_mode_selected.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wähle einen Spielmodus'**
|
||||
String get no_mode_selected;
|
||||
|
||||
/// No description provided for @no_default_mode.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Kein Modus'**
|
||||
String get no_default_mode;
|
||||
|
||||
/// No description provided for @no_default_description.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.'**
|
||||
String get no_default_description;
|
||||
|
||||
/// No description provided for @point_limit_description.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -329,7 +437,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @unlimited_description.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange gespielt, bis ihr keine Lust mehr habt.'**
|
||||
/// **'Es wird so lange gespielt, bis ihr keine Lust mehr habt. Das Spiel kann jederzeit manuell beendet werden.'**
|
||||
String get unlimited_description;
|
||||
|
||||
/// No description provided for @results.
|
||||
@@ -350,6 +458,12 @@ abstract class AppLocalizations {
|
||||
/// **'Kamikaze'**
|
||||
String get kamikaze;
|
||||
|
||||
/// No description provided for @who_has_kamikaze.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wer hat Kamikaze?'**
|
||||
String get who_has_kamikaze;
|
||||
|
||||
/// No description provided for @done.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -362,11 +476,36 @@ abstract class AppLocalizations {
|
||||
/// **'Nächste Runde'**
|
||||
String get next_round;
|
||||
|
||||
/// No description provided for @statistics.
|
||||
/// No description provided for @bonus_points_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Statistiken'**
|
||||
String get statistics;
|
||||
/// **'Bonus-Punkte!'**
|
||||
String get bonus_points_title;
|
||||
|
||||
/// No description provided for @bonus_points_message.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
/// No description provided for @end_of_game_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiel beendet'**
|
||||
String get end_of_game_title;
|
||||
|
||||
/// No description provided for @end_of_game_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'{playerCount, plural, =1{{names} hat das Spiel mit {points} Punkten gewonnen. Glückwunsch!} other{{names} haben das Spiel mit {points} Punkten gewonnen. Glückwunsch!}}'**
|
||||
String end_of_game_message(int playerCount, String names, int points);
|
||||
|
||||
/// No description provided for @end_game.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiel beenden'**
|
||||
String get end_game;
|
||||
|
||||
/// No description provided for @delete_game.
|
||||
///
|
||||
@@ -386,11 +525,53 @@ abstract class AppLocalizations {
|
||||
/// **'Spiel exportieren'**
|
||||
String get export_game;
|
||||
|
||||
/// No description provided for @game_process.
|
||||
/// No description provided for @id_error_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'ID Fehler'**
|
||||
String get id_error_title;
|
||||
|
||||
/// No description provided for @id_error_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.'**
|
||||
String get id_error_message;
|
||||
|
||||
/// No description provided for @end_game_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiel beenden?'**
|
||||
String get end_game_title;
|
||||
|
||||
/// No description provided for @end_game_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.'**
|
||||
String get end_game_message;
|
||||
|
||||
/// No description provided for @statistics.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Statistiken'**
|
||||
String get statistics;
|
||||
|
||||
/// No description provided for @point_overview.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Punktetabelle'**
|
||||
String get point_overview;
|
||||
|
||||
/// No description provided for @scoring_history.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spielverlauf'**
|
||||
String get game_process;
|
||||
String get scoring_history;
|
||||
|
||||
/// No description provided for @empty_graph_text.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.'**
|
||||
String get empty_graph_text;
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
@@ -404,23 +585,17 @@ abstract class AppLocalizations {
|
||||
/// **'Cabo-Strafe'**
|
||||
String get cabo_penalty;
|
||||
|
||||
/// No description provided for @cabo_penalty_subtitle.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'... für falsches Cabo sagen'**
|
||||
String get cabo_penalty_subtitle;
|
||||
|
||||
/// No description provided for @point_limit.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Punkte-Limit'**
|
||||
String get point_limit;
|
||||
|
||||
/// No description provided for @point_limit_subtitle.
|
||||
/// No description provided for @standard_mode.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'... hier ist Schluss'**
|
||||
String get point_limit_subtitle;
|
||||
/// **'Standard-Modus'**
|
||||
String get standard_mode;
|
||||
|
||||
/// No description provided for @reset_to_default.
|
||||
///
|
||||
@@ -437,32 +612,98 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @import_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Daten importieren'**
|
||||
/// **'Spieldaten importieren'**
|
||||
String get import_data;
|
||||
|
||||
/// No description provided for @export_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Daten exportieren'**
|
||||
/// **'Spieldaten exportieren'**
|
||||
String get export_data;
|
||||
|
||||
/// No description provided for @error.
|
||||
/// No description provided for @delete_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Alle Spieldaten löschen'**
|
||||
String get delete_data;
|
||||
|
||||
/// No description provided for @delete_data_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spieldaten löschen?'**
|
||||
String get delete_data_title;
|
||||
|
||||
/// No description provided for @delete_data_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'**
|
||||
String get delete_data_message;
|
||||
|
||||
/// No description provided for @app.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'App'**
|
||||
String get app;
|
||||
|
||||
/// No description provided for @import_success_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Import erfolgreich'**
|
||||
String get import_success_title;
|
||||
|
||||
/// No description provided for @import_success_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Die Spieldaten wurden erfolgreich importiert.'**
|
||||
String get import_success_message;
|
||||
|
||||
/// No description provided for @import_validation_error_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Validierung fehlgeschlagen'**
|
||||
String get import_validation_error_title;
|
||||
|
||||
/// No description provided for @import_validation_error_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Es wurden keine Cabo-Counter Spieldaten gefunden. Bitte stellen Sie sicher, dass es sich um eine gültige Cabo-Counter Exportdatei handelt.'**
|
||||
String get import_validation_error_message;
|
||||
|
||||
/// No description provided for @import_format_error_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Falsches Format'**
|
||||
String get import_format_error_title;
|
||||
|
||||
/// No description provided for @import_format_error_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Die Datei ist kein gültiges JSON-Format oder enthält ungültige Daten.'**
|
||||
String get import_format_error_message;
|
||||
|
||||
/// No description provided for @import_generic_error_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Import fehlgeschlagen'**
|
||||
String get import_generic_error_title;
|
||||
|
||||
/// No description provided for @import_generic_error_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Der Import ist fehlgeschlagen.'**
|
||||
String get import_generic_error_message;
|
||||
|
||||
/// No description provided for @export_error_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Fehler'**
|
||||
String get error;
|
||||
String get export_error_title;
|
||||
|
||||
/// No description provided for @error_import.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Datei konnte nicht importiert werden'**
|
||||
String get error_import;
|
||||
|
||||
/// No description provided for @error_export.
|
||||
/// No description provided for @export_error_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Datei konnte nicht exportiert werden'**
|
||||
String get error_export;
|
||||
String get export_error_message;
|
||||
|
||||
/// No description provided for @error_found.
|
||||
///
|
||||
@@ -476,29 +717,35 @@ abstract class AppLocalizations {
|
||||
/// **'Issue erstellen'**
|
||||
String get create_issue;
|
||||
|
||||
/// No description provided for @wiki.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wiki'**
|
||||
String get wiki;
|
||||
|
||||
/// No description provided for @app_version.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'App-Version'**
|
||||
String get app_version;
|
||||
|
||||
/// No description provided for @privacy_policy.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Datenschutzerklärung'**
|
||||
String get privacy_policy;
|
||||
|
||||
/// No description provided for @loading.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lädt...'**
|
||||
String get loading;
|
||||
|
||||
/// No description provided for @build.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Build'**
|
||||
/// **'Build-Nr.'**
|
||||
String get build;
|
||||
|
||||
/// No description provided for @load_version.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lade Version...'**
|
||||
String get load_version;
|
||||
|
||||
/// No description provided for @about_text.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Hey :) Danke, dass du als eine:r der ersten User meiner ersten eigenen App dabei bist! Ich hab sehr viel Arbeit in dieses Projekt gesteckt und auch, wenn ich (hoffentlich) an vieles Gedacht hab, wird auf jeden Fall noch nicht alles 100% funktionieren. Solltest du also irgendwelche Fehler entdecken oder Feedback zum Design oder der Benutzerfreundlichkeit haben, teile Sie mir gern über die Testflight App oder auf den dir bekannten Wegen mit. Danke! '**
|
||||
String get about_text;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
373
lib/l10n/generated/app_localizations_de.dart
Normal file
373
lib/l10n/generated/app_localizations_de.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for German (`de`).
|
||||
class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get app_name => 'Cabo Counter';
|
||||
|
||||
@override
|
||||
String get round => 'Runde';
|
||||
|
||||
@override
|
||||
String get rounds => 'Runden';
|
||||
|
||||
@override
|
||||
String get mode => 'Modus';
|
||||
|
||||
@override
|
||||
String get points => 'Punkte';
|
||||
|
||||
@override
|
||||
String get unlimited => 'Unbegrenzt';
|
||||
|
||||
@override
|
||||
String get delete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get game => 'Spiel';
|
||||
|
||||
@override
|
||||
String get games => 'Spiele';
|
||||
|
||||
@override
|
||||
String get gamemode => 'Spielmodus';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
|
||||
@override
|
||||
String get player => 'Spieler:in';
|
||||
|
||||
@override
|
||||
String get players => 'Spieler:innen';
|
||||
|
||||
@override
|
||||
String get name => 'Name';
|
||||
|
||||
@override
|
||||
String get back => 'Zurück';
|
||||
|
||||
@override
|
||||
String get home => 'Home';
|
||||
|
||||
@override
|
||||
String get about => 'Über';
|
||||
|
||||
@override
|
||||
String get licenses => 'Lizenzen';
|
||||
|
||||
@override
|
||||
String get license_details => 'Lizenzdetails';
|
||||
|
||||
@override
|
||||
String get no_license_text => 'Keine Lizenz verfügbar';
|
||||
|
||||
@override
|
||||
String get legal_notice => 'Impressum';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Ganz schön leer hier...';
|
||||
|
||||
@override
|
||||
String get empty_text_2 =>
|
||||
'Füge über den Button oben rechts eine neue Runde hinzu';
|
||||
|
||||
@override
|
||||
String get delete_game_title => 'Spiel löschen?';
|
||||
|
||||
@override
|
||||
String delete_game_message(String gameTitle) {
|
||||
return 'Bist du sicher, dass du das Spiel \"$gameTitle\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pre_rating_title => 'Gefällt dir die App?';
|
||||
|
||||
@override
|
||||
String get pre_rating_message =>
|
||||
'Feedback hilft mir, die App zu verbessern. Vielen Dank!';
|
||||
|
||||
@override
|
||||
String get yes => 'Ja';
|
||||
|
||||
@override
|
||||
String get no => 'Nein';
|
||||
|
||||
@override
|
||||
String get bad_rating_title => 'Unzufrieden mit der App?';
|
||||
|
||||
@override
|
||||
String get bad_rating_message =>
|
||||
'Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!';
|
||||
|
||||
@override
|
||||
String get contact_email => 'E-Mail schreiben';
|
||||
|
||||
@override
|
||||
String get email_subject => 'Feedback: Cabo Counter App';
|
||||
|
||||
@override
|
||||
String get email_body => 'Ich habe folgendes Feedback...';
|
||||
|
||||
@override
|
||||
String get overview => 'Übersicht';
|
||||
|
||||
@override
|
||||
String get new_game => 'Neues Spiel';
|
||||
|
||||
@override
|
||||
String get game_title => 'Titel des Spiels';
|
||||
|
||||
@override
|
||||
String get select_mode => 'Wähle einen Modus';
|
||||
|
||||
@override
|
||||
String get add_player => 'Spieler:in hinzufügen';
|
||||
|
||||
@override
|
||||
String get create_game => 'Spiel erstellen';
|
||||
|
||||
@override
|
||||
String get max_players_title => 'Maximale Anzahl erreicht';
|
||||
|
||||
@override
|
||||
String get max_players_message =>
|
||||
'Es können maximal 5 Spieler:innen hinzugefügt werden.';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_title => 'Kein Titel';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_message =>
|
||||
'Es muss ein Titel für das Spiel eingegeben werden.';
|
||||
|
||||
@override
|
||||
String get no_mode_title => 'Kein Modus';
|
||||
|
||||
@override
|
||||
String get no_mode_message => 'Es muss ein Spielmodus ausgewählt werden.';
|
||||
|
||||
@override
|
||||
String get min_players_title => 'Zu wenig Spieler:innen';
|
||||
|
||||
@override
|
||||
String get min_players_message =>
|
||||
'Es müssen mindestens 2 Spieler:innen hinzugefügt werden';
|
||||
|
||||
@override
|
||||
String get no_name_title => 'Kein Name';
|
||||
|
||||
@override
|
||||
String get no_name_message => 'Jede:r Spieler:in muss einen Namen haben.';
|
||||
|
||||
@override
|
||||
String get select_game_mode => 'Spielmodus auswählen';
|
||||
|
||||
@override
|
||||
String get no_mode_selected => 'Wähle einen Spielmodus';
|
||||
|
||||
@override
|
||||
String get no_default_mode => 'Kein Modus';
|
||||
|
||||
@override
|
||||
String get no_default_description =>
|
||||
'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'Es wird so lange gespielt, bis ein:e Spieler:in mehr als $pointLimit Punkte erreicht';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unlimited_description =>
|
||||
'Es wird so lange gespielt, bis ihr keine Lust mehr habt. Das Spiel kann jederzeit manuell beendet werden.';
|
||||
|
||||
@override
|
||||
String get results => 'Ergebnisse';
|
||||
|
||||
@override
|
||||
String get who_said_cabo => 'Wer hat CABO gesagt?';
|
||||
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get who_has_kamikaze => 'Wer hat Kamikaze?';
|
||||
|
||||
@override
|
||||
String get done => 'Fertig';
|
||||
|
||||
@override
|
||||
String get next_round => 'Nächste Runde';
|
||||
|
||||
@override
|
||||
String get bonus_points_title => 'Bonus-Punkte!';
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names haben exakt das Punktelimit von $pointLimit Punkten erreicht und bekommen deshalb jeweils $bonusPoints Punkte abgezogen!',
|
||||
one:
|
||||
'$names hat exakt das Punktelimit von $pointLimit Punkten erreicht und bekommt deshalb $bonusPoints Punkte abgezogen!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_of_game_title => 'Spiel beendet';
|
||||
|
||||
@override
|
||||
String end_of_game_message(int playerCount, String names, int points) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names haben das Spiel mit $points Punkten gewonnen. Glückwunsch!',
|
||||
one: '$names hat das Spiel mit $points Punkten gewonnen. Glückwunsch!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_game => 'Spiel beenden';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Spiel löschen';
|
||||
|
||||
@override
|
||||
String get new_game_same_settings => 'Neues Spiel mit gleichen Einstellungen';
|
||||
|
||||
@override
|
||||
String get export_game => 'Spiel exportieren';
|
||||
|
||||
@override
|
||||
String get id_error_title => 'ID Fehler';
|
||||
|
||||
@override
|
||||
String get id_error_message =>
|
||||
'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.';
|
||||
|
||||
@override
|
||||
String get end_game_title => 'Spiel beenden?';
|
||||
|
||||
@override
|
||||
String get end_game_message =>
|
||||
'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get point_overview => 'Punktetabelle';
|
||||
|
||||
@override
|
||||
String get scoring_history => 'Spielverlauf';
|
||||
|
||||
@override
|
||||
String get empty_graph_text =>
|
||||
'Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo-Strafe';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Punkte-Limit';
|
||||
|
||||
@override
|
||||
String get standard_mode => 'Standard-Modus';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Auf Standard zurücksetzen';
|
||||
|
||||
@override
|
||||
String get game_data => 'Spieldaten';
|
||||
|
||||
@override
|
||||
String get import_data => 'Spieldaten importieren';
|
||||
|
||||
@override
|
||||
String get export_data => 'Spieldaten exportieren';
|
||||
|
||||
@override
|
||||
String get delete_data => 'Alle Spieldaten löschen';
|
||||
|
||||
@override
|
||||
String get delete_data_title => 'Spieldaten löschen?';
|
||||
|
||||
@override
|
||||
String get delete_data_message =>
|
||||
'Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get app => 'App';
|
||||
|
||||
@override
|
||||
String get import_success_title => 'Import erfolgreich';
|
||||
|
||||
@override
|
||||
String get import_success_message =>
|
||||
'Die Spieldaten wurden erfolgreich importiert.';
|
||||
|
||||
@override
|
||||
String get import_validation_error_title => 'Validierung fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get import_validation_error_message =>
|
||||
'Es wurden keine Cabo-Counter Spieldaten gefunden. Bitte stellen Sie sicher, dass es sich um eine gültige Cabo-Counter Exportdatei handelt.';
|
||||
|
||||
@override
|
||||
String get import_format_error_title => 'Falsches Format';
|
||||
|
||||
@override
|
||||
String get import_format_error_message =>
|
||||
'Die Datei ist kein gültiges JSON-Format oder enthält ungültige Daten.';
|
||||
|
||||
@override
|
||||
String get import_generic_error_title => 'Import fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get import_generic_error_message => 'Der Import ist fehlgeschlagen.';
|
||||
|
||||
@override
|
||||
String get export_error_title => 'Fehler';
|
||||
|
||||
@override
|
||||
String get export_error_message => 'Datei konnte nicht exportiert werden';
|
||||
|
||||
@override
|
||||
String get error_found => 'Fehler gefunden?';
|
||||
|
||||
@override
|
||||
String get create_issue => 'Issue erstellen';
|
||||
|
||||
@override
|
||||
String get wiki => 'Wiki';
|
||||
|
||||
@override
|
||||
String get app_version => 'App-Version';
|
||||
|
||||
@override
|
||||
String get privacy_policy => 'Datenschutzerklärung';
|
||||
|
||||
@override
|
||||
String get loading => 'Lädt...';
|
||||
|
||||
@override
|
||||
String get build => 'Build-Nr.';
|
||||
}
|
||||
370
lib/l10n/generated/app_localizations_en.dart
Normal file
370
lib/l10n/generated/app_localizations_en.dart
Normal file
@@ -0,0 +1,370 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get app_name => 'Cabo Counter';
|
||||
|
||||
@override
|
||||
String get round => 'Round';
|
||||
|
||||
@override
|
||||
String get rounds => 'Rounds';
|
||||
|
||||
@override
|
||||
String get mode => 'Mode';
|
||||
|
||||
@override
|
||||
String get points => 'Points';
|
||||
|
||||
@override
|
||||
String get unlimited => 'Unlimited';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get game => 'Game';
|
||||
|
||||
@override
|
||||
String get games => 'Games';
|
||||
|
||||
@override
|
||||
String get gamemode => 'Gamemode';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
|
||||
@override
|
||||
String get player => 'Player';
|
||||
|
||||
@override
|
||||
String get players => 'Players';
|
||||
|
||||
@override
|
||||
String get name => 'Name';
|
||||
|
||||
@override
|
||||
String get back => 'Back';
|
||||
|
||||
@override
|
||||
String get home => 'Home';
|
||||
|
||||
@override
|
||||
String get about => 'About';
|
||||
|
||||
@override
|
||||
String get licenses => 'Licenses';
|
||||
|
||||
@override
|
||||
String get license_details => 'License Details';
|
||||
|
||||
@override
|
||||
String get no_license_text => 'No license available';
|
||||
|
||||
@override
|
||||
String get legal_notice => 'Legal Notice';
|
||||
|
||||
@override
|
||||
String get empty_text_1 => 'Pretty empty here...';
|
||||
|
||||
@override
|
||||
String get empty_text_2 =>
|
||||
'Create a new game using the button in the top right.';
|
||||
|
||||
@override
|
||||
String get delete_game_title => 'Delete game?';
|
||||
|
||||
@override
|
||||
String delete_game_message(String gameTitle) {
|
||||
return 'Are you sure you want to delete the game \"$gameTitle\"? This action cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pre_rating_title => 'Do you like the app?';
|
||||
|
||||
@override
|
||||
String get pre_rating_message =>
|
||||
'Feedback helps me to continuously improve the app. Thank you!';
|
||||
|
||||
@override
|
||||
String get yes => 'Yes';
|
||||
|
||||
@override
|
||||
String get no => 'No';
|
||||
|
||||
@override
|
||||
String get bad_rating_title => 'Not satisfied?';
|
||||
|
||||
@override
|
||||
String get bad_rating_message =>
|
||||
'Feel free to send me an email directly so we can solve your problem!';
|
||||
|
||||
@override
|
||||
String get contact_email => 'Contact via E-Mail';
|
||||
|
||||
@override
|
||||
String get email_subject => 'Feedback: Cabo Counter App';
|
||||
|
||||
@override
|
||||
String get email_body => 'I have the following feedback...';
|
||||
|
||||
@override
|
||||
String get overview => 'Overview';
|
||||
|
||||
@override
|
||||
String get new_game => 'New Game';
|
||||
|
||||
@override
|
||||
String get game_title => 'Game Title';
|
||||
|
||||
@override
|
||||
String get select_mode => 'Select a mode';
|
||||
|
||||
@override
|
||||
String get add_player => 'Add Player';
|
||||
|
||||
@override
|
||||
String get create_game => 'Create Game';
|
||||
|
||||
@override
|
||||
String get max_players_title => 'Player Limit Reached';
|
||||
|
||||
@override
|
||||
String get max_players_message => 'You can add a maximum of 5 players.';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_title => 'Missing Game Title';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_message => 'Please enter a title for your game.';
|
||||
|
||||
@override
|
||||
String get no_mode_title => 'Game Mode Required';
|
||||
|
||||
@override
|
||||
String get no_mode_message => 'Please select a game mode to continue';
|
||||
|
||||
@override
|
||||
String get min_players_title => 'Too Few Players';
|
||||
|
||||
@override
|
||||
String get min_players_message =>
|
||||
'At least 2 players are required to start the game.';
|
||||
|
||||
@override
|
||||
String get no_name_title => 'Missing Player Names';
|
||||
|
||||
@override
|
||||
String get no_name_message => 'Each player must have a name.';
|
||||
|
||||
@override
|
||||
String get select_game_mode => 'Select game mode';
|
||||
|
||||
@override
|
||||
String get no_mode_selected => 'No mode selected';
|
||||
|
||||
@override
|
||||
String get no_default_mode => 'No default mode';
|
||||
|
||||
@override
|
||||
String get no_default_description => 'The default mode gets reset.';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'The game ends when a player scores more than $pointLimit points.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unlimited_description =>
|
||||
'The game continues until you decide to stop playing. The game can be ended manually at any time.';
|
||||
|
||||
@override
|
||||
String get results => 'Results';
|
||||
|
||||
@override
|
||||
String get who_said_cabo => 'Who called Cabo?';
|
||||
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get who_has_kamikaze => 'Who has Kamikaze?';
|
||||
|
||||
@override
|
||||
String get done => 'Done';
|
||||
|
||||
@override
|
||||
String get next_round => 'Next Round';
|
||||
|
||||
@override
|
||||
String get bonus_points_title => 'Bonus-Points!';
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names have reached exactly the point limit of $pointLimit points and therefore get $bonusPoints points deducted!',
|
||||
one:
|
||||
'$names has reached exactly the point limit of $pointLimit points and therefore gets $bonusPoints points deducted!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_of_game_title => 'End of Game';
|
||||
|
||||
@override
|
||||
String end_of_game_message(int playerCount, String names, int points) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names won the game with $points points. Congratulations to everyone!',
|
||||
one: '$names won the game with $points points. Congratulations!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_game => 'End Game';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Delete Game';
|
||||
|
||||
@override
|
||||
String get new_game_same_settings => 'New Game with same Settings';
|
||||
|
||||
@override
|
||||
String get export_game => 'Export Game';
|
||||
|
||||
@override
|
||||
String get id_error_title => 'ID Error';
|
||||
|
||||
@override
|
||||
String get id_error_message =>
|
||||
'The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.';
|
||||
|
||||
@override
|
||||
String get end_game_title => 'End the game?';
|
||||
|
||||
@override
|
||||
String get end_game_message =>
|
||||
'Do you want to end the game? The game gets marked as finished and cannot be continued.';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
|
||||
@override
|
||||
String get point_overview => 'Point Overview';
|
||||
|
||||
@override
|
||||
String get scoring_history => 'Scoring History';
|
||||
|
||||
@override
|
||||
String get empty_graph_text =>
|
||||
'You must play at least one round for the game progress graph to be displayed.';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo Penalty';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Point Limit';
|
||||
|
||||
@override
|
||||
String get standard_mode => 'Default Mode';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Reset to Default';
|
||||
|
||||
@override
|
||||
String get game_data => 'Game Data';
|
||||
|
||||
@override
|
||||
String get import_data => 'Import Data';
|
||||
|
||||
@override
|
||||
String get export_data => 'Export Data';
|
||||
|
||||
@override
|
||||
String get delete_data => 'Delete all Game Data';
|
||||
|
||||
@override
|
||||
String get delete_data_title => 'Delete game data?';
|
||||
|
||||
@override
|
||||
String get delete_data_message =>
|
||||
'Are you sure you want to delete all game data? This action cannot be undone.';
|
||||
|
||||
@override
|
||||
String get app => 'App';
|
||||
|
||||
@override
|
||||
String get import_success_title => 'Import successful';
|
||||
|
||||
@override
|
||||
String get import_success_message =>
|
||||
'The game data has been successfully imported.';
|
||||
|
||||
@override
|
||||
String get import_validation_error_title => 'Validation failed';
|
||||
|
||||
@override
|
||||
String get import_validation_error_message =>
|
||||
'No Cabo-Counter game data was found. Please make sure that this is a valid Cabo-Counter export file.';
|
||||
|
||||
@override
|
||||
String get import_format_error_title => 'Wrong format';
|
||||
|
||||
@override
|
||||
String get import_format_error_message =>
|
||||
'The file is not a valid JSON format or contains invalid data.';
|
||||
|
||||
@override
|
||||
String get import_generic_error_title => 'Import failed';
|
||||
|
||||
@override
|
||||
String get import_generic_error_message => 'The import has failed.';
|
||||
|
||||
@override
|
||||
String get export_error_title => 'Export failed';
|
||||
|
||||
@override
|
||||
String get export_error_message => 'Could not export file';
|
||||
|
||||
@override
|
||||
String get error_found => 'Found a bug?';
|
||||
|
||||
@override
|
||||
String get create_issue => 'Create Issue';
|
||||
|
||||
@override
|
||||
String get wiki => 'Wiki';
|
||||
|
||||
@override
|
||||
String get app_version => 'App Version';
|
||||
|
||||
@override
|
||||
String get privacy_policy => 'Privacy Policy';
|
||||
|
||||
@override
|
||||
String get loading => 'Loading...';
|
||||
|
||||
@override
|
||||
String get build => 'Build No.';
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.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/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/tab_view.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.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();
|
||||
Globals.pointLimit = await ConfigService.getPointLimit();
|
||||
Globals.caboPenalty = await ConfigService.getCaboPenalty();
|
||||
await VersionService.init();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
@@ -39,6 +41,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
/// Every time the app goes into the background or is closed,
|
||||
/// save the current game sessions to local storage.
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
@@ -65,6 +70,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
return supportedLocales.first;
|
||||
},
|
||||
theme: CupertinoThemeData(
|
||||
applyThemeToAll: true,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: CustomTheme.primaryColor,
|
||||
scaffoldBackgroundColor: CustomTheme.backgroundColor,
|
||||
|
||||
95
lib/presentation/views/about/about_view.dart
Normal file
95
lib/presentation/views/about/about_view.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/about/licenses/license_view.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// A view that displays information about the app, including its name, version,
|
||||
/// privacy policy, imprint, and licenses.
|
||||
class AboutView extends StatelessWidget {
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).about),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app_name,
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).app_version} ${VersionService.getVersionWithBuild()}',
|
||||
style: TextStyle(fontSize: 15, color: Colors.grey[300]),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('assets/cabo_counter-logo_rounded.png'),
|
||||
)),
|
||||
CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(AppLocalizations.of(context).privacy_policy),
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kPrivacyPolicyLink)),
|
||||
),
|
||||
CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(AppLocalizations.of(context).legal_notice),
|
||||
onPressed: () => launchUrl(Uri.parse(Constants.kLegalLink)),
|
||||
),
|
||||
CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(AppLocalizations.of(context).licenses),
|
||||
onPressed: () => Navigator.push(context,
|
||||
CupertinoPageRoute(builder: (_) => const LicenseView()))),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Text(
|
||||
'\u00A9 Felix Kirchner',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kInstagramLink)),
|
||||
icon: const Icon(FontAwesomeIcons.instagram)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('mailto:${Constants.kEmail}')),
|
||||
icon: const Icon(CupertinoIcons.envelope)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubLink)),
|
||||
icon: const Icon(FontAwesomeIcons.github)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart'
|
||||
show AppLocalizations;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// 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;
|
||||
const LicenseDetailView(
|
||||
{super.key, required this.title, required this.license});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(
|
||||
AppLocalizations.of(context).license_details,
|
||||
),
|
||||
previousPageTitle: AppLocalizations.of(context).licenses,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.buttonBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Text(
|
||||
license,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/presentation/views/about/licenses/license_view.dart
Normal file
67
lib/presentation/views/about/licenses/license_view.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/about/licenses/license_detail_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/about/licenses/oss_licenses.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// 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});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).licenses),
|
||||
previousPageTitle: AppLocalizations.of(context).about,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: ossLicenses.length,
|
||||
itemBuilder: (_, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => LicenseDetailView(
|
||||
title: ossLicenses[index].name,
|
||||
license: ossLicenses[index].license ??
|
||||
AppLocalizations.of(context).no_license_text,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
trailing: const CupertinoListTileChevron(),
|
||||
title: Text(
|
||||
ossLicenses[index].name,
|
||||
style: GoogleFonts.roboto(),
|
||||
),
|
||||
subtitle: Text(ossLicenses[index].description),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
5899
lib/presentation/views/about/licenses/oss_licenses.dart
Normal file
5899
lib/presentation/views/about/licenses/oss_licenses.dart
Normal file
File diff suppressed because it is too large
Load Diff
552
lib/presentation/views/home/active_game/active_game_view.dart
Normal file
552
lib/presentation/views/home/active_game/active_game_view.dart
Normal file
@@ -0,0 +1,552 @@
|
||||
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/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';
|
||||
import 'package:cabo_counter/presentation/views/home/active_game/points_view.dart';
|
||||
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: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});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_ActiveGameViewState createState() => _ActiveGameViewState();
|
||||
}
|
||||
|
||||
class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
/// Constant value to represent a press on the cancel button in round view.
|
||||
static const int kRoundCancelled = -1;
|
||||
|
||||
final confettiController = ConfettiController(
|
||||
duration: const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
late final GameSession gameSession;
|
||||
|
||||
/// A list of the ranks for each player corresponding to their index in sortedPlayerIndices
|
||||
late List<int> denseRanks;
|
||||
|
||||
/// A list of player indices sorted by their scores in ascending order.
|
||||
late List<int> sortedPlayerIndices;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
gameSession = widget.gameSession;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
ListenableBuilder(
|
||||
listenable: gameSession,
|
||||
builder: (context, _) {
|
||||
sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
denseRanks = _calculateDenseRank(
|
||||
gameSession.playerScores, sortedPlayerIndices);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: AppLocalizations.of(context).games,
|
||||
middle: Text(AppLocalizations.of(context).overview),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).name),
|
||||
trailing: Text(
|
||||
gameSession.gameTitle,
|
||||
style: TextStyle(color: CustomTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).mode),
|
||||
trailing: Text(
|
||||
gameSession.isPointsLimitEnabled
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited,
|
||||
style: TextStyle(color: CustomTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: gameSession.players.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
int playerIndex = sortedPlayerIndices[index];
|
||||
return CupertinoListTile(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(14, 5, 14, 0),
|
||||
title: Row(
|
||||
children: [
|
||||
_getPlacementTextWidget(index),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
gameSession.players[playerIndex],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${gameSession.playerScores[playerIndex]} '
|
||||
'${AppLocalizations.of(context).points}')
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).rounds,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: gameSession.roundNumber,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(
|
||||
'${AppLocalizations.of(context).round} ${index + 1}',
|
||||
),
|
||||
trailing: index + 1 !=
|
||||
gameSession.roundNumber ||
|
||||
gameSession.isGameFinished == true
|
||||
? (const Text('\u{2705}',
|
||||
style: TextStyle(fontSize: 22)))
|
||||
: const Text('\u{23F3}',
|
||||
style: TextStyle(fontSize: 22)),
|
||||
onTap: () async {
|
||||
_openRoundView(context, index + 1);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.scoring_history,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => GraphView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).point_overview,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => PointsView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: !gameSession.isPointsLimitEnabled,
|
||||
child: CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished
|
||||
? const TextStyle(color: Colors.white)
|
||||
: const TextStyle(
|
||||
color: Colors.white30),
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
if (gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished) {
|
||||
_showEndGameDialog();
|
||||
}
|
||||
}),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
_showDeleteGameDialog().then((value) {
|
||||
if (value) {
|
||||
_removeGameSession(gameSession);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.new_game_same_settings,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => CreateGameView(
|
||||
gameTitle:
|
||||
gameSession.gameTitle,
|
||||
gameMode: widget.gameSession
|
||||
.isPointsLimitEnabled ==
|
||||
true
|
||||
? GameMode.pointLimit
|
||||
: GameMode.unlimited,
|
||||
players: gameSession.players,
|
||||
)));
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).export_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () async {
|
||||
final success = await LocalStorageService
|
||||
.exportSingleGameSession(
|
||||
widget.gameSession);
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
CupertinoAlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.export_error_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)
|
||||
.export_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
.ok),
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: ConfettiWidget(
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
particleDrag: 0.07,
|
||||
emissionFrequency: 0.1,
|
||||
numberOfParticles: 10,
|
||||
minBlastForce: 5,
|
||||
maxBlastForce: 20,
|
||||
confettiController: confettiController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm ending the game.
|
||||
/// If the user confirms, it calls the `endGame` method on the game manager
|
||||
void _showEndGameDialog() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).end_game_title),
|
||||
content: Text(AppLocalizations.of(context).end_game_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
gameManager.endGame(gameSession.id);
|
||||
_playFinishAnimation(context);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of player indices sorted by their scores in
|
||||
/// ascending order.
|
||||
List<int> _getSortedPlayerIndices() {
|
||||
List<int> playerIndices =
|
||||
List<int>.generate(gameSession.players.length, (index) => index);
|
||||
// Sort the indices based on the summed points
|
||||
playerIndices.sort((a, b) {
|
||||
int scoreA = gameSession.playerScores[a];
|
||||
int scoreB = gameSession.playerScores[b];
|
||||
if (scoreA != scoreB) {
|
||||
return scoreA.compareTo(scoreB);
|
||||
}
|
||||
return a.compareTo(b);
|
||||
});
|
||||
return playerIndices;
|
||||
}
|
||||
|
||||
/// Calculates the dense rank for a player based on their index in the sorted list of players.
|
||||
List<int> _calculateDenseRank(
|
||||
List<int> playerScores, List<int> sortedIndices) {
|
||||
List<int> denseRanks = [];
|
||||
int rank = 1;
|
||||
for (int i = 0; i < sortedIndices.length; i++) {
|
||||
if (i > 0) {
|
||||
int prevScore = playerScores[sortedIndices[i - 1]];
|
||||
int currScore = playerScores[sortedIndices[i]];
|
||||
if (currScore != prevScore) {
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
denseRanks.add(rank);
|
||||
}
|
||||
return denseRanks;
|
||||
}
|
||||
|
||||
/// Returns a text widget representing the placement text based on the given placement number.
|
||||
/// [index] is the index of the player in [players] list,
|
||||
Text _getPlacementTextWidget(int index) {
|
||||
int placement = denseRanks[index];
|
||||
switch (placement) {
|
||||
case 1:
|
||||
return const Text('\u{1F947}', style: TextStyle(fontSize: 22)); // 🥇
|
||||
case 2:
|
||||
return const Text('\u{1F948}', style: TextStyle(fontSize: 22)); // 🥈
|
||||
case 3:
|
||||
return const Text('\u{1F949}', style: TextStyle(fontSize: 22)); // 🥉
|
||||
default:
|
||||
return Text(' $placement.',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold));
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm deleting the game session.
|
||||
Future<bool> _showDeleteGameDialog() async {
|
||||
return await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_game_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)
|
||||
.delete_game_message(gameSession.gameTitle),
|
||||
),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).delete,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.red),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Removes the game session in the game manager and navigates back to the previous screen.
|
||||
/// If the game session does not exist in the game list, it shows an error dialog.
|
||||
Future<void> _removeGameSession(GameSession gameSession) async {
|
||||
if (gameManager.gameExistsInGameList(gameSession.id)) {
|
||||
Navigator.pop(context);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gameManager.removeGameSessionById(gameSession.id);
|
||||
});
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).id_error_title),
|
||||
content: Text(AppLocalizations.of(context).id_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively opens the RoundView for the specified round number.
|
||||
/// It starts with the given [roundNumber] and continues to open the next round
|
||||
/// until the user navigates back or the round number is invalid.
|
||||
void _openRoundView(BuildContext context, int roundNumber) async {
|
||||
final round = await Navigator.of(context, rootNavigator: true).push(
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => RoundView(
|
||||
gameSession: gameSession,
|
||||
roundNumber: roundNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// If the user presses the cancel button
|
||||
if (round == kRoundCancelled) return;
|
||||
|
||||
if (widget.gameSession.isGameFinished && context.mounted) {
|
||||
_playFinishAnimation(context);
|
||||
}
|
||||
|
||||
// If the previous round was not the last one
|
||||
if (round != null && round >= 0) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: Constants.kRoundViewDelay));
|
||||
if (context.mounted) {
|
||||
_openRoundView(context, round);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 winnerAmount = winner.contains('&') ? 2 : 1;
|
||||
|
||||
confettiController.play();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: Constants.kPopUpDelay));
|
||||
|
||||
if (context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).end_of_game_title),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.end_of_game_message(winnerAmount, winner, winnerPoints)),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () {
|
||||
confettiController.stop();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
139
lib/presentation/views/home/active_game/graph_view.dart
Normal file
139
lib/presentation/views/home/active_game/graph_view.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
/// 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;
|
||||
|
||||
const GraphView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
/// List of colors for the graph lines.
|
||||
final List<Color> lineColors = [
|
||||
CustomTheme.graphColor1,
|
||||
CustomTheme.graphColor2,
|
||||
CustomTheme.graphColor3,
|
||||
CustomTheme.graphColor4,
|
||||
CustomTheme.graphColor5
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).scoring_history),
|
||||
previousPageTitle: AppLocalizations.of(context).overview,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Visibility(
|
||||
visible: widget.gameSession.roundNumber > 1 ||
|
||||
widget.gameSession.isGameFinished,
|
||||
replacement: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Center(
|
||||
child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).empty_graph_text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SfCartesianChart(
|
||||
enableAxisAnimation: true,
|
||||
legend: const Legend(
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
isVisible: true,
|
||||
position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
labelAlignment: LabelAlignment.center,
|
||||
labelPosition: ChartDataLabelPosition.inside,
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
axisLabelFormatter: (AxisLabelRenderDetails details) {
|
||||
if (details.value == 0) {
|
||||
return ChartAxisLabel('', const TextStyle());
|
||||
}
|
||||
return ChartAxisLabel(
|
||||
'${details.value.toInt()}', const TextStyle());
|
||||
},
|
||||
),
|
||||
series: getCumulativeScores(),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns a list of LineSeries representing the cumulative scores of each player.
|
||||
/// Each series contains data points for each round, showing the cumulative score up to that round.
|
||||
/// The x-axis represents the round number, and the y-axis represents the cumulative score.
|
||||
List<LineSeries<(int, num), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
for (var round in rounds) {
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
runningTotals[i] += round.scoreUpdates[i];
|
||||
cumulativeScores[i].add(runningTotals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const double jitterStep = 0.03;
|
||||
|
||||
/// Create a list of LineSeries for each player
|
||||
/// Each series contains data points for each round
|
||||
return List.generate(playerCount, (i) {
|
||||
final data = List.generate(
|
||||
cumulativeScores[i].length + 1,
|
||||
(j) => (
|
||||
j,
|
||||
j == 0 || cumulativeScores[i][j - 1] == 0
|
||||
? 0 // 0 points at the start of the game or when the value is 0 (don't subtract jitter step)
|
||||
|
||||
// Adds a small jitter to the cumulative scores to prevent overlapping data points in the graph.
|
||||
// The jitter is centered around zero by subtracting playerCount ~/ 2 from the player index i.
|
||||
: cumulativeScores[i][j - 1] + (i - playerCount ~/ 2) * jitterStep
|
||||
),
|
||||
);
|
||||
|
||||
/// Create a LineSeries for the player
|
||||
/// The xValueMapper maps the round number, and the yValueMapper maps the cumulative score.
|
||||
return LineSeries<(int, num), int>(
|
||||
name: playerNames[i],
|
||||
dataSource: data,
|
||||
xValueMapper: (record, _) => record.$1,
|
||||
yValueMapper: (record, _) => record.$2,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: lineColors[i],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
enum GameMode {
|
||||
none,
|
||||
pointLimit,
|
||||
unlimited,
|
||||
}
|
||||
|
||||
/// 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;
|
||||
const ModeSelectionMenu(
|
||||
{super.key, required this.pointLimit, required this.showDeselection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).gamemode),
|
||||
previousPageTitle:
|
||||
!showDeselection ? AppLocalizations.of(context).new_game : '',
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text('$pointLimit ${AppLocalizations.of(context).points}',
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)
|
||||
.point_limit_description(pointLimit),
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.pointLimit);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).unlimited,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).unlimited_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.unlimited);
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: showDeselection,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).no_default_mode,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).no_default_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.none);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
262
lib/presentation/views/home/active_game/points_view.dart
Normal file
262
lib/presentation/views/home/active_game/points_view.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
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';
|
||||
|
||||
/// 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;
|
||||
|
||||
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).overview,
|
||||
),
|
||||
child: SafeArea(child: LayoutBuilder(builder: (context, constraints) {
|
||||
const double caboFieldWidthFactor = 0.2;
|
||||
const double tablePadding = 8;
|
||||
final int playerCount = widget.gameSession.players.length;
|
||||
const double roundColWidth = 35;
|
||||
final double playerColWidth =
|
||||
(constraints.maxWidth - roundColWidth - (tablePadding)) /
|
||||
playerCount;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: constraints.maxWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: tablePadding),
|
||||
child: DataTable(
|
||||
dataRowMaxHeight: 0,
|
||||
dataRowMinHeight: 0,
|
||||
columnSpacing: 0,
|
||||
horizontalMargin: 0,
|
||||
columns: [
|
||||
const DataColumn(
|
||||
label: SizedBox(
|
||||
width: roundColWidth,
|
||||
child: Text(
|
||||
'#',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
numeric: true,
|
||||
),
|
||||
...widget.gameSession.players.map(
|
||||
(player) => DataColumn(
|
||||
label: SizedBox(
|
||||
width: playerColWidth,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
player,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: constraints.maxWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: tablePadding),
|
||||
child: DataTable(
|
||||
dataRowMaxHeight: 75,
|
||||
dataRowMinHeight: 75,
|
||||
columnSpacing: 0,
|
||||
horizontalMargin: 0,
|
||||
headingRowHeight: 0,
|
||||
columns: [
|
||||
const DataColumn(
|
||||
label: SizedBox(
|
||||
width: roundColWidth,
|
||||
child: Text(
|
||||
'#',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
numeric: true,
|
||||
),
|
||||
...widget.gameSession.players.map(
|
||||
(player) => DataColumn(
|
||||
label: SizedBox(
|
||||
width: playerColWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8),
|
||||
child: Text(
|
||||
player,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0),
|
||||
child: Container(
|
||||
width: playerColWidth *
|
||||
(playerCount *
|
||||
caboFieldWidthFactor), // Adjust width based on amount of players
|
||||
decoration: BoxDecoration(
|
||||
color: saidCabo
|
||||
? CustomTheme
|
||||
.buttonBackgroundColor
|
||||
: CupertinoColors.transparent,
|
||||
borderRadius:
|
||||
BorderRadius.circular(5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: update <= 0
|
||||
? CustomTheme
|
||||
.pointLossColor
|
||||
: CustomTheme
|
||||
.pointGainColor,
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6),
|
||||
),
|
||||
child: Text(
|
||||
'${update >= 0 ? '+' : ''}$update',
|
||||
style: const TextStyle(
|
||||
color:
|
||||
CupertinoColors.white,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$score',
|
||||
style: TextStyle(
|
||||
color: CustomTheme.white,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
})));
|
||||
}
|
||||
}
|
||||
557
lib/presentation/views/home/active_game/round_view.dart
Normal file
557
lib/presentation/views/home/active_game/round_view.dart
Normal file
@@ -0,0 +1,557 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/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;
|
||||
const RoundView(
|
||||
{super.key, required this.roundNumber, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_RoundViewState createState() => _RoundViewState();
|
||||
}
|
||||
|
||||
class _RoundViewState extends State<RoundView> {
|
||||
/// The current game session.
|
||||
late GameSession gameSession = widget.gameSession;
|
||||
|
||||
/// Index of the player who said CABO.
|
||||
int _caboPlayerIndex = 0;
|
||||
|
||||
/// Index of the player who has Kamikaze.
|
||||
/// Default is null (no Kamikaze player).
|
||||
int? _kamikazePlayerIndex;
|
||||
|
||||
/// List of text controllers for the score text fields.
|
||||
late final List<TextEditingController> _scoreControllerList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => TextEditingController(),
|
||||
);
|
||||
|
||||
/// List of focus nodes for the score text fields.
|
||||
late final List<FocusNode> _focusNodeList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => FocusNode(),
|
||||
);
|
||||
|
||||
late List<GlobalKey> _textFieldKeys;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print('=== Runde ${widget.roundNumber} geöffnet ===');
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished == true) {
|
||||
print(
|
||||
'Diese wurde bereits gespielt, deshalb werden die alten Punktestaende angezeigt');
|
||||
|
||||
// If the current round has already been played, the text fields
|
||||
// are filled with the scores from this round
|
||||
for (int i = 0; i < _scoreControllerList.length; i++) {
|
||||
_scoreControllerList[i].text =
|
||||
gameSession.roundList[widget.roundNumber - 1].scores[i].toString();
|
||||
}
|
||||
_caboPlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].caboPlayerIndex;
|
||||
_kamikazePlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].kamikazePlayerIndex;
|
||||
}
|
||||
|
||||
_textFieldKeys = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => GlobalKey(),
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final rotatedPlayers = _getRotatedPlayers();
|
||||
final originalIndices = _getOriginalIndices();
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context, -1)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
trailing: Visibility(
|
||||
visible: widget.gameSession.isGameFinished,
|
||||
child: const Icon(
|
||||
CupertinoIcons.lock,
|
||||
size: 25,
|
||||
))),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 20 + bottomInset),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).round} ${widget.roundNumber}',
|
||||
style: CustomTheme.roundTitle),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppLocalizations.of(context).who_said_cabo,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.gameSession.players.length > 3 ? 5 : 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
unselectedColor:
|
||||
CustomTheme.mainElementBackgroundColor,
|
||||
selectedColor: CustomTheme.primaryColor,
|
||||
groupValue: _caboPlayerIndex,
|
||||
children: Map.fromEntries(widget.gameSession.players
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 8,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_caboPlayerIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: rotatedPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final originalIndex = originalIndices[index];
|
||||
final name = rotatedPlayers[index];
|
||||
bool shouldShowMedal =
|
||||
index == 0 && widget.roundNumber > 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 20),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CustomTheme.playerTileColor,
|
||||
title: Row(children: [
|
||||
Expanded(
|
||||
child: Row(children: [
|
||||
Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const SizedBox(width: 10),
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const Icon(FontAwesomeIcons.crown,
|
||||
size: 15))
|
||||
]))
|
||||
]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[originalIndex]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: SizedBox(
|
||||
width: 100,
|
||||
key: _textFieldKeys[originalIndex],
|
||||
child: CupertinoTextField(
|
||||
maxLength: 3,
|
||||
focusNode: _focusNodeList[originalIndex],
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
signed: true,
|
||||
decimal: false,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textInputAction: index ==
|
||||
widget.gameSession.players.length - 1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.next,
|
||||
controller:
|
||||
_scoreControllerList[originalIndex],
|
||||
placeholder:
|
||||
AppLocalizations.of(context).points,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) =>
|
||||
_focusNextTextfield(originalIndex),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Center(
|
||||
heightFactor: 1,
|
||||
child: CustomButton(
|
||||
onPressed: () async {
|
||||
if (await _showKamikazeSheet(context)) {
|
||||
if (!context.mounted) return;
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).kamikaze,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.kamikazeColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
KeyboardVisibilityBuilder(
|
||||
builder: (context, visible) {
|
||||
if (!visible) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
color: CustomTheme.mainElementBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, false);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
if (!widget.gameSession.isGameFinished)
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the index of the player who won the previous round.
|
||||
/// Returns 0 in the first round, as there is no previous round.
|
||||
int _getPreviousRoundWinnerIndex() {
|
||||
if (widget.roundNumber == 1) {
|
||||
return 0; // If it's the first round, the order should be the same as the players list.
|
||||
}
|
||||
|
||||
final List<int> scores =
|
||||
widget.gameSession.roundList[widget.roundNumber - 2].scoreUpdates;
|
||||
final int winnerIndex = scores.indexOf(0);
|
||||
|
||||
// Fallback if no player has 0 points, which should not happen in a valid game.
|
||||
if (winnerIndex == -1) {
|
||||
return 0;
|
||||
}
|
||||
return winnerIndex;
|
||||
}
|
||||
|
||||
/// Rotates the players list based on the previous round's winner.
|
||||
List<String> _getRotatedPlayers() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
widget.gameSession.players[winnerIndex],
|
||||
...widget.gameSession.players.sublist(winnerIndex + 1),
|
||||
...widget.gameSession.players.sublist(0, winnerIndex)
|
||||
];
|
||||
}
|
||||
|
||||
/// Gets the original indices of the players by recalculating it from the rotated list.
|
||||
List<int> _getOriginalIndices() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
winnerIndex,
|
||||
...List.generate(widget.gameSession.players.length - winnerIndex - 1,
|
||||
(i) => winnerIndex + i + 1),
|
||||
...List.generate(winnerIndex, (i) => i)
|
||||
];
|
||||
}
|
||||
|
||||
/// Shows a Cupertino action sheet to select the player who has Kamikaze.
|
||||
/// It returns true if a player was selected, false if the action was cancelled.
|
||||
Future<bool> _showKamikazeSheet(BuildContext context) async {
|
||||
return await showCupertinoModalPopup<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(AppLocalizations.of(context).kamikaze),
|
||||
message: Text(AppLocalizations.of(context).who_has_kamikaze),
|
||||
actions: widget.gameSession.players.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
_kamikazePlayerIndex = index;
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(color: CustomTheme.kamikazeColor),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
isDestructiveAction: true,
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Focuses the next text field in the list of text fields.
|
||||
/// [index] is the index of the current text field.
|
||||
void _focusNextTextfield(int index) {
|
||||
final originalIndices = _getOriginalIndices();
|
||||
final currentPos = originalIndices.indexOf(index);
|
||||
|
||||
if (currentPos < originalIndices.length - 1) {
|
||||
final nextIndex = originalIndices[currentPos + 1];
|
||||
FocusScope.of(context)
|
||||
.requestFocus(_focusNodeList[originalIndices[currentPos + 1]]);
|
||||
|
||||
final scrollContext = _textFieldKeys[nextIndex].currentContext;
|
||||
if (scrollContext != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Scrollable.ensureVisible(
|
||||
scrollContext,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
alignment: 0.55,
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_focusNodeList[index].unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the inputs for the round are valid.
|
||||
/// Returns true if the inputs are valid, false otherwise.
|
||||
/// Round Inputs are valid if every player has a score or
|
||||
/// kamikaze is selected for a player
|
||||
bool _areRoundInputsValid() {
|
||||
if (_areTextFieldsEmpty() && _kamikazePlayerIndex == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if any of the text fields for the players points are empty.
|
||||
/// Returns true if any of the text fields is empty, false otherwise.
|
||||
bool _areTextFieldsEmpty() {
|
||||
for (TextEditingController t in _scoreControllerList) {
|
||||
if (t.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Finishes the current round.
|
||||
/// It first determines, ifCalls the [_calculateScoredPoints()] method to calculate the points for
|
||||
/// every player. If the round is the highest round played in this game,
|
||||
/// it expands the player score lists. At the end it updates the score
|
||||
/// array for the game.
|
||||
List<int> _finishRound() {
|
||||
print('====================================');
|
||||
print('Runde ${widget.roundNumber} beendet');
|
||||
// The shown round is smaller than the newest round
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber) {
|
||||
print('Da diese Runde bereits gespielt wurde, werden die alten '
|
||||
'Punktestaende ueberschrieben');
|
||||
}
|
||||
if (_kamikazePlayerIndex != null) {
|
||||
print('${widget.gameSession.players[_kamikazePlayerIndex!]} hat Kamikaze '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen 50 Punkte');
|
||||
widget.gameSession
|
||||
.applyKamikaze(widget.roundNumber, _kamikazePlayerIndex!);
|
||||
} else {
|
||||
List<int> roundScores = [];
|
||||
for (TextEditingController c in _scoreControllerList) {
|
||||
if (c.text.isNotEmpty) roundScores.add(int.parse(c.text));
|
||||
}
|
||||
widget.gameSession.calculateScoredPoints(
|
||||
widget.roundNumber, roundScores, _caboPlayerIndex);
|
||||
}
|
||||
List<int> bonusPlayers = widget.gameSession.updatePoints();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
print('Das Spiel ist beendet');
|
||||
} else if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
/// Shows a popup dialog with the information which player received the bonus points.
|
||||
Future<void> _showBonusPopup(
|
||||
BuildContext context, List<int> bonusPlayers) async {
|
||||
int pointLimit = widget.gameSession.pointLimit;
|
||||
int bonusPoints = (pointLimit / 2).round();
|
||||
|
||||
String resultText =
|
||||
_getBonusPopupMessageString(pointLimit, bonusPoints, bonusPlayers);
|
||||
|
||||
await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).bonus_points_title),
|
||||
content: Text(resultText),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates the message string for the bonus popup.
|
||||
/// It takes the [pointLimit], [bonusPoints] and the list of [bonusPlayers]
|
||||
/// and returns a formatted string.
|
||||
String _getBonusPopupMessageString(
|
||||
int pointLimit, int bonusPoints, List<int> bonusPlayers) {
|
||||
List<String> nameList =
|
||||
bonusPlayers.map((i) => widget.gameSession.players[i]).toList();
|
||||
String resultText = '';
|
||||
if (nameList.length == 1) {
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length, nameList.first, pointLimit, bonusPoints);
|
||||
} else {
|
||||
resultText = nameList.length == 2
|
||||
? '${nameList[0]} & ${nameList[1]}'
|
||||
: '${nameList.sublist(0, nameList.length - 1).join(', ')} & ${nameList.last}';
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length,
|
||||
resultText,
|
||||
pointLimit,
|
||||
bonusPoints,
|
||||
);
|
||||
}
|
||||
return resultText;
|
||||
}
|
||||
|
||||
/// Handles the navigation for the end of the round.
|
||||
/// It checks for bonus players and shows a popup, saves the game session,
|
||||
/// and navigates to the next round or back to the previous screen.
|
||||
/// It takes the BuildContext [context] and a boolean [navigateToNextRound] to determine
|
||||
/// if it should navigate to the next round or not.
|
||||
Future<void> _endOfRoundNavigation(
|
||||
BuildContext context, bool navigateToNextRound) async {
|
||||
List<int> bonusPlayersIndices = _finishRound();
|
||||
if (bonusPlayersIndices.isNotEmpty) {
|
||||
await _showBonusPopup(context, bonusPlayersIndices);
|
||||
}
|
||||
|
||||
LocalStorageService.saveGameSessions();
|
||||
|
||||
if (context.mounted) {
|
||||
// If the game is finished, pop the context and return to the previous screen.
|
||||
if (widget.gameSession.isGameFinished) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is false, pop the context and return to the previous screen.
|
||||
if (!navigateToNextRound) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is true and the game isn't finished yet,
|
||||
// pop the context and navigate to the next round.
|
||||
Navigator.pop(context, widget.roundNumber + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _scoreControllerList) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final focusNode in _focusNodeList) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
504
lib/presentation/views/home/create_game_view.dart
Normal file
504
lib/presentation/views/home/create_game_view.dart
Normal file
@@ -0,0 +1,504 @@
|
||||
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/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';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_button.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum CreateStatus {
|
||||
noGameTitle,
|
||||
noModeSelected,
|
||||
minPlayers,
|
||||
maxPlayers,
|
||||
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;
|
||||
final List<String>? players;
|
||||
|
||||
const CreateGameView({
|
||||
super.key,
|
||||
this.gameTitle,
|
||||
this.players,
|
||||
required this.gameMode,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_CreateGameViewState createState() => _CreateGameViewState();
|
||||
}
|
||||
|
||||
class _CreateGameViewState extends State<CreateGameView> {
|
||||
final TextEditingController _gameTitleTextController =
|
||||
TextEditingController();
|
||||
|
||||
/// List of text controllers for player names.
|
||||
final List<TextEditingController> _playerNameTextControllers = [
|
||||
TextEditingController()
|
||||
];
|
||||
|
||||
/// List of focus nodes for player name text fields.
|
||||
final List<FocusNode> _playerNameFocusNodes = [FocusNode()];
|
||||
|
||||
/// Maximum number of players allowed in the game.
|
||||
final int maxPlayers = 5;
|
||||
|
||||
/// Factor to adjust the view length when the keyboard is visible.
|
||||
final double keyboardHeightAdjustmentFactor = 0.75;
|
||||
|
||||
/// Variable to hold the selected game mode.
|
||||
late GameMode gameMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
gameMode = widget.gameMode;
|
||||
|
||||
_gameTitleTextController.text = widget.gameTitle ?? '';
|
||||
|
||||
if (widget.players != null) {
|
||||
_playerNameTextControllers.clear();
|
||||
for (var player in widget.players!) {
|
||||
_playerNameTextControllers.add(TextEditingController(text: player));
|
||||
_playerNameFocusNodes.add(FocusNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, dynamic result) async {
|
||||
if (!didPop) {
|
||||
await _keyboardDelay();
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: AppLocalizations.of(context).games,
|
||||
middle: Text(AppLocalizations.of(context).new_game),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
maxLength: 20,
|
||||
prefix: Text(AppLocalizations.of(context).name),
|
||||
textAlign: TextAlign.right,
|
||||
placeholder: AppLocalizations.of(context).game_title,
|
||||
controller: _gameTitleTextController,
|
||||
onSubmitted: (_) {
|
||||
_playerNameFocusNodes.isNotEmpty
|
||||
? _playerNameFocusNodes[0].requestFocus()
|
||||
: FocusScope.of(context).unfocus();
|
||||
},
|
||||
textInputAction: _playerNameFocusNodes.isNotEmpty
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
readOnly: true,
|
||||
prefix: Text(AppLocalizations.of(context).mode),
|
||||
suffix: Row(
|
||||
children: [
|
||||
_getDisplayedGameMode(),
|
||||
const SizedBox(width: 3),
|
||||
const CupertinoListTileChevron(),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
await _keyboardDelay();
|
||||
|
||||
if (context.mounted) {
|
||||
final selectedMode = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
gameMode = selectedMode ?? gameMode;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: _playerNameTextControllers.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < _playerNameTextControllers.length &&
|
||||
newIndex <= _playerNameTextControllers.length) {
|
||||
if (newIndex > oldIndex) newIndex--;
|
||||
final item =
|
||||
_playerNameTextControllers.removeAt(oldIndex);
|
||||
_playerNameTextControllers.insert(newIndex, item);
|
||||
}
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
key: ValueKey(index),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Icon(
|
||||
CupertinoIcons.minus_circle_fill,
|
||||
color: CustomTheme.red,
|
||||
size: 25,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_playerNameTextControllers[index].dispose();
|
||||
_playerNameTextControllers.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: _playerNameTextControllers[index],
|
||||
focusNode: _playerNameFocusNodes[index],
|
||||
maxLength: 12,
|
||||
placeholder:
|
||||
'${AppLocalizations.of(context).player} ${index + 1}',
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(),
|
||||
textInputAction: index + 1 <
|
||||
_playerNameTextControllers.length
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
onSubmitted: (_) {
|
||||
if (index + 1 <
|
||||
_playerNameFocusNodes.length) {
|
||||
_playerNameFocusNodes[index + 1]
|
||||
.requestFocus();
|
||||
} else {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _playerNameTextControllers.length > 1
|
||||
? 1.0
|
||||
: 0.0,
|
||||
duration: const Duration(
|
||||
milliseconds: Constants.kFadeInDuration),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Icon(
|
||||
CupertinoIcons.line_horizontal_3,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 50),
|
||||
child: Stack(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: null,
|
||||
child: Icon(
|
||||
CupertinoIcons.plus_circle_fill,
|
||||
color: CustomTheme.primaryColor,
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).add_player,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_playerNameTextControllers.length <
|
||||
maxPlayers) {
|
||||
setState(() {
|
||||
_playerNameTextControllers
|
||||
.add(TextEditingController());
|
||||
_playerNameFocusNodes.add(FocusNode());
|
||||
});
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) {
|
||||
_playerNameFocusNodes.last.requestFocus();
|
||||
});
|
||||
} else {
|
||||
_showFeedbackDialog(CreateStatus.maxPlayers);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 50),
|
||||
child: Center(
|
||||
child: CustomButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).create_game,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await _keyboardDelay();
|
||||
_checkAllGameAttributes();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
KeyboardVisibilityBuilder(builder: (context, visible) {
|
||||
if (visible) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).viewInsets.bottom *
|
||||
keyboardHeightAdjustmentFactor,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
))));
|
||||
}
|
||||
|
||||
/// Returns a widget that displays the currently selected game mode in the View.
|
||||
Text _getDisplayedGameMode() {
|
||||
if (gameMode == GameMode.none) {
|
||||
return Text(AppLocalizations.of(context).no_mode_selected);
|
||||
} else if (gameMode == GameMode.pointLimit) {
|
||||
return Text(
|
||||
'${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}',
|
||||
style: TextStyle(color: CustomTheme.primaryColor));
|
||||
} else {
|
||||
return Text(AppLocalizations.of(context).unlimited,
|
||||
style: TextStyle(color: CustomTheme.primaryColor));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks all game attributes before creating a new game.
|
||||
/// If any attribute is invalid, it shows a feedback dialog.
|
||||
/// If all attributes are valid, it calls the `_createGame` method.
|
||||
void _checkAllGameAttributes() {
|
||||
if (_gameTitleTextController.text == '') {
|
||||
_showFeedbackDialog(CreateStatus.noGameTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameMode == GameMode.none) {
|
||||
_showFeedbackDialog(CreateStatus.noModeSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerNameTextControllers.length < 2) {
|
||||
_showFeedbackDialog(CreateStatus.minPlayers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_everyPlayerHasAName()) {
|
||||
_showFeedbackDialog(CreateStatus.noPlayerName);
|
||||
return;
|
||||
}
|
||||
|
||||
_createGame();
|
||||
}
|
||||
|
||||
/// Checks if every player has a name.
|
||||
/// Returns true if all players have a name, false otherwise.
|
||||
bool _everyPlayerHasAName() {
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
if (controller.text == '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Displays a feedback dialog based on the [CreateStatus].
|
||||
void _showFeedbackDialog(CreateStatus status) {
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the title and message for the dialog based on the [CreateStatus].
|
||||
(String, String) _getDialogContent(CreateStatus status) {
|
||||
switch (status) {
|
||||
case CreateStatus.noGameTitle:
|
||||
return (
|
||||
AppLocalizations.of(context).no_gameTitle_title,
|
||||
AppLocalizations.of(context).no_gameTitle_message
|
||||
);
|
||||
case CreateStatus.noModeSelected:
|
||||
return (
|
||||
AppLocalizations.of(context).no_mode_title,
|
||||
AppLocalizations.of(context).no_mode_message
|
||||
);
|
||||
|
||||
case CreateStatus.minPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).min_players_title,
|
||||
AppLocalizations.of(context).min_players_message
|
||||
);
|
||||
case CreateStatus.maxPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).max_players_title,
|
||||
AppLocalizations.of(context).max_players_message
|
||||
);
|
||||
case CreateStatus.noPlayerName:
|
||||
return (
|
||||
AppLocalizations.of(context).no_name_title,
|
||||
AppLocalizations.of(context).no_name_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new gameSession and navigates to the active game view.
|
||||
/// This method creates a new gameSession object with the provided attributes in the text fields.
|
||||
/// 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();
|
||||
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
gameManager.addGameSession(gameSession);
|
||||
final session = gameManager.getGameSessionById(id) ?? gameSession;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ActiveGameView(gameSession: session)),
|
||||
(Route<dynamic> route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
|
||||
/// If the keyboard is visible, this method will unfocus the current text field
|
||||
/// to prevent the keyboard from interfering with the navigation bar.
|
||||
Future<void> _keyboardDelay() async {
|
||||
if (!KeyboardVisibilityController().isVisible) {
|
||||
return;
|
||||
} else {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: Constants.kKeyboardDelay));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_gameTitleTextController.dispose();
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (var focusnode in _playerNameFocusNodes) {
|
||||
focusnode.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
379
lib/presentation/views/home/main_menu_view.dart
Normal file
379
lib/presentation/views/home/main_menu_view.dart
Normal file
@@ -0,0 +1,379 @@
|
||||
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/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/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
enum PreRatingDialogDecision { yes, no, cancel }
|
||||
|
||||
enum BadRatingDialogDecision { email, cancel }
|
||||
|
||||
/// 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});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_MainMenuViewState createState() => _MainMenuViewState();
|
||||
}
|
||||
|
||||
class _MainMenuViewState extends State<MainMenuView> {
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
LocalStorageService.loadGameSessions().then((_) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
});
|
||||
gameManager.addListener(_updateView);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
precacheImage(
|
||||
const AssetImage('assets/cabo_counter-logo_rounded.png'), context);
|
||||
await Constants.rateMyApp.init();
|
||||
|
||||
if (Constants.rateMyApp.shouldOpenDialog &&
|
||||
Constants.appDevPhase != 'Beta') {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
if (!mounted) return;
|
||||
_handleFeedbackDialog(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateView() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: gameManager,
|
||||
builder: (context, _) {
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const SettingsView(),
|
||||
),
|
||||
).then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.settings, size: 30)),
|
||||
middle: Text(AppLocalizations.of(context).games),
|
||||
trailing: IconButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => CreateGameView(
|
||||
gameMode: ConfigService.getGameMode()),
|
||||
),
|
||||
),
|
||||
icon: const Icon(CupertinoIcons.add)),
|
||||
),
|
||||
child: CupertinoPageScaffold(
|
||||
child: SafeArea(
|
||||
child: Visibility(
|
||||
visible: _isLoading,
|
||||
replacement: Visibility(
|
||||
visible: gameManager.gameList.isEmpty,
|
||||
replacement: ListView.separated(
|
||||
itemCount: gameManager.gameList.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
indent: 50,
|
||||
endIndent: 50,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final session = gameManager.gameList[index];
|
||||
return ListenableBuilder(
|
||||
listenable: session,
|
||||
builder: (context, _) {
|
||||
return Dismissible(
|
||||
key: Key(session.id),
|
||||
background: Container(
|
||||
color: CustomTheme.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: const Icon(
|
||||
CupertinoIcons.delete,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (direction) async {
|
||||
return await _showDeleteGamePopup(
|
||||
context, session.gameTitle);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
gameManager.removeGameSessionById(session.id);
|
||||
},
|
||||
dismissThresholds: const {
|
||||
DismissDirection.startToEnd: 0.6
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0),
|
||||
child: CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(session.gameTitle),
|
||||
subtitle: Visibility(
|
||||
visible: session.isGameFinished,
|
||||
replacement: Text(
|
||||
'${AppLocalizations.of(context).mode}: ${_translateGameMode(session)}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
child: Text(
|
||||
'\u{1F947} ${session.winner}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
)),
|
||||
trailing: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Text('${session.roundNumber}'),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(CupertinoIcons
|
||||
.arrow_2_circlepath_circle_fill),
|
||||
const SizedBox(width: 15),
|
||||
Text('${session.players.length}'),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(
|
||||
CupertinoIcons.person_2_fill),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final session =
|
||||
gameManager.gameList[index];
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ActiveGameView(
|
||||
gameSession: session),
|
||||
),
|
||||
).then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => CreateGameView(
|
||||
gameMode: ConfigService.getGameMode()),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.plus,
|
||||
size: 60,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 70),
|
||||
child: Text(
|
||||
'${AppLocalizations.of(context).empty_text_1}\n${AppLocalizations.of(context).empty_text_2}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Center(child: CupertinoActivityIndicator()),
|
||||
),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
/// Translates the game mode boolean into the corresponding String.
|
||||
/// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'.
|
||||
String _translateGameMode(GameSession gameSession) {
|
||||
if (gameSession.isPointsLimitEnabled) {
|
||||
return '${gameSession.pointLimit} ${AppLocalizations.of(context).points}';
|
||||
}
|
||||
return AppLocalizations.of(context).unlimited;
|
||||
}
|
||||
|
||||
/// Handles the feedback dialog when the conditions for rating are met.
|
||||
/// It shows a dialog asking the user if they like the app,
|
||||
/// and based on their response, it either opens the rating dialog or an email client for feedback.
|
||||
Future<void> _handleFeedbackDialog(BuildContext context) async {
|
||||
final String emailSubject = AppLocalizations.of(context).email_subject;
|
||||
final String emailBody = AppLocalizations.of(context).email_body;
|
||||
|
||||
final Uri emailUri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: Constants.kEmail,
|
||||
query: 'subject=$emailSubject'
|
||||
'&body=$emailBody',
|
||||
);
|
||||
|
||||
PreRatingDialogDecision preRatingDecision =
|
||||
await _showPreRatingDialog(context);
|
||||
BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel;
|
||||
|
||||
// so that the bad rating dialog is not shown immediately
|
||||
await Future.delayed(const Duration(milliseconds: Constants.kPopUpDelay));
|
||||
|
||||
switch (preRatingDecision) {
|
||||
case PreRatingDialogDecision.yes:
|
||||
if (context.mounted) Constants.rateMyApp.showStarRateDialog(context);
|
||||
break;
|
||||
case PreRatingDialogDecision.no:
|
||||
if (context.mounted) {
|
||||
badRatingDecision = await _showBadRatingDialog(context);
|
||||
}
|
||||
if (badRatingDecision == BadRatingDialogDecision.email) {
|
||||
if (context.mounted) {
|
||||
launchUrl(emailUri);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PreRatingDialogDecision.cancel:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog to delete all game sessions.
|
||||
/// Returns true if the user confirms the deletion, false otherwise.
|
||||
/// [gameTitle] is the title of the game session to be deleted.
|
||||
Future<bool> _showDeleteGamePopup(
|
||||
BuildContext context, String gameTitle) async {
|
||||
return await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game_title,
|
||||
),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.delete_game_message(gameTitle)),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).delete,
|
||||
),
|
||||
)
|
||||
]);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Shows a dialog asking the user if they like the app.
|
||||
/// Returns the user's decision as an integer.
|
||||
/// - PRE_RATING_DIALOG_YES: User likes the app and wants to rate it.
|
||||
/// - PRE_RATING_DIALOG_NO: User does not like the app and wants to provide feedback.
|
||||
/// - PRE_RATING_DIALOG_CANCEL: User cancels the dialog.
|
||||
Future<PreRatingDialogDecision> _showPreRatingDialog(
|
||||
BuildContext context) async {
|
||||
return await showCupertinoDialog<PreRatingDialogDecision>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).pre_rating_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).pre_rating_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () => Navigator.of(context)
|
||||
.pop(PreRatingDialogDecision.yes),
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).yes),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(PreRatingDialogDecision.no),
|
||||
child: Text(AppLocalizations.of(context).no),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
)
|
||||
],
|
||||
)) ??
|
||||
PreRatingDialogDecision.cancel;
|
||||
}
|
||||
|
||||
/// Shows a dialog asking the user for feedback if they do not like the app.
|
||||
/// Returns the user's decision as an integer.
|
||||
/// - BAD_RATING_DIALOG_EMAIL: User wants to send an email with feedback.
|
||||
/// - BAD_RATING_DIALOG_CANCEL: User cancels the dialog.
|
||||
Future<BadRatingDialogDecision> _showBadRatingDialog(
|
||||
BuildContext context) async {
|
||||
return await showCupertinoDialog<BadRatingDialogDecision>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).bad_rating_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).bad_rating_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.of(context)
|
||||
.pop(BadRatingDialogDecision.email),
|
||||
child: Text(AppLocalizations.of(context).contact_email),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(AppLocalizations.of(context).cancel))
|
||||
],
|
||||
)) ??
|
||||
BadRatingDialogDecision.cancel;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
gameManager.removeListener(_updateView);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
302
lib/presentation/views/home/settings_view.dart
Normal file
302
lib/presentation/views/home/settings_view.dart
Normal file
@@ -0,0 +1,302 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/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/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});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
UniqueKey _stepperKey1 = UniqueKey();
|
||||
UniqueKey _stepperKey2 = UniqueKey();
|
||||
GameMode defaultMode = ConfigService.getGameMode();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).settings),
|
||||
previousPageTitle: AppLocalizations.of(context).games,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).cabo_penalty,
|
||||
prefixIcon: CupertinoIcons.bolt_fill,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey1,
|
||||
initialValue: ConfigService.getCaboPenalty(),
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).point_limit,
|
||||
prefixIcon: FontAwesomeIcons.bullseye,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: ConfigService.getPointLimit(),
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).standard_mode,
|
||||
prefixIcon: CupertinoIcons.square_stack,
|
||||
suffixWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
defaultMode == GameMode.none
|
||||
? AppLocalizations.of(context).no_default_mode
|
||||
: (defaultMode == GameMode.pointLimit
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
const CupertinoListTileChevron()
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final selectedMode = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
defaultMode = selectedMode ?? GameMode.none;
|
||||
});
|
||||
ConfigService.setGameMode(defaultMode);
|
||||
},
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText:
|
||||
AppLocalizations.of(context).reset_to_default,
|
||||
prefixIcon: CupertinoIcons.arrow_counterclockwise,
|
||||
onPressed: () {
|
||||
ConfigService.resetConfig();
|
||||
setState(() {
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
defaultMode = ConfigService.getGameMode();
|
||||
});
|
||||
},
|
||||
)
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game_data,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).import_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_down,
|
||||
onPressed: () async {
|
||||
final status =
|
||||
await LocalStorageService.importJsonFile();
|
||||
showFeedbackDialog(status);
|
||||
},
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).export_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_up,
|
||||
onPressed: () => LocalStorageService.exportGameData(),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).delete_data,
|
||||
prefixIcon: CupertinoIcons.trash,
|
||||
onPressed: () => _deleteAllGames(),
|
||||
),
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).wiki,
|
||||
prefixIcon: CupertinoIcons.book,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubWikiLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).error_found,
|
||||
prefixIcon: FontAwesomeIcons.github,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubIssuesLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).app_version,
|
||||
prefixIcon: CupertinoIcons.tag,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getVersion(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).build,
|
||||
prefixIcon: CupertinoIcons.number,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getBuildNumber(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
])),
|
||||
const SizedBox(height: 50)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm the deletion of all game data.
|
||||
/// When confirmed, it deletes all game data from local storage.
|
||||
void _deleteAllGames() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_data_title),
|
||||
content: Text(AppLocalizations.of(context).delete_data_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
onPressed: () {
|
||||
LocalStorageService.deleteAllGames();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showFeedbackDialog(ImportStatus status) {
|
||||
if (status == ImportStatus.canceled) return;
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
(String, String) _getDialogContent(ImportStatus status) {
|
||||
switch (status) {
|
||||
case ImportStatus.success:
|
||||
return (
|
||||
AppLocalizations.of(context).import_success_title,
|
||||
AppLocalizations.of(context).import_success_message
|
||||
);
|
||||
case ImportStatus.validationError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_validation_error_title,
|
||||
AppLocalizations.of(context).import_validation_error_message
|
||||
);
|
||||
|
||||
case ImportStatus.formatError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_format_error_title,
|
||||
AppLocalizations.of(context).import_format_error_message
|
||||
);
|
||||
case ImportStatus.genericError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_generic_error_title,
|
||||
AppLocalizations.of(context).import_generic_error_message
|
||||
);
|
||||
case ImportStatus.canceled:
|
||||
return ('', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/views/information_view.dart';
|
||||
import 'package:cabo_counter/views/main_menu_view.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
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';
|
||||
|
||||
/// 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});
|
||||
|
||||
@@ -16,8 +25,9 @@ class _TabViewState extends State<TabView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoTabScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
tabBar: CupertinoTabBar(
|
||||
backgroundColor: CustomTheme.backgroundTintColor,
|
||||
backgroundColor: CustomTheme.mainElementBackgroundColor,
|
||||
iconSize: 27,
|
||||
height: 55,
|
||||
items: <BottomNavigationBarItem>[
|
||||
@@ -39,7 +49,7 @@ class _TabViewState extends State<TabView> {
|
||||
if (index == 0) {
|
||||
return const MainMenuView();
|
||||
} else {
|
||||
return const InformationView();
|
||||
return const AboutView();
|
||||
}
|
||||
});
|
||||
},
|
||||
23
lib/presentation/widgets/custom_button.dart
Normal file
23
lib/presentation/widgets/custom_button.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
const CustomButton({super.key, required this.child, this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CustomTheme.buttonBackgroundColor,
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/presentation/widgets/custom_form_row.dart
Normal file
58
lib/presentation/widgets/custom_form_row.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
final Widget? suffixWidget;
|
||||
final void Function()? onPressed;
|
||||
const CustomFormRow({
|
||||
super.key,
|
||||
required this.prefixText,
|
||||
required this.prefixIcon,
|
||||
this.onPressed,
|
||||
this.suffixWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomFormRow> createState() => _CustomFormRowState();
|
||||
}
|
||||
|
||||
class _CustomFormRowState extends State<CustomFormRow> {
|
||||
late Widget suffixWidget;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
suffixWidget = widget.suffixWidget ?? const SizedBox.shrink();
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: widget.onPressed,
|
||||
child: CupertinoFormRow(
|
||||
prefix: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.prefixIcon,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(widget.prefixText),
|
||||
],
|
||||
),
|
||||
padding: suffixWidget is CustomStepper
|
||||
? const EdgeInsets.fromLTRB(15, 0, 0, 0)
|
||||
: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||
child: suffixWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart'; // Für iOS-Style
|
||||
|
||||
class Stepper extends StatefulWidget {
|
||||
/// 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;
|
||||
final int? initialValue;
|
||||
final int step;
|
||||
final ValueChanged<int> onChanged;
|
||||
const Stepper({
|
||||
const CustomStepper({
|
||||
super.key,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
@@ -17,10 +29,10 @@ class Stepper extends StatefulWidget {
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_StepperState createState() => _StepperState();
|
||||
_CustomStepperState createState() => _CustomStepperState();
|
||||
}
|
||||
|
||||
class _StepperState extends State<Stepper> {
|
||||
class _CustomStepperState extends State<CustomStepper> {
|
||||
late int _value;
|
||||
|
||||
@override
|
||||
@@ -34,18 +46,20 @@ class _StepperState extends State<Stepper> {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _decrement,
|
||||
child: const Icon(CupertinoIcons.minus),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text('$_value', style: const TextStyle(fontSize: 18)),
|
||||
child: Text('$_value',
|
||||
style: TextStyle(fontSize: 18, color: CustomTheme.white)),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _increment,
|
||||
child: const Icon(CupertinoIcons.add),
|
||||
),
|
||||
@@ -1,55 +1,109 @@
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
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 int _defaultPointLimit = 100; // Default Value
|
||||
static const int _defaultCaboPenalty = 5; // Default Value
|
||||
static const String _keyGameMode = 'gameMode';
|
||||
// Actual values used in the app
|
||||
static int _pointLimit = 100;
|
||||
static int _caboPenalty = 5;
|
||||
static int _gameMode = -1;
|
||||
// Default values
|
||||
static const int _defaultPointLimit = 100;
|
||||
static const int _defaultCaboPenalty = 5;
|
||||
static const int _defaultGameMode = -1;
|
||||
|
||||
static Future<void> initConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Default values only set if they are not already set
|
||||
prefs.setInt(
|
||||
_keyPointLimit, prefs.getInt(_keyPointLimit) ?? _defaultPointLimit);
|
||||
prefs.setInt(
|
||||
_keyCaboPenalty, prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty);
|
||||
// Initialize pointLimit, caboPenalty, and gameMode from SharedPreferences
|
||||
// If they are not set, use the default values
|
||||
_pointLimit = prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
|
||||
_caboPenalty = prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
|
||||
_gameMode = prefs.getInt(_keyGameMode) ?? _defaultGameMode;
|
||||
|
||||
// Save the initial values to SharedPreferences
|
||||
prefs.setInt(_keyPointLimit, _pointLimit);
|
||||
prefs.setInt(_keyCaboPenalty, _caboPenalty);
|
||||
prefs.setInt(_keyGameMode, _gameMode);
|
||||
}
|
||||
|
||||
/// Getter for the point limit.
|
||||
static Future<int> getPointLimit() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
|
||||
/// Retrieves the current game mode.
|
||||
///
|
||||
/// The game mode is determined based on the stored integer value:
|
||||
/// - `0`: [GameMode.pointLimit]
|
||||
/// - `1`: [GameMode.unlimited]
|
||||
/// - Any other value: [GameMode.none] (-1 is used as a default for no mode)
|
||||
///
|
||||
/// Returns the corresponding [GameMode] enum value.
|
||||
static GameMode getGameMode() {
|
||||
switch (_gameMode) {
|
||||
case 0:
|
||||
return GameMode.pointLimit;
|
||||
case 1:
|
||||
return GameMode.unlimited;
|
||||
default:
|
||||
return GameMode.none;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the game mode for the application.
|
||||
///
|
||||
/// [newGameMode] is the new game mode to be set. It can be one of the following:
|
||||
/// - `GameMode.pointLimit`: The game ends when a pleayer reaches the point limit.
|
||||
/// - `GameMode.unlimited`: Every game goes for infinity until you end it.
|
||||
/// - `GameMode.none`: No default mode set.
|
||||
///
|
||||
/// This method updates the `_gameMode` field and persists the value in `SharedPreferences`.
|
||||
static Future<void> setGameMode(GameMode newGameMode) async {
|
||||
int gameMode;
|
||||
switch (newGameMode) {
|
||||
case GameMode.pointLimit:
|
||||
gameMode = 0;
|
||||
break;
|
||||
case GameMode.unlimited:
|
||||
gameMode = 1;
|
||||
break;
|
||||
default:
|
||||
gameMode = -1;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyGameMode, gameMode);
|
||||
_gameMode = gameMode;
|
||||
}
|
||||
|
||||
static int getPointLimit() => _pointLimit;
|
||||
|
||||
/// Setter for the point limit.
|
||||
/// [newPointLimit] is the new point limit to be set.
|
||||
static Future<void> setPointLimit(int newPointLimit) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyPointLimit, newPointLimit);
|
||||
_pointLimit = newPointLimit;
|
||||
}
|
||||
|
||||
/// Getter for the cabo penalty.
|
||||
static Future<int> getCaboPenalty() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
|
||||
}
|
||||
static int getCaboPenalty() => _caboPenalty;
|
||||
|
||||
/// Setter for the cabo penalty.
|
||||
/// [newCaboPenalty] is the new cabo penalty to be set.
|
||||
static Future<void> setCaboPenalty(int newCaboPenalty) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyCaboPenalty, newCaboPenalty);
|
||||
_caboPenalty = newCaboPenalty;
|
||||
}
|
||||
|
||||
/// Resets the configuration to default values.
|
||||
static Future<void> resetConfig() async {
|
||||
Globals.pointLimit = _defaultPointLimit;
|
||||
Globals.caboPenalty = _defaultCaboPenalty;
|
||||
ConfigService._pointLimit = _defaultPointLimit;
|
||||
ConfigService._caboPenalty = _defaultCaboPenalty;
|
||||
ConfigService._gameMode = _defaultGameMode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyPointLimit, _defaultPointLimit);
|
||||
await prefs.setInt(_keyCaboPenalty, _defaultCaboPenalty);
|
||||
|
||||
@@ -9,11 +9,19 @@ 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 getJsonFile() {
|
||||
/// 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);
|
||||
@@ -31,7 +39,7 @@ class LocalStorageService {
|
||||
print('[local_storage_service.dart] Versuche, Daten zu speichern...');
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
final jsonFile = getJsonFile();
|
||||
final jsonFile = _getGameDataAsJsonFile();
|
||||
await file.writeAsString(jsonFile);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden zwischengespeichert.');
|
||||
@@ -47,6 +55,7 @@ class LocalStorageService {
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
|
||||
// Check if the file exists
|
||||
if (!await file.exists()) {
|
||||
print(
|
||||
'[local_storage_service.dart] Es existiert noch keine Datei mit Spieldaten');
|
||||
@@ -57,12 +66,14 @@ class LocalStorageService {
|
||||
'[local_storage_service.dart] Es existiert bereits eine Datei mit Spieldaten');
|
||||
final jsonString = await file.readAsString();
|
||||
|
||||
// Check if the file is empty
|
||||
if (jsonString.isEmpty) {
|
||||
print('[local_storage_service.dart] Die gefundene Datei ist leer');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await validateJsonSchema(jsonString)) {
|
||||
// Validate the JSON schema
|
||||
if (!await _validateJsonSchema(jsonString, true)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei konnte nicht validiert werden');
|
||||
gameManager.gameList = [];
|
||||
@@ -78,6 +89,11 @@ class LocalStorageService {
|
||||
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;
|
||||
@@ -89,19 +105,27 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to save a JSON file with the current game data.
|
||||
static Future<bool> exportJsonFile() async {
|
||||
final jsonString = getJsonFile();
|
||||
/// 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 result = await FileSaver.instance.saveAs(
|
||||
name: 'cabo_counter_data',
|
||||
final path = await FileSaver.instance.saveAs(
|
||||
name: fileName,
|
||||
bytes: bytes,
|
||||
ext: 'json',
|
||||
mimeType: MimeType.json,
|
||||
);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden exportiert. Dateipfad: $result');
|
||||
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(
|
||||
@@ -110,45 +134,82 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<bool> importJsonFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
dialogTitle: 'Wähle eine Datei mit Spieldaten aus',
|
||||
static Future<ImportStatus> importJsonFile() async {
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
if (path == null) {
|
||||
print(
|
||||
'[local_storage_service.dart] Der Filepicker-Dialog wurde abgebrochen');
|
||||
return false;
|
||||
return ImportStatus.canceled;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonString = await _readFileContent(result.files.single);
|
||||
final jsonString = await _readFileContent(path.files.single);
|
||||
|
||||
if (!await validateJsonSchema(jsonString)) {
|
||||
return false;
|
||||
// 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;
|
||||
}
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
gameManager.gameList = jsonData
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei wurde erfolgreich Importiertn');
|
||||
return true;
|
||||
'[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 false;
|
||||
return ImportStatus.formatError;
|
||||
} on Exception catch (e) {
|
||||
print(
|
||||
'[local_storage_service.dart] Fehler beim Dateizugriff. Exception: $e');
|
||||
return false;
|
||||
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!);
|
||||
@@ -158,15 +219,28 @@ class LocalStorageService {
|
||||
}
|
||||
|
||||
/// Validates the JSON data against the schema.
|
||||
static Future<bool> validateJsonSchema(String jsonString) async {
|
||||
/// 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 schemaString = await rootBundle.loadString('assets/schema.json');
|
||||
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.');
|
||||
print(
|
||||
'[local_storage_service.dart] JSON ist erfolgreich validiert. Typ: ${isGameList ? 'Game List' : 'Single Game'}');
|
||||
return true;
|
||||
}
|
||||
print(
|
||||
|
||||
32
lib/services/version_service.dart
Normal file
32
lib/services/version_service.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class VersionService {
|
||||
static String _version = '-.-.-';
|
||||
static String _buildNumber = '-';
|
||||
|
||||
static Future<void> init() async {
|
||||
var packageInfo = await PackageInfo.fromPlatform();
|
||||
_version = packageInfo.version;
|
||||
_buildNumber = packageInfo.buildNumber;
|
||||
}
|
||||
|
||||
static String getVersionNumber() {
|
||||
return _version;
|
||||
}
|
||||
|
||||
static String getVersion() {
|
||||
if (_version == '-.-.-') {
|
||||
return getVersionNumber();
|
||||
}
|
||||
return '${Constants.appDevPhase} $_version';
|
||||
}
|
||||
|
||||
static String getBuildNumber() {
|
||||
return _buildNumber;
|
||||
}
|
||||
|
||||
static String getVersionWithBuild() {
|
||||
return '$_version ($_buildNumber)';
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CustomTheme {
|
||||
static Color white = CupertinoColors.white;
|
||||
static Color primaryColor = CupertinoColors.systemGreen;
|
||||
static Color backgroundColor = const Color(0xFF101010);
|
||||
static Color backgroundTintColor = CupertinoColors.darkBackgroundGray;
|
||||
|
||||
static TextStyle modeTitle = TextStyle(
|
||||
color: primaryColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
static const TextStyle modeDescription = TextStyle(
|
||||
fontSize: 16,
|
||||
);
|
||||
|
||||
static TextStyle rowTitle = TextStyle(
|
||||
fontSize: 20,
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
static TextStyle roundTitle = TextStyle(
|
||||
fontSize: 60,
|
||||
color: white,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
class Globals {
|
||||
static int pointLimit = 100;
|
||||
static int caboPenalty = 5;
|
||||
static String appDevPhase = 'Beta';
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
import 'package:cabo_counter/data/models/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/views/graph_view.dart';
|
||||
import 'package:cabo_counter/views/round_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActiveGameView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
const ActiveGameView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_ActiveGameViewState createState() => _ActiveGameViewState();
|
||||
}
|
||||
|
||||
class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.gameSession,
|
||||
builder: (context, _) {
|
||||
List<int> sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(widget.gameSession.gameTitle),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.gameSession.players.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
int playerIndex = sortedPlayerIndices[index];
|
||||
return CupertinoListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
_getPlacementPrefix(index),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
widget.gameSession.players[playerIndex],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${widget.gameSession.playerScores[playerIndex]} '
|
||||
'${AppLocalizations.of(context).points}')
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).rounds,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.gameSession.roundNumber,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(
|
||||
'${AppLocalizations.of(context).round} ${index + 1}',
|
||||
),
|
||||
trailing: index + 1 !=
|
||||
widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished ==
|
||||
true
|
||||
? (const Text('\u{2705}',
|
||||
style: TextStyle(fontSize: 22)))
|
||||
: const Text('\u{23F3}',
|
||||
style: TextStyle(fontSize: 22)),
|
||||
onTap: () async {
|
||||
// ignore: unused_local_variable
|
||||
final val = await Navigator.of(context,
|
||||
rootNavigator: true)
|
||||
.push(
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => RoundView(
|
||||
gameSession: widget.gameSession,
|
||||
roundNumber: index + 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => GraphView(
|
||||
gameSession: widget.gameSession,
|
||||
)))),
|
||||
CupertinoListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).delete_game,
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
)),
|
||||
onTap: () {},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.new_game_same_settings,
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
))),
|
||||
CupertinoListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).export_game,
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a list of player indices sorted by their scores in
|
||||
/// ascending order.
|
||||
List<int> _getSortedPlayerIndices() {
|
||||
List<int> playerIndices =
|
||||
List<int>.generate(widget.gameSession.players.length, (index) => index);
|
||||
// Sort the indices based on the summed points
|
||||
playerIndices.sort((a, b) {
|
||||
int scoreA = widget.gameSession.playerScores[a];
|
||||
int scoreB = widget.gameSession.playerScores[b];
|
||||
return scoreA.compareTo(scoreB);
|
||||
});
|
||||
return playerIndices;
|
||||
}
|
||||
|
||||
/// Returns a widget that displays the placement prefix based on the index.
|
||||
/// First three places are represented by medals, and the rest are numbered.
|
||||
/// [index] is the index of the player in the descending sorted list.
|
||||
Widget _getPlacementPrefix(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return const Text(
|
||||
'\u{1F947}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
case 1:
|
||||
return const Text(
|
||||
'\u{1F948}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
case 2:
|
||||
return const Text(
|
||||
'\u{1F949}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
default:
|
||||
return Text(
|
||||
' ${index + 1}.',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import 'package:cabo_counter/data/models/game_manager.dart';
|
||||
import 'package:cabo_counter/data/models/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/views/mode_selection_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CreateGame extends StatefulWidget {
|
||||
const CreateGame({super.key});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_CreateGameState createState() => _CreateGameState();
|
||||
}
|
||||
|
||||
class _CreateGameState extends State<CreateGame> {
|
||||
final List<TextEditingController> _playerNameTextControllers = [
|
||||
TextEditingController()
|
||||
];
|
||||
final TextEditingController _gameTitleTextController =
|
||||
TextEditingController();
|
||||
|
||||
/// Maximum number of players allowed in the game.
|
||||
final int maxPlayers = 5;
|
||||
|
||||
/// Variable to store the selected game mode.
|
||||
bool? selectedMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: AppLocalizations.of(context).overview,
|
||||
middle: Text(AppLocalizations.of(context).new_game),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
maxLength: 16,
|
||||
prefix: Text(AppLocalizations.of(context).name),
|
||||
textAlign: TextAlign.right,
|
||||
placeholder: AppLocalizations.of(context).game_title,
|
||||
controller: _gameTitleTextController,
|
||||
),
|
||||
),
|
||||
// Spielmodus-Auswahl mit Chevron
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
decoration: const BoxDecoration(),
|
||||
readOnly: true,
|
||||
prefix: Text(AppLocalizations.of(context).mode),
|
||||
suffix: Row(
|
||||
children: [
|
||||
Text(
|
||||
selectedMode == null
|
||||
? AppLocalizations.of(context).select_mode
|
||||
: (selectedMode!
|
||||
? '${Globals.pointLimit} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
const CupertinoListTileChevron(),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: Globals.pointLimit,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
setState(() {
|
||||
selectedMode = selected;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).players,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _playerNameTextControllers.length +
|
||||
1, // +1 für den + Button
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _playerNameTextControllers.length) {
|
||||
// + Button als letztes Element
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
CupertinoIcons.add_circled,
|
||||
color: CupertinoColors.activeGreen,
|
||||
size: 25,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context).add_player,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_playerNameTextControllers.length < maxPlayers) {
|
||||
setState(() {
|
||||
_playerNameTextControllers
|
||||
.add(TextEditingController());
|
||||
});
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context)
|
||||
.max_players_title),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.max_players_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child:
|
||||
Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Spieler-Einträge
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circle_fill,
|
||||
color: CupertinoColors.destructiveRed,
|
||||
size: 25,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_playerNameTextControllers[index].dispose();
|
||||
_playerNameTextControllers.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: _playerNameTextControllers[index],
|
||||
placeholder:
|
||||
'${AppLocalizations.of(context).player} ${index + 1}',
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).create_game,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_gameTitleTextController.text == '') {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).no_gameTitle_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).no_gameTitle_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (selectedMode == null) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).no_mode_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).no_mode_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_playerNameTextControllers.length < 2) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).min_players_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).min_players_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!everyPlayerHasAName()) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).no_name_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).no_name_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
}
|
||||
GameSession gameSession = GameSession(
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: players,
|
||||
pointLimit: Globals.pointLimit,
|
||||
caboPenalty: Globals.caboPenalty,
|
||||
isPointsLimitEnabled: selectedMode!,
|
||||
);
|
||||
final index = await gameManager.addGameSession(gameSession);
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ActiveGameView(
|
||||
gameSession: gameManager.gameList[index])));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
))));
|
||||
}
|
||||
|
||||
bool everyPlayerHasAName() {
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
if (controller.text == '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:cabo_counter/data/models/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class GraphView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const GraphView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
/// List of colors for the graph lines.
|
||||
List<Color> lineColors = [
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.orange.shade400,
|
||||
Colors.purple,
|
||||
Colors.green,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).game_process),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: SfCartesianChart(
|
||||
legend:
|
||||
const Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(),
|
||||
primaryYAxis: const NumericAxis(),
|
||||
series: getCumulativeScores(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of LineSeries representing the cumulative scores of each player.
|
||||
/// Each series contains data points for each round, showing the cumulative score up to that round.
|
||||
/// The x-axis represents the round number, and the y-axis represents the cumulative score.
|
||||
List<LineSeries<(int, int), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
for (var round in rounds) {
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
runningTotals[i] += round.scores[i];
|
||||
cumulativeScores[i].add(runningTotals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a list of LineSeries for each player
|
||||
/// Each series contains data points for each round
|
||||
return List.generate(playerCount, (i) {
|
||||
final data = List.generate(
|
||||
cumulativeScores[i].length,
|
||||
(j) => (j + 1, cumulativeScores[i][j]), // (round, score)
|
||||
);
|
||||
|
||||
/// Create a LineSeries for the player
|
||||
/// The xValueMapper maps the round number, and the yValueMapper maps the cumulative score.
|
||||
return LineSeries<(int, int), int>(
|
||||
name: playerNames[i],
|
||||
dataSource: data,
|
||||
xValueMapper: (record, _) => record.$1, // Runde
|
||||
yValueMapper: (record, _) => record.$2, // Punktestand
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: lineColors[i],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class InformationView extends StatelessWidget {
|
||||
const InformationView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).about),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app_name,
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('assets/cabo_counter-logo_rounded.png'),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).about_text,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
const Text(
|
||||
'\u00A9 Felix Kirchner',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('https://www.instagram.com/fx.kr')),
|
||||
icon: const Icon(FontAwesomeIcons.instagram)),
|
||||
IconButton(
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse('mailto:felix.kirchner.fk@gmail.com')),
|
||||
icon: const Icon(CupertinoIcons.envelope)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('https://www.github.com/flixcoo')),
|
||||
icon: const Icon(FontAwesomeIcons.github)),
|
||||
],
|
||||
),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import 'package:cabo_counter/data/models/game_manager.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/views/settings_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MainMenuView extends StatefulWidget {
|
||||
const MainMenuView({super.key});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_MainMenuViewState createState() => _MainMenuViewState();
|
||||
}
|
||||
|
||||
class _MainMenuViewState extends State<MainMenuView> {
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
LocalStorageService.loadGameSessions().then((_) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
});
|
||||
gameManager.addListener(_updateView);
|
||||
}
|
||||
|
||||
void _updateView() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: gameManager,
|
||||
builder: (context, _) {
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const SettingsView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.settings, size: 30)),
|
||||
middle: const Text('Cabo Counter'),
|
||||
trailing: IconButton(
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const CreateGame(),
|
||||
),
|
||||
)
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.add)),
|
||||
),
|
||||
child: CupertinoPageScaffold(
|
||||
child: SafeArea(
|
||||
child: _isLoading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: gameManager.gameList.isEmpty
|
||||
? Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center, // Oben ausrichten
|
||||
children: [
|
||||
const SizedBox(height: 30), // Abstand von oben
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {}),
|
||||
child: Icon(
|
||||
CupertinoIcons.plus,
|
||||
size: 60,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 10), // Abstand von oben
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 70),
|
||||
child: Text(
|
||||
'Ganz schön leer hier...\nFüge über den Button oben rechts eine neue Runde hinzu.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: gameManager.gameList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final session = gameManager.gameList[index];
|
||||
return ListenableBuilder(
|
||||
listenable: session,
|
||||
builder: (context, _) {
|
||||
return Dismissible(
|
||||
key: Key(session.gameTitle),
|
||||
background: Container(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding:
|
||||
const EdgeInsets.only(left: 20.0),
|
||||
child: const Icon(
|
||||
CupertinoIcons.delete,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
direction: DismissDirection.startToEnd,
|
||||
confirmDismiss: (direction) async {
|
||||
final String gameTitle = gameManager
|
||||
.gameList[index].gameTitle;
|
||||
return await _showDeleteGamePopup(
|
||||
gameTitle);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
gameManager.removeGameSession(index);
|
||||
},
|
||||
dismissThresholds: const {
|
||||
DismissDirection.startToEnd: 0.6
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0),
|
||||
child: CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(session.gameTitle),
|
||||
subtitle:
|
||||
session.isGameFinished == true
|
||||
? Text(
|
||||
'\u{1F947} ${session.winner}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14),
|
||||
)
|
||||
: Text(
|
||||
'${AppLocalizations.of(context).mode}: ${_translateGameMode(session.isPointsLimitEnabled)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14),
|
||||
),
|
||||
trailing: Row(
|
||||
children: [
|
||||
Text('${session.roundNumber}'),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(CupertinoIcons
|
||||
.arrow_2_circlepath_circle_fill),
|
||||
const SizedBox(width: 15),
|
||||
Text('${session.players.length}'),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(
|
||||
CupertinoIcons.person_2_fill),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
//ignore: unused_local_variable
|
||||
final val = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
ActiveGameView(
|
||||
gameSession: gameManager
|
||||
.gameList[index]),
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Translates the game mode boolean into the corresponding String.
|
||||
/// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'.
|
||||
String _translateGameMode(bool pointLimit) {
|
||||
if (pointLimit) {
|
||||
return '${Globals.pointLimit} ${AppLocalizations.of(context).points}';
|
||||
}
|
||||
return AppLocalizations.of(context).unlimited;
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog to delete all game sessions.
|
||||
/// Returns true if the user confirms the deletion, false otherwise.
|
||||
/// [gameTitle] is the title of the game session to be deleted.
|
||||
Future<bool> _showDeleteGamePopup(String gameTitle) async {
|
||||
bool? shouldDelete = await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_game_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).delete_game_message(gameTitle)),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
return shouldDelete;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
gameManager.removeListener(_updateView);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class ModeSelectionMenu extends StatelessWidget {
|
||||
final int pointLimit;
|
||||
const ModeSelectionMenu({super.key, required this.pointLimit});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).select_game_mode),
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text('$pointLimit ${AppLocalizations.of(context).points}',
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)
|
||||
.point_limit_description(pointLimit),
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).unlimited,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).unlimited_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
import 'package:cabo_counter/data/models/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
|
||||
class RoundView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
final int roundNumber;
|
||||
const RoundView(
|
||||
{super.key, required this.roundNumber, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_RoundViewState createState() => _RoundViewState();
|
||||
}
|
||||
|
||||
class _RoundViewState extends State<RoundView> {
|
||||
/// The current game session.
|
||||
late GameSession gameSession = widget.gameSession;
|
||||
|
||||
/// Index of the player who said CABO.
|
||||
int _caboPlayerIndex = 0;
|
||||
|
||||
/// Index of the player who has Kamikaze.
|
||||
/// Default is null (no Kamikaze player).
|
||||
int? _kamikazePlayerIndex;
|
||||
|
||||
/// List of text controllers for the score text fields.
|
||||
late final List<TextEditingController> _scoreControllerList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => TextEditingController(),
|
||||
);
|
||||
|
||||
/// List of focus nodes for the score text fields.
|
||||
late final List<FocusNode> _focusNodeList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => FocusNode(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print('=== Runde ${widget.roundNumber} geöffnet ===');
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished == true) {
|
||||
print(
|
||||
'Diese wurde bereits gespielt, deshalb werden die alten Punktestaende angezeigt');
|
||||
|
||||
// If the current round has already been played, the text fields
|
||||
// are filled with the scores from this round
|
||||
for (int i = 0; i < _scoreControllerList.length; i++) {
|
||||
_scoreControllerList[i].text =
|
||||
gameSession.roundList[widget.roundNumber - 1].scores[i].toString();
|
||||
}
|
||||
_caboPlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].caboPlayerIndex;
|
||||
_kamikazePlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].kamikazePlayerIndex;
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: true,
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context, widget.gameSession)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 100 + bottomInset),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).round} ${widget.roundNumber}',
|
||||
style: CustomTheme.roundTitle),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppLocalizations.of(context).who_said_cabo,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.gameSession.players.length > 3 ? 5 : 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
unselectedColor: CustomTheme.backgroundTintColor,
|
||||
selectedColor: CustomTheme.primaryColor,
|
||||
groupValue: _caboPlayerIndex,
|
||||
children: Map.fromEntries(widget.gameSession.players
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.gameSession
|
||||
.getLengthOfPlayerNames() >
|
||||
20
|
||||
? (widget.gameSession
|
||||
.getLengthOfPlayerNames() >
|
||||
32
|
||||
? 5
|
||||
: 10)
|
||||
: 15,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: widget.gameSession
|
||||
.getLengthOfPlayerNames() >
|
||||
28
|
||||
? 14
|
||||
: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_caboPlayerIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).player),
|
||||
trailing: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points))),
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Center(
|
||||
child: Text(AppLocalizations.of(context)
|
||||
.kamikaze))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.gameSession.players.length,
|
||||
itemBuilder: (context, index) {
|
||||
final name = widget.gameSession.players[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 20),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CupertinoColors.secondaryLabel,
|
||||
title: Row(children: [Text(name)]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[index]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: CupertinoTextField(
|
||||
maxLength: 3,
|
||||
focusNode: _focusNodeList[index],
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
signed: true,
|
||||
decimal: false,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textInputAction: index ==
|
||||
widget.gameSession.players
|
||||
.length -
|
||||
1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.next,
|
||||
controller: _scoreControllerList[index],
|
||||
placeholder:
|
||||
AppLocalizations.of(context).points,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) =>
|
||||
_focusNextTextfield(index),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 50),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_kamikazePlayerIndex =
|
||||
(_kamikazePlayerIndex == index)
|
||||
? null
|
||||
: index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _kamikazePlayerIndex == index
|
||||
? CupertinoColors.systemRed
|
||||
: CupertinoColors
|
||||
.tertiarySystemFill,
|
||||
border: Border.all(
|
||||
color: _kamikazePlayerIndex == index
|
||||
? CupertinoColors.systemRed
|
||||
: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
child: _kamikazePlayerIndex == index
|
||||
? const Icon(
|
||||
CupertinoIcons.exclamationmark,
|
||||
size: 16,
|
||||
color: CupertinoColors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 22),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomInset,
|
||||
child: KeyboardVisibilityBuilder(builder: (context, visible) {
|
||||
if (!visible) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
color: CustomTheme.backgroundTintColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
Navigator.pop(context, widget.gameSession);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
Navigator.pop(context, widget.gameSession);
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacement(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => RoundView(
|
||||
gameSession: widget.gameSession,
|
||||
roundNumber: widget.roundNumber + 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Focuses the next text field in the list of text fields.
|
||||
/// [index] is the index of the current text field.
|
||||
void _focusNextTextfield(int index) {
|
||||
if (index < widget.gameSession.players.length - 1) {
|
||||
FocusScope.of(context).requestFocus(_focusNodeList[index + 1]);
|
||||
} else {
|
||||
_focusNodeList[index].unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the inputs for the round are valid.
|
||||
/// Returns true if the inputs are valid, false otherwise.
|
||||
/// Round Inputs are valid if every player has a score or
|
||||
/// kamikaze is selected for a player
|
||||
bool _areRoundInputsValid() {
|
||||
if (_areTextFieldsEmpty() && _kamikazePlayerIndex == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if any of the text fields for the players points are empty.
|
||||
/// Returns true if any of the text fields is empty, false otherwise.
|
||||
bool _areTextFieldsEmpty() {
|
||||
for (TextEditingController t in _scoreControllerList) {
|
||||
if (t.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Finishes the current round.
|
||||
/// It first determines, ifCalls the [_calculateScoredPoints()] method to calculate the points for
|
||||
/// every player. If the round is the highest round played in this game,
|
||||
/// it expands the player score lists. At the end it updates the score
|
||||
/// array for the game.
|
||||
void _finishRound() {
|
||||
print('====================================');
|
||||
print('Runde ${widget.roundNumber} beendet');
|
||||
// The shown round is smaller than the newest round
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber) {
|
||||
print('Da diese Runde bereits gespielt wurde, werden die alten '
|
||||
'Punktestaende ueberschrieben');
|
||||
}
|
||||
if (_kamikazePlayerIndex != null) {
|
||||
print('${widget.gameSession.players[_kamikazePlayerIndex!]} hat Kamikaze '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen 50 Punkte');
|
||||
widget.gameSession
|
||||
.applyKamikaze(widget.roundNumber, _kamikazePlayerIndex!);
|
||||
} else {
|
||||
List<int> roundScores = [];
|
||||
for (TextEditingController c in _scoreControllerList) {
|
||||
if (c.text.isNotEmpty) roundScores.add(int.parse(c.text));
|
||||
}
|
||||
widget.gameSession.calculateScoredPoints(
|
||||
widget.roundNumber, roundScores, _caboPlayerIndex);
|
||||
}
|
||||
widget.gameSession.updatePoints();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
print('Das Spiel ist beendet');
|
||||
} else if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _scoreControllerList) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final focusNode in _focusNodeList) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/widgets/stepper.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
UniqueKey _stepperKey1 = UniqueKey();
|
||||
UniqueKey _stepperKey2 = UniqueKey();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).settings),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoListTile(
|
||||
padding: EdgeInsets.zero,
|
||||
title: Text(AppLocalizations.of(context).cabo_penalty),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).cabo_penalty_subtitle),
|
||||
trailing: Stepper(
|
||||
key: _stepperKey1,
|
||||
initialValue: Globals.caboPenalty,
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
Globals.caboPenalty = newCaboPenalty;
|
||||
});
|
||||
},
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoListTile(
|
||||
padding: EdgeInsets.zero,
|
||||
title: Text(AppLocalizations.of(context).point_limit),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context).point_limit_subtitle),
|
||||
trailing: Stepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: Globals.pointLimit,
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
Globals.pointLimit = newPointLimit;
|
||||
});
|
||||
},
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Center(
|
||||
heightFactor: 0.9,
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => setState(() {
|
||||
ConfigService.resetConfig();
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
}),
|
||||
child:
|
||||
Text(AppLocalizations.of(context).reset_to_default),
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game_data,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30),
|
||||
child: Center(
|
||||
heightFactor: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
color: CustomTheme.primaryColor,
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).import_data,
|
||||
style:
|
||||
TextStyle(color: CustomTheme.backgroundColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success =
|
||||
await LocalStorageService.importJsonFile();
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)
|
||||
.error),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)
|
||||
.error_import),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
.ok),
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
CupertinoButton(
|
||||
color: CustomTheme.primaryColor,
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).export_data,
|
||||
style:
|
||||
TextStyle(color: CustomTheme.backgroundColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success =
|
||||
await LocalStorageService.exportJsonFile();
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).error),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.error_export),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child:
|
||||
Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(AppLocalizations.of(context).error_found),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 30),
|
||||
child: Center(
|
||||
child: CupertinoButton(
|
||||
onPressed: () => launchUrl(Uri.parse(
|
||||
'https://github.com/flixcoo/Cabo-Counter/issues')),
|
||||
child: Text(AppLocalizations.of(context).create_issue),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: _getPackageInfo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
'${Globals.appDevPhase} ${snapshot.data!.version} '
|
||||
'(${AppLocalizations.of(context).build} ${snapshot.data!.buildNumber})',
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'${AppLocalizations.of(context).app_version} -.-.- (${AppLocalizations.of(context).build} -)',
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
AppLocalizations.of(context).load_version,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PackageInfo> _getPackageInfo() async {
|
||||
return await PackageInfo.fromPlatform();
|
||||
}
|
||||
}
|
||||
12
pubspec.yaml
12
pubspec.yaml
@@ -2,7 +2,7 @@ name: cabo_counter
|
||||
description: "Mobile app for the card game Cabo"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.3.2+245
|
||||
version: 0.5.8+674
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -26,6 +26,13 @@ dependencies:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
syncfusion_flutter_charts: ^30.1.37
|
||||
uuid: ^4.5.1
|
||||
rate_my_app: ^2.3.2
|
||||
reorderables: ^0.4.2
|
||||
collection: ^1.18.0
|
||||
confetti: ^0.6.0
|
||||
flutter_oss_licenses: ^2.0.1
|
||||
google_fonts: ^6.3.0
|
||||
drift: ^2.26.1
|
||||
drift_flutter: ^0.2.4
|
||||
|
||||
@@ -42,4 +49,5 @@ flutter:
|
||||
uses-material-design: false
|
||||
assets:
|
||||
- assets/cabo_counter-logo_rounded.png
|
||||
- assets/schema.json
|
||||
- assets/game_list-schema.json
|
||||
- assets/game-schema.json
|
||||
|
||||
@@ -9,6 +9,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
session = GameSession(
|
||||
id: '1',
|
||||
createdAt: testDate,
|
||||
gameTitle: testTitle,
|
||||
players: testPlayers,
|
||||
@@ -61,11 +62,6 @@ void main() {
|
||||
});
|
||||
|
||||
group('Helper Functions', () {
|
||||
test('getLengthOfPlayerNames', () {
|
||||
expect(session.getLengthOfPlayerNames(),
|
||||
equals(15)); // Alice(5) + Bob(3) + Charlie(7)
|
||||
});
|
||||
|
||||
test('increaseRound', () {
|
||||
expect(session.roundNumber, 1);
|
||||
session.increaseRound();
|
||||
@@ -115,15 +111,15 @@ void main() {
|
||||
expect(session.roundList[0].caboPlayerIndex, 0);
|
||||
});
|
||||
|
||||
test('updatePoints - game not finished', () async {
|
||||
test('updatePoints - game not finished', () {
|
||||
session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.isGameFinished, isFalse);
|
||||
});
|
||||
|
||||
test('updatePoints - game finished', () async {
|
||||
test('updatePoints - game finished', () {
|
||||
session.addRoundScoresToList(1, [101, 20, 30], [101, 20, 30], 0);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.isGameFinished, isTrue);
|
||||
});
|
||||
|
||||
@@ -155,9 +151,9 @@ void main() {
|
||||
expect(session.playerScores, equals([50, 0, 30]));
|
||||
});
|
||||
|
||||
test('_setWinner via updatePoints', () async {
|
||||
test('_setWinner via updatePoints', () {
|
||||
session.addRoundScoresToList(1, [101, 20, 30], [101, 0, 30], 1);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.winner, 'Bob'); // Bob has lowest score (20)
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user