diff --git a/.gitignore b/.gitignore
index 50aec09..09b5d34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,3 @@
-increment_build_number.sh
-
# Do not remove or rename entries in this file, only add new ones
# See https://github.com/flutter/flutter/issues/128635 for more context.
diff --git a/.metadata b/.metadata
index fbf1e56..7f3042e 100644
--- a/.metadata
+++ b/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
+ revision: "b25305a8832cfc6ba632a7f87ad455e319dccce8"
channel: "stable"
project_type: app
@@ -13,14 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- - platform: ios
- create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- - platform: web
- create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
+ create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ - platform: android
+ create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
# User provided section
diff --git a/README.md b/README.md
index f3f16cd..464f55b 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# CABO Counter
-
+


-
+



diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..fa964b0
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "flixcoo.cabo_counter"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = "27.0.12077973"
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "flixcoo.cabo_counter"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ebcf66d
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..db35381
Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ
diff --git a/android/app/src/main/kotlin/flixcoo/cabo_counter/MainActivity.kt b/android/app/src/main/kotlin/flixcoo/cabo_counter/MainActivity.kt
new file mode 100644
index 0000000..2139efc
--- /dev/null
+++ b/android/app/src/main/kotlin/flixcoo/cabo_counter/MainActivity.kt
@@ -0,0 +1,5 @@
+package flixcoo.cabo_counter
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..9933510
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..9933510
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..4ae7d12
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..4ae7d12
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..aae21ce
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..7cb62a0
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..44cfaeb
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..dce96af
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..0163710
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..7af803a
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..d052c9a
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..5cd66b3
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..740ff80
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..ddffe3f
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..2a2e4e5
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..c67377b
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..90989d6
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..20c9c09
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..4fbcecd
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..c75fa72
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..2a1e274
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..47668b6
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..b9049c9
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..c83cd30
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
new file mode 100644
index 0000000..1249793
--- /dev/null
+++ b/android/app/src/main/res/values-night-v31/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..a37ba7a
--- /dev/null
+++ b/android/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FF101010
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..a6b9d9b
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..39ba5b8
--- /dev/null
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..a37ba7a
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FF101010
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7f2cb72
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..89176ef
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,21 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..f018a61
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4ef43f
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..ab39a10
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,25 @@
+pluginManagement {
+ val flutterSdkPath = run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.7.3" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/assets/cabo-counter-logo_rounded.png b/assets/cabo-counter-logo_rounded.png
deleted file mode 100644
index 68d0a13..0000000
Binary files a/assets/cabo-counter-logo_rounded.png and /dev/null differ
diff --git a/assets/cabo_counter-logo_rounded.png b/assets/cabo_counter-logo_rounded.png
new file mode 100644
index 0000000..90ffa5a
Binary files /dev/null and b/assets/cabo_counter-logo_rounded.png differ
diff --git a/assets/schema.json b/assets/schema.json
new file mode 100644
index 0000000..17d7faa
--- /dev/null
+++ b/assets/schema.json
@@ -0,0 +1,94 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Generated schema for cabo game data",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "type": "string"
+ },
+ "gameTitle": {
+ "type": "string"
+ },
+ "players": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pointLimit": {
+ "type": "number"
+ },
+ "caboPenalty": {
+ "type": "number"
+ },
+ "isPointsLimitEnabled": {
+ "type": "boolean"
+ },
+ "isGameFinished": {
+ "type": "boolean"
+ },
+ "winner": {
+ "type": "string"
+ },
+ "roundNumber": {
+ "type": "number"
+ },
+ "playerScores": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "roundList": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "roundNum": {
+ "type": "number"
+ },
+ "caboPlayerIndex": {
+ "type": "number"
+ },
+ "kamikazePlayerIndex": {
+ "type": ["number", "null"]
+ },
+ "scores": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "scoreUpdates": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ },
+ "required": [
+ "roundNum",
+ "caboPlayerIndex",
+ "scores",
+ "scoreUpdates"
+ ]
+ }
+ }
+ },
+ "required": [
+ "createdAt",
+ "gameTitle",
+ "players",
+ "pointLimit",
+ "caboPenalty",
+ "isPointsLimitEnabled",
+ "isGameFinished",
+ "winner",
+ "roundNumber",
+ "playerScores",
+ "roundList"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/increment_build_number.sh b/increment_build_number.sh
new file mode 100644
index 0000000..7d209bc
--- /dev/null
+++ b/increment_build_number.sh
@@ -0,0 +1,2 @@
+build_number=$(awk -F'+' '/version:/ {print $2}' pubspec.yaml); build_number=${build_number:-0};
+sed -i '' -E "s/(version: [0-9]+\.[0-9]+\.[0-9]\+)[0-9]*/\1$((build_number + 1))/" pubspec.yaml
\ No newline at end of file
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 804f441..21a9acd 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -13,9 +13,9 @@
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
BA5CEC53A2821E187DBB1FD7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0BC0DD8B3C0A792C3146B81 /* Pods_RunnerTests.framework */; };
D75B248D5087FCA1FE89A25D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8C4A61D5CAF34BC96AB7335 /* Pods_Runner.framework */; };
+ DCDAA4912DF6315D0037AD02 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCDAA4902DF6315D0037AD02 /* Launch Screen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -58,11 +58,11 @@
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
A1E03C9434E30A36490D45FD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
BD7B095CB3C8E1C485D27AF6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
C8C4A61D5CAF34BC96AB7335 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ DCDAA4902DF6315D0037AD02 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; };
E5945FF93491A654655A9828 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
F0BC0DD8B3C0A792C3146B81 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -145,12 +145,12 @@
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ DCDAA4902DF6315D0037AD02 /* Launch Screen.storyboard */,
);
path = Runner;
sourceTree = "";
@@ -259,9 +259,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ DCDAA4912DF6315D0037AD02 /* Launch Screen.storyboard in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -400,14 +400,6 @@
name = Main.storyboard;
sourceTree = "";
};
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C147001CF9000F007C117D /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -469,6 +461,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -478,13 +471,13 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Cabo Counter";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
- IPHONEOS_DEPLOYMENT_TARGET = 18.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.flixcoo.caboCounter;
+ PRODUCT_BUNDLE_IDENTIFIER = flixcoo.caboCounter;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -666,6 +659,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -675,13 +669,13 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Cabo Counter";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
- IPHONEOS_DEPLOYMENT_TARGET = 18.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.flixcoo.caboCounter;
+ PRODUCT_BUNDLE_IDENTIFIER = flixcoo.caboCounter;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -701,6 +695,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -710,13 +705,13 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Cabo Counter";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
- IPHONEOS_DEPLOYMENT_TARGET = 18.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.flixcoo.caboCounter;
+ PRODUCT_BUNDLE_IDENTIFIER = flixcoo.caboCounter;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 1728d10..52bde3b 100644
--- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
index f3c2851..d4f5a74 100644
--- a/ios/Runner/Base.lproj/Main.storyboard
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -1,8 +1,9 @@
-
-
+
+
+
-
-
+
+
@@ -14,13 +15,14 @@
-
+
-
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index f992151..48dde5b 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,46 +2,52 @@
- CADisableMinimumFrameDurationOnPhone
+ LSApplicationCategoryType
+ public.app-category.utilities
+ ITSAppUsesNonExemptEncryption
+
+ LSRequiresIPhoneOS
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
+ UIApplicationSupportsIndirectInputEvents
+
+ CFBundleSignature
+ ????
CFBundleDisplayName
Cabo Counter
- CFBundleExecutable
- $(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
CFBundleName
cabo_counter_app
CFBundlePackageType
APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ UILaunchStoryboardName
+ Launch Screen.storyboard
+ UIMainStoryboardFile
+ Main
+ NSPhotoLibraryUsageDescription
+ This app does not access your photo library. This message is required for technical reasons only.
LSApplicationQueriesSchemes
https
http
instagram
- LSRequiresIPhoneOS
-
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
UIStatusBarHidden
UIStatusBarStyle
- UIStatusBarStyleDarkContent
+ UIStatusBarStyleLightContent
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
diff --git a/ios/Runner/Launch Screen.storyboard b/ios/Runner/Launch Screen.storyboard
new file mode 100644
index 0000000..0e405e7
--- /dev/null
+++ b/ios/Runner/Launch Screen.storyboard
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/data/game_manager.dart b/lib/data/game_manager.dart
new file mode 100644
index 0000000..94b6287
--- /dev/null
+++ b/lib/data/game_manager.dart
@@ -0,0 +1,41 @@
+import 'package:cabo_counter/data/game_session.dart';
+import 'package:cabo_counter/services/local_storage_service.dart';
+import 'package:flutter/foundation.dart';
+
+class GameManager extends ChangeNotifier {
+ List gameList = [];
+
+ /// Adds a new game session to the list and sorts it by creation date.
+ /// Takes a [GameSession] object as input. It then adds the session to 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.
+ /// Returns the index of the newly added session in the sorted list.
+ Future addGameSession(GameSession session) async {
+ 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.');
+ return gameList.indexOf(session);
+ }
+
+ /// 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) {
+ gameList[index].removeListener(notifyListeners);
+ gameList.removeAt(index);
+ notifyListeners();
+ LocalStorageService.saveGameSessions();
+ }
+}
+
+final gameManager = GameManager();
diff --git a/lib/data/game_session.dart b/lib/data/game_session.dart
index 0403822..4896e02 100644
--- a/lib/data/game_session.dart
+++ b/lib/data/game_session.dart
@@ -1,43 +1,77 @@
import 'package:cabo_counter/data/round.dart';
+import 'package:flutter/cupertino.dart';
/// This class represents a game session for Cabo game.
/// [createdAt] is the timestamp of when the game session was created.
/// [gameTitle] is the title of the game.
-/// [gameHasPointLimit] is a boolean indicating if the game has the default
+/// [isPointsLimitEnabled] is a boolean indicating if the game has the default
/// point limit of 101 points or not.
/// [players] is a string list of player names.
/// [playerScores] is a list of the summed scores of all players.
/// [roundNumber] is the current round number.
/// [isGameFinished] is a boolean indicating if the game has ended yet.
/// [winner] is the name of the player who won the game.
-class GameSession {
- final DateTime createdAt = DateTime.now();
+class GameSession extends ChangeNotifier {
+ final DateTime createdAt;
final String gameTitle;
- final bool gameHasPointLimit;
final List players;
- late List playerScores;
- List roundList = [];
- int roundNumber = 1;
+ final int pointLimit;
+ final int caboPenalty;
+ final bool isPointsLimitEnabled;
bool isGameFinished = false;
String winner = '';
+ int roundNumber = 1;
+ late List playerScores;
+ List roundList = [];
GameSession({
+ required this.createdAt,
required this.gameTitle,
- required this.gameHasPointLimit,
required this.players,
+ required this.pointLimit,
+ required this.caboPenalty,
+ required this.isPointsLimitEnabled,
}) {
playerScores = List.filled(players.length, 0);
}
@override
- String toString() {
+ toString() {
return ('GameSession: [createdAt: $createdAt, gameTitle: $gameTitle, '
- 'gameHasPointLimit: $gameHasPointLimit, players: $players, '
- 'playerScores: $playerScores, roundList: $roundList, '
- 'roundNumber: $roundNumber, isGameFinished: $isGameFinished, '
- 'winner: $winner]');
+ 'isPointsLimitEnabled: $isPointsLimitEnabled, pointLimit: $pointLimit, caboPenalty: $caboPenalty,'
+ ' players: $players, playerScores: $playerScores, roundList: $roundList, winner: $winner]');
}
+ /// Converts the GameSession object to a JSON map.
+ Map toJson() => {
+ 'createdAt': createdAt.toIso8601String(),
+ 'gameTitle': gameTitle,
+ 'players': players,
+ 'pointLimit': pointLimit,
+ 'caboPenalty': caboPenalty,
+ 'isPointsLimitEnabled': isPointsLimitEnabled,
+ 'isGameFinished': isGameFinished,
+ 'winner': winner,
+ 'roundNumber': roundNumber,
+ 'playerScores': playerScores,
+ 'roundList': roundList.map((e) => e.toJson()).toList()
+ };
+
+ /// Creates a GameSession object from a JSON map.
+ GameSession.fromJson(Map json)
+ : createdAt = DateTime.parse(json['createdAt']),
+ gameTitle = json['gameTitle'],
+ players = List.from(json['players']),
+ pointLimit = json['pointLimit'],
+ caboPenalty = json['caboPenalty'],
+ isPointsLimitEnabled = json['isPointsLimitEnabled'],
+ isGameFinished = json['isGameFinished'],
+ winner = json['winner'],
+ roundNumber = json['roundNumber'],
+ playerScores = List.from(json['playerScores']),
+ roundList =
+ (json['roundList'] as List).map((e) => Round.fromJson(e)).toList();
+
/// Returns the length of all player names combined.
int getLengthOfPlayerNames() {
int length = 0;
@@ -52,13 +86,13 @@ class GameSession {
void applyKamikaze(int roundNum, int kamikazePlayerIndex) {
List roundScores = List.generate(players.length, (_) => 0);
List scoreUpdates = List.generate(players.length, (_) => 0);
- for (int i = 0; i < roundScores.length; i++) {
+ for (int i = 0; i < scoreUpdates.length; i++) {
if (i != kamikazePlayerIndex) {
scoreUpdates[i] += 50;
}
}
addRoundScoresToList(
- roundNum, roundScores, scoreUpdates, kamikazePlayerIndex);
+ roundNum, roundScores, scoreUpdates, 0, kamikazePlayerIndex);
}
/// Checks the scores of the current round and assigns points to the players.
@@ -92,7 +126,7 @@ class GameSession {
print('${players[caboPlayerIndex]} hat CABO gesagt '
'und bekommt 0 Punkte');
print('Alle anderen Spieler bekommen ihre Punkte');
- _assignPoints(roundNum, roundScores, [caboPlayerIndex]);
+ _assignPoints(roundNum, roundScores, caboPlayerIndex, [caboPlayerIndex]);
} else {
// A player other than the one who said CABO has the fewest points.
print('${players[caboPlayerIndex]} hat CABO gesagt, '
@@ -101,10 +135,16 @@ class GameSession {
for (int i in lowestScoreIndex) {
print('${players[i]}: ${roundScores[i]} Punkte');
}
- _assignPoints(roundNum, roundScores, lowestScoreIndex, caboPlayerIndex);
+ _assignPoints(roundNum, roundScores, caboPlayerIndex, lowestScoreIndex,
+ caboPlayerIndex);
}
}
+ /// The _getLowestScoreIndex method but forwarded for testing purposes.
+ @visibleForTesting
+ List testingGetLowestScoreIndex(List roundScores) =>
+ _getLowestScoreIndex(roundScores);
+
/// Returns the index of the player with the lowest score. If there are
/// multiple players with the same lowest score, all of them are returned.
/// [roundScores] is a list of the scores of all players in the current round.
@@ -123,12 +163,19 @@ class GameSession {
return lowestScoreIndex;
}
+ @visibleForTesting
+ void testingAssignPoints(int roundNum, List roundScores,
+ int caboPlayerIndex, List winnerIndex, [int? loserIndex]) =>
+ _assignPoints(
+ roundNum, roundScores, caboPlayerIndex, winnerIndex, loserIndex);
+
/// Assigns points to the players based on the scores of the current round.
/// [roundNum] is the number of the current round.
/// [roundScores] is the raw list of the scores of all players in the current round.
/// [winnerIndex] is the index of the player who receives 5 extra points
- void _assignPoints(int roundNum, List roundScores, List winnerIndex,
- [int loserIndex = -1]) {
+ void _assignPoints(int roundNum, List roundScores, int caboPlayerIndex,
+ List winnerIndex,
+ [int? loserIndex]) {
/// List of the updates for every player score
List scoreUpdates = [...roundScores];
print('Folgende Punkte wurden aus der Runde übernommen:');
@@ -139,7 +186,7 @@ class GameSession {
print('${players[i]} hat gewonnen und bekommt 0 Punkte');
scoreUpdates[i] = 0;
}
- if (loserIndex != -1) {
+ if (loserIndex != null) {
print('${players[loserIndex]} bekommt 5 Fehlerpunkte');
scoreUpdates[loserIndex] += 5;
}
@@ -148,7 +195,7 @@ class GameSession {
print('${players[i]}: ${scoreUpdates[i]}');
}
print('scoreUpdates: $scoreUpdates, roundScores: $roundScores');
- addRoundScoresToList(roundNum, roundScores, scoreUpdates);
+ addRoundScoresToList(roundNum, roundScores, scoreUpdates, caboPlayerIndex);
}
/// Sets the scores of the players for a specific round.
@@ -157,19 +204,25 @@ class GameSession {
/// playerScores. Its important that each index of the [roundScores] list
/// corresponds to the index of the player in the [playerScores] list.
void addRoundScoresToList(
- int roundNum, List roundScores, List scoreUpdates,
- [int? kamikazePlayerIndex]) {
+ int roundNum,
+ List roundScores,
+ List scoreUpdates,
+ int caboPlayerIndex, [
+ int? kamikazePlayerIndex,
+ ]) {
Round newRound = Round(
roundNum: roundNum,
+ caboPlayerIndex: caboPlayerIndex,
+ kamikazePlayerIndex: kamikazePlayerIndex,
scores: roundScores,
scoreUpdates: scoreUpdates,
- kamikazePlayerIndex: kamikazePlayerIndex,
);
if (roundNum > roundList.length) {
roundList.add(newRound);
} else {
roundList[roundNum - 1] = newRound;
}
+ notifyListeners();
}
/// This method updates the points of each player after a round.
@@ -182,13 +235,13 @@ class GameSession {
/// 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.
- void updatePoints() {
+ Future updatePoints() async {
_sumPoints();
- if (gameHasPointLimit) {
+ if (isPointsLimitEnabled) {
_checkHundredPointsReached();
for (int i = 0; i < playerScores.length; i++) {
- if (playerScores[i] > 100) {
+ if (playerScores[i] > pointLimit) {
isGameFinished = true;
print('${players[i]} hat die 100 Punkte ueberschritten, '
'deswegen wurde das Spiel beendet');
@@ -196,8 +249,12 @@ class GameSession {
}
}
}
+ notifyListeners();
}
+ @visibleForTesting
+ void testingSumPoints() => _sumPoints();
+
/// Sums up the points of all players and stores the result in the
/// playerScores list.
void _sumPoints() {
@@ -207,6 +264,7 @@ class GameSession {
playerScores[i] += roundList[j].scoreUpdates[i];
}
}
+ notifyListeners();
}
/// Checks if a player has reached 100 points in the current round.
@@ -214,7 +272,7 @@ class GameSession {
/// the corresponding round update.
void _checkHundredPointsReached() {
for (int i = 0; i < players.length; i++) {
- if (playerScores[i] == 100) {
+ if (playerScores[i] == pointLimit) {
print('${players[i]} hat genau 100 Punkte erreicht und bekommt '
'deswegen 50 Punkte abgezogen');
roundList[roundNumber - 1].scoreUpdates[i] -= 50;
@@ -236,10 +294,14 @@ class GameSession {
}
}
winner = lowestPlayer;
+ notifyListeners();
}
/// Increases the round number by 1.
void increaseRound() {
roundNumber++;
+ print('roundNumber erhöht: $roundNumber — Hash: ${identityHashCode(this)}');
+
+ notifyListeners();
}
}
diff --git a/lib/data/round.dart b/lib/data/round.dart
index 1f03b33..dcc5d9f 100644
--- a/lib/data/round.dart
+++ b/lib/data/round.dart
@@ -1,3 +1,5 @@
+import 'package:cabo_counter/data/game_session.dart';
+
/// This class represents a single round in the game.
/// It is stored within the [GameSession] class.
/// [roundNum] is the number of the round its reppresenting.
@@ -7,13 +9,40 @@
/// kamikaze, this value is null.
class Round {
final int roundNum;
+ final int caboPlayerIndex;
+ final int? kamikazePlayerIndex;
final List scores;
final List scoreUpdates;
- final int? kamikazePlayerIndex;
- Round(
- {required this.roundNum,
- required this.scores,
- required this.scoreUpdates,
- this.kamikazePlayerIndex});
+ Round({
+ required this.roundNum,
+ required this.caboPlayerIndex,
+ this.kamikazePlayerIndex,
+ required this.scores,
+ required this.scoreUpdates,
+ });
+
+ @override
+ toString() {
+ return 'Round $roundNum, caboPlayerIndex: $caboPlayerIndex, '
+ 'kamikazePlayerIndex: $kamikazePlayerIndex, scores: $scores, '
+ 'scoreUpdates: $scoreUpdates, ';
+ }
+
+ /// Converts the Round object to a JSON map.
+ Map toJson() => {
+ 'roundNum': roundNum,
+ 'caboPlayerIndex': caboPlayerIndex,
+ 'kamikazePlayerIndex': kamikazePlayerIndex,
+ 'scores': scores,
+ 'scoreUpdates': scoreUpdates,
+ };
+
+ /// Creates a Round object from a JSON map.
+ Round.fromJson(Map json)
+ : roundNum = json['roundNum'],
+ caboPlayerIndex = json['caboPlayerIndex'],
+ kamikazePlayerIndex = json['kamikazePlayerIndex'],
+ scores = List.from(json['scores']),
+ scoreUpdates = List.from(json['scoreUpdates']);
}
diff --git a/lib/main.dart b/lib/main.dart
index 9580339..9cc3648 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,28 +1,66 @@
-import 'package:cabo_counter/utility/theme.dart' as theme;
-import 'package:cabo_counter/views/main_menu_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:flutter/cupertino.dart';
+import 'package:flutter/services.dart';
-void main() {
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ await SystemChrome.setPreferredOrientations(
+ [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
+ await ConfigService.initConfig();
+ Globals.pointLimit = await ConfigService.getPointLimit();
+ Globals.caboPenalty = await ConfigService.getCaboPenalty();
runApp(const App());
}
-class App extends StatelessWidget {
+class App extends StatefulWidget {
const App({super.key});
+ @override
+ State createState() => _AppState();
+}
+
+class _AppState extends State with WidgetsBindingObserver {
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ LocalStorageService.loadGameSessions();
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.paused ||
+ state == AppLifecycleState.detached) {
+ LocalStorageService.saveGameSessions();
+ }
+ }
+
@override
Widget build(BuildContext context) {
+ LocalStorageService.loadGameSessions();
+
return CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
- primaryColor: theme.primaryColor,
- scaffoldBackgroundColor: theme.backgroundColor,
+ primaryColor: CustomTheme.primaryColor,
+ scaffoldBackgroundColor: CustomTheme.backgroundColor,
textTheme: CupertinoTextThemeData(
- primaryColor: theme.primaryColor,
+ primaryColor: CustomTheme.primaryColor,
),
),
debugShowCheckedModeBanner: false,
title: 'Cabo Counter',
- home: const MainMenuView(),
+ home: const TabView(),
);
}
}
diff --git a/lib/services/config_service.dart b/lib/services/config_service.dart
new file mode 100644
index 0000000..1c8275a
--- /dev/null
+++ b/lib/services/config_service.dart
@@ -0,0 +1,57 @@
+import 'package:cabo_counter/utility/globals.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.
+class ConfigService {
+ 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 Future 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);
+ }
+
+ /// Getter for the point limit.
+ static Future getPointLimit() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
+ }
+
+ /// Setter for the point limit.
+ /// [newPointLimit] is the new point limit to be set.
+ static Future setPointLimit(int newPointLimit) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setInt(_keyPointLimit, newPointLimit);
+ }
+
+ /// Getter for the cabo penalty.
+ static Future getCaboPenalty() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
+ }
+
+ /// Setter for the cabo penalty.
+ /// [newCaboPenalty] is the new cabo penalty to be set.
+ static Future setCaboPenalty(int newCaboPenalty) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setInt(_keyCaboPenalty, newCaboPenalty);
+ }
+
+ /// Resets the configuration to default values.
+ static Future resetConfig() async {
+ Globals.pointLimit = _defaultPointLimit;
+ Globals.caboPenalty = _defaultCaboPenalty;
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setInt(_keyPointLimit, _defaultPointLimit);
+ await prefs.setInt(_keyCaboPenalty, _defaultCaboPenalty);
+ }
+}
diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart
new file mode 100644
index 0000000..e3fddcc
--- /dev/null
+++ b/lib/services/local_storage_service.dart
@@ -0,0 +1,195 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:cabo_counter/data/game_manager.dart';
+import 'package:cabo_counter/data/game_session.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:file_saver/file_saver.dart';
+import 'package:flutter/services.dart';
+import 'package:json_schema/json_schema.dart';
+import 'package:path_provider/path_provider.dart';
+
+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() {
+ final jsonFile =
+ gameManager.gameList.map((session) => session.toJson()).toList();
+ return json.encode(jsonFile);
+ }
+
+ /// Returns the path to the local JSON file.
+ static Future _getFilePath() async {
+ final directory = await getApplicationDocumentsDirectory();
+ final path = '${directory.path}/$_fileName';
+ return File(path);
+ }
+
+ /// Saves the game sessions to a local JSON file.
+ static Future saveGameSessions() async {
+ print('[local_storage_service.dart] Versuche, Daten zu speichern...');
+ try {
+ final file = await _getFilePath();
+ final jsonFile = getJsonFile();
+ await file.writeAsString(jsonFile);
+ print(
+ '[local_storage_service.dart] Die Spieldaten wurden zwischengespeichert.');
+ } catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Speichern der Spieldaten. Exception: $e');
+ }
+ }
+
+ /// Loads the game data from a local JSON file.
+ static Future loadGameSessions() async {
+ print('[local_storage_service.dart] Versuche, Daten zu laden...');
+ try {
+ final file = await _getFilePath();
+
+ if (!await file.exists()) {
+ print(
+ '[local_storage_service.dart] Es existiert noch keine Datei mit Spieldaten');
+ return false;
+ }
+
+ print(
+ '[local_storage_service.dart] Es existiert bereits eine Datei mit Spieldaten');
+ final jsonString = await file.readAsString();
+
+ if (jsonString.isEmpty) {
+ print('[local_storage_service.dart] Die gefundene Datei ist leer');
+ return false;
+ }
+
+ if (!await validateJsonSchema(jsonString)) {
+ print(
+ '[local_storage_service.dart] Die Datei konnte nicht validiert werden');
+ gameManager.gameList = [];
+ return false;
+ }
+ print('[local_storage_service.dart] Die gefundene Datei hat Inhalt');
+ print(
+ '[local_storage_service.dart] Die gefundene Datei wurde erfolgreich validiert');
+ final jsonList = json.decode(jsonString) as List;
+
+ gameManager.gameList = jsonList
+ .map((jsonItem) =>
+ GameSession.fromJson(jsonItem as Map))
+ .toList();
+
+ print(
+ '[local_storage_service.dart] Die Spieldaten wurden erfolgreich geladen und verarbeitet');
+ return true;
+ } catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Laden der Spieldaten:\n$e');
+ gameManager.gameList = [];
+ return false;
+ }
+ }
+
+ /// Opens the file picker to save a JSON file with the current game data.
+ static Future exportJsonFile() async {
+ final jsonString = getJsonFile();
+ try {
+ final bytes = Uint8List.fromList(utf8.encode(jsonString));
+ final result = await FileSaver.instance.saveAs(
+ name: 'cabo_counter_data',
+ bytes: bytes,
+ ext: 'json',
+ mimeType: MimeType.json,
+ );
+ print(
+ '[local_storage_service.dart] Die Spieldaten wurden exportiert. Dateipfad: $result');
+ return true;
+ } catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Exportieren der Spieldaten. Exception: $e');
+ return false;
+ }
+ }
+
+ /// Opens the file picker to import a JSON file and loads the game data from it.
+ static Future importJsonFile() async {
+ final result = await FilePicker.platform.pickFiles(
+ dialogTitle: 'Wähle eine Datei mit Spieldaten aus',
+ type: FileType.custom,
+ allowedExtensions: ['json'],
+ );
+
+ if (result == null) {
+ print(
+ '[local_storage_service.dart] Der Filepicker-Dialog wurde abgebrochen');
+ return false;
+ }
+
+ try {
+ final jsonString = await _readFileContent(result.files.single);
+
+ if (!await validateJsonSchema(jsonString)) {
+ return false;
+ }
+ final jsonData = json.decode(jsonString) as List;
+ gameManager.gameList = jsonData
+ .map((jsonItem) =>
+ GameSession.fromJson(jsonItem as Map))
+ .toList();
+ print(
+ '[local_storage_service.dart] Die Datei wurde erfolgreich Importiertn');
+ return true;
+ } on FormatException catch (e) {
+ print(
+ '[local_storage_service.dart] Ungültiges JSON-Format. Exception: $e');
+ return false;
+ } on Exception catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Dateizugriff. Exception: $e');
+ return false;
+ }
+ }
+
+ /// Helper method to read file content from either bytes or path
+ static Future _readFileContent(PlatformFile file) async {
+ if (file.bytes != null) return utf8.decode(file.bytes!);
+ if (file.path != null) return await File(file.path!).readAsString();
+
+ throw Exception('Die Datei hat keinen lesbaren Inhalt');
+ }
+
+ /// Validates the JSON data against the schema.
+ static Future validateJsonSchema(String jsonString) async {
+ 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.');
+ return true;
+ }
+ print(
+ '[local_storage_service.dart] JSON ist nicht gültig.\nFehler: ${result.errors}');
+ return false;
+ } catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Validieren des JSON-Schemas: $e');
+ return false;
+ }
+ }
+
+ static Future deleteAllGames() async {
+ try {
+ gameManager.gameList.clear();
+ await saveGameSessions();
+ print(
+ '[local_storage_service.dart] Alle Runden wurden erfolgreich gelöscht.');
+ return true;
+ } catch (e) {
+ print(
+ '[local_storage_service.dart] Fehler beim Löschen aller Runden: $e');
+ return false;
+ }
+ }
+}
diff --git a/lib/utility/styles.dart b/lib/utility/custom_theme.dart
similarity index 55%
rename from lib/utility/styles.dart
rename to lib/utility/custom_theme.dart
index d0f2921..77a2f5b 100644
--- a/lib/utility/styles.dart
+++ b/lib/utility/custom_theme.dart
@@ -1,8 +1,10 @@
import 'package:flutter/cupertino.dart';
-abstract class Styles {
+class CustomTheme {
+ static Color white = CupertinoColors.white;
static Color primaryColor = CupertinoColors.systemGreen;
- static Color backgroundColor = const Color(0xFF080808);
+ static Color backgroundColor = const Color(0xFF101010);
+ static Color backgroundTintColor = CupertinoColors.darkBackgroundGray;
static TextStyle modeTitle = TextStyle(
color: primaryColor,
@@ -14,21 +16,15 @@ abstract class Styles {
fontSize: 16,
);
- static TextStyle createGameTitle = TextStyle(
+ static TextStyle rowTitle = TextStyle(
fontSize: 20,
color: primaryColor,
fontWeight: FontWeight.bold,
);
- static TextStyle roundTitle = const TextStyle(
+ static TextStyle roundTitle = TextStyle(
fontSize: 60,
- color: CupertinoColors.white,
- fontWeight: FontWeight.bold,
- );
-
- static TextStyle roundPlayers = const TextStyle(
- fontSize: 20,
- color: CupertinoColors.white,
+ color: white,
fontWeight: FontWeight.bold,
);
}
diff --git a/lib/utility/globals.dart b/lib/utility/globals.dart
index 2d22891..54cf2d0 100644
--- a/lib/utility/globals.dart
+++ b/lib/utility/globals.dart
@@ -1,5 +1,5 @@
-import 'package:cabo_counter/data/game_session.dart';
-
class Globals {
- static Map gamesMap = {};
+ static int pointLimit = 100;
+ static int caboPenalty = 5;
+ static String appDevPhase = 'Beta';
}
diff --git a/lib/utility/theme.dart b/lib/utility/theme.dart
deleted file mode 100644
index 6b4f963..0000000
--- a/lib/utility/theme.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/cupertino.dart';
-
-Color white = CupertinoColors.white;
-Color primaryColor = CupertinoColors.systemGreen;
-Color backgroundColor = const Color(0xFF101010);
-Color backgroundTintColor = CupertinoColors.darkBackgroundGray;
-
-TextStyle modeTitle = TextStyle(
- color: primaryColor,
- fontSize: 20,
- fontWeight: FontWeight.bold,
-);
-
-const TextStyle modeDescription = TextStyle(
- fontSize: 16,
-);
-
-TextStyle createGameTitle = TextStyle(
- fontSize: 20,
- color: primaryColor,
- fontWeight: FontWeight.bold,
-);
-
-TextStyle roundTitle = const TextStyle(
- fontSize: 60,
- color: CupertinoColors.white,
- fontWeight: FontWeight.bold,
-);
-
-TextStyle roundPlayers = const TextStyle(
- fontSize: 20,
- color: CupertinoColors.white,
- fontWeight: FontWeight.bold,
-);
diff --git a/lib/views/active_game_view.dart b/lib/views/active_game_view.dart
index 59d3a1b..ae513d4 100644
--- a/lib/views/active_game_view.dart
+++ b/lib/views/active_game_view.dart
@@ -1,5 +1,5 @@
import 'package:cabo_counter/data/game_session.dart';
-import 'package:cabo_counter/utility/theme.dart' as theme;
+import 'package:cabo_counter/utility/custom_theme.dart';
import 'package:cabo_counter/views/round_view.dart';
import 'package:flutter/cupertino.dart';
@@ -15,91 +15,98 @@ class ActiveGameView extends StatefulWidget {
class _ActiveGameViewState extends State {
@override
Widget build(BuildContext context) {
- List sortedPlayerIndices = _getSortedPlayerIndices();
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- middle: Text(widget.gameSession.gameTitle),
- ),
- child: SafeArea(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
- child: Text(
- 'Spieler:innen',
- style: theme.createGameTitle,
+ return ListenableBuilder(
+ listenable: widget.gameSession,
+ builder: (context, _) {
+ List sortedPlayerIndices = _getSortedPlayerIndices();
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: Text(widget.gameSession.gameTitle),
+ ),
+ child: SafeArea(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
+ child: Text(
+ 'Spieler:innen',
+ style: CustomTheme.rowTitle,
+ ),
+ ),
+ ListView.builder(
+ shrinkWrap: true,
+ 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]} '
+ 'Punkte')
+ ],
+ ),
+ );
+ },
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
+ child: Text(
+ 'Runden',
+ style: CustomTheme.rowTitle,
+ ),
+ ),
+ ListView.builder(
+ shrinkWrap: true,
+ itemCount: widget.gameSession.roundNumber,
+ itemBuilder: (BuildContext context, int index) {
+ return Padding(
+ padding: const EdgeInsets.all(1),
+ child: CupertinoListTile(
+ title: Text(
+ 'Runde ${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),
+ ),
+ );
+ },
+ ));
+ },
+ ),
+ ],
),
),
- ListView.builder(
- shrinkWrap: true,
- 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]} '
- 'Punkte')
- ],
- ),
- );
- },
- ),
- Padding(
- padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
- child: Text(
- 'Runden',
- style: theme.createGameTitle,
- ),
- ),
- ListView.builder(
- shrinkWrap: true,
- itemCount: widget.gameSession.roundNumber,
- itemBuilder: (BuildContext context, int index) {
- return Padding(
- padding: const EdgeInsets.all(1),
- child: CupertinoListTile(
- title: Text(
- 'Runde ${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.push(
- context,
- CupertinoPageRoute(
- fullscreenDialog: true,
- builder: (context) => RoundView(
- gameSession: widget.gameSession,
- roundNumber: index + 1),
- ),
- );
- setState(() {});
- },
- ));
- },
- ),
- ],
- ),
- ),
- );
+ );
+ });
}
/// Returns a list of player indices sorted by their scores in
diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart
index 2ffbe22..565d68a 100644
--- a/lib/views/create_game_view.dart
+++ b/lib/views/create_game_view.dart
@@ -1,5 +1,7 @@
+import 'package:cabo_counter/data/game_manager.dart';
import 'package:cabo_counter/data/game_session.dart';
-import 'package:cabo_counter/utility/styles.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';
@@ -42,11 +44,11 @@ class _CreateGameState extends State {
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text(
'Spiel',
- style: Styles.createGameTitle,
+ style: CustomTheme.rowTitle,
),
),
Padding(
- padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
+ padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
child: CupertinoTextField(
decoration: const BoxDecoration(),
maxLength: 16,
@@ -58,7 +60,7 @@ class _CreateGameState extends State {
),
// Spielmodus-Auswahl mit Chevron
Padding(
- padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
+ padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
child: CupertinoTextField(
decoration: const BoxDecoration(),
readOnly: true,
@@ -75,15 +77,15 @@ class _CreateGameState extends State {
],
),
onTap: () async {
- // Öffne das Modus-Auswahlmenü
final selected = await Navigator.push(
context,
CupertinoPageRoute(
- builder: (context) => const ModeSelectionMenu(),
+ builder: (context) => ModeSelectionMenu(
+ pointLimit: Globals.pointLimit,
+ ),
),
);
- // Aktualisiere den ausgewählten Modus
if (selected != null) {
setState(() {
selectedMode = selected;
@@ -96,7 +98,7 @@ class _CreateGameState extends State {
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text(
'Spieler:innen',
- style: Styles.createGameTitle,
+ style: CustomTheme.rowTitle,
),
),
Expanded(
@@ -204,7 +206,7 @@ class _CreateGameState extends State {
),
],
),
- onPressed: () {
+ onPressed: () async {
if (_gameTitleTextController.text == '') {
showCupertinoDialog(
context: context,
@@ -273,7 +275,6 @@ class _CreateGameState extends State {
],
),
);
- return;
}
List players = [];
@@ -281,15 +282,24 @@ class _CreateGameState extends State {
players.add(controller.text);
}
GameSession gameSession = GameSession(
+ createdAt: DateTime.now(),
gameTitle: _gameTitleTextController.text,
players: players,
- gameHasPointLimit: selectedMode!,
+ pointLimit: Globals.pointLimit,
+ caboPenalty: Globals.caboPenalty,
+ isPointsLimitEnabled: selectedMode!,
);
- Navigator.pushReplacement(
- context,
- CupertinoPageRoute(
- builder: (context) =>
- ActiveGameView(gameSession: gameSession)));
+ final index = await gameManager.addGameSession(gameSession);
+ print('index des spiels: $index');
+ if (context.mounted) {
+ Navigator.pushReplacement(
+ context,
+ CupertinoPageRoute(
+ builder: (context) => ActiveGameView(
+ gameSession: gameManager.gameList[index])));
+ } else {
+ print('Context is not mounted');
+ }
},
),
),
diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart
index 667d180..e5c0833 100644
--- a/lib/views/information_view.dart
+++ b/lib/views/information_view.dart
@@ -1,113 +1,78 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
-import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class InformationView extends StatelessWidget {
const InformationView({super.key});
- Future _getPackageInfo() async {
- return await PackageInfo.fromPlatform();
- }
-
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
+ resizeToAvoidBottomInset: false,
navigationBar: const CupertinoNavigationBar(
middle: Text('Über'),
),
child: SafeArea(
- child: Stack(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const Padding(
- padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
- child: Text(
- 'Cabo Counter',
- style: TextStyle(
- fontSize: 30,
- fontWeight: FontWeight.bold,
- ),
- ),
+ const Padding(
+ padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
+ child: Text(
+ 'Cabo Counter',
+ style: TextStyle(
+ fontSize: 30,
+ fontWeight: FontWeight.bold,
),
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 20, vertical: 10),
- child: SizedBox(
- height: 200,
- child:
- Image.asset('assets/cabo-counter-logo_rounded.png'),
- )),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 30),
- child: 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, zögere bitte nicht sie mir auf den dir '
- 'bekannten Wegen mitzuteilen. Danke! ',
- 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)),
- ],
- )
- ],
- ),
- Positioned(
- bottom: 30,
- left: 0,
- right: 0,
- child: FutureBuilder(
- future: _getPackageInfo(),
- builder: (context, snapshot) {
- if (snapshot.hasData) {
- return Text(
- 'Alpha ${snapshot.data!.version} '
- '(Build ${snapshot.data!.buildNumber})',
- textAlign: TextAlign.center,
- );
- } else if (snapshot.hasError) {
- return const Text(
- 'App-Version -.-.- (Build -)',
- textAlign: TextAlign.center,
- );
- }
- return const Text(
- 'Lade Version...',
- textAlign: TextAlign.center,
- );
- },
),
),
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
+ child: SizedBox(
+ height: 200,
+ child: Image.asset('assets/cabo_counter-logo_rounded.png'),
+ )),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 30),
+ child: 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, zögere bitte nicht sie mir auf den dir '
+ 'bekannten Wegen mitzuteilen. Danke! ',
+ 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)),
+ ],
+ ),
],
)));
}
diff --git a/lib/views/main_menu_view.dart b/lib/views/main_menu_view.dart
index d89a91e..7c7e019 100644
--- a/lib/views/main_menu_view.dart
+++ b/lib/views/main_menu_view.dart
@@ -1,7 +1,9 @@
-import 'package:cabo_counter/data/game_session.dart';
+import 'package:cabo_counter/data/game_manager.dart';
+import 'package:cabo_counter/services/local_storage_service.dart';
+import 'package:cabo_counter/utility/custom_theme.dart';
import 'package:cabo_counter/views/active_game_view.dart';
import 'package:cabo_counter/views/create_game_view.dart';
-import 'package:cabo_counter/views/information_view.dart';
+import 'package:cabo_counter/views/settings_view.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -14,125 +16,214 @@ class MainMenuView extends StatefulWidget {
}
class _MainMenuViewState extends State {
- final List gameSessionArray = [
- GameSession(
- gameTitle: 'Spiel am 27.02.2025',
- players: ['Clara', 'Tobias', 'Yannik', 'Lena', 'Lekaia'],
- gameHasPointLimit: true),
- GameSession(
- gameTitle: 'Freundschaftsrunde',
- players: ['Felix', 'Jonas', 'Nils'],
- gameHasPointLimit: false),
- GameSession(
- gameTitle: 'Familienabend',
- players: ['Mama', 'Papa', 'Lisa'],
- gameHasPointLimit: true,
- ),
- GameSession(
- gameTitle: 'Turnier 1. Runde',
- players: ['Tim', 'Max', 'Sophie', 'Lena'],
- gameHasPointLimit: false),
- GameSession(
- gameTitle: '2 Namen max length',
- players: ['Heinrich', 'Johannes'],
- gameHasPointLimit: true),
- GameSession(
- gameTitle: '3 Namen max length',
- players: ['Benjamin', 'Stefanie', 'Wolfgang'],
- gameHasPointLimit: false),
- GameSession(
- gameTitle: '4 Namen max length',
- players: ['Leonhard', 'Mathilde', 'Bernhard', 'Gerlinde'],
- gameHasPointLimit: true),
- GameSession(
- gameTitle: '5 Namen max length',
- players: ['Hartmuth', 'Elisabet', 'Rosalind', 'Theresia', 'Karoline'],
- gameHasPointLimit: false),
- ];
+ 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) {
- gameSessionArray.sort((b, a) => a.createdAt.compareTo(b.createdAt));
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- leading: IconButton(
- onPressed: () {
- Navigator.push(
- context,
- CupertinoPageRoute(
- builder: (context) => const InformationView(),
- ),
- );
- },
- icon: const Icon(
- CupertinoIcons.info_circle,
- 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: ListView.builder(
- itemCount: gameSessionArray.length,
- itemBuilder: (context, index) {
- final session = gameSessionArray[index];
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 10.0),
- child: CupertinoListTile(
- title: Text(session.gameTitle),
- subtitle: session.isGameFinished == true
- ? Text(
- '\u{1F947} ${session.winner}',
- style: const TextStyle(fontSize: 14),
- )
- : Text(
- 'Modus: ${_translateGameMode(session.gameHasPointLimit)}',
- style: const TextStyle(fontSize: 14),
+ 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(),
),
- 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: gameSessionArray[index]),
- ),
- );
- setState(() {});
- },
- ));
- },
- ),
- ),
- ),
- );
+ )
+ },
+ 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(
+ 'Modus: ${_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 '101 Punkte';
return 'Unbegrenzt';
}
+
+ /// 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 _showDeleteGamePopup(String gameTitle) async {
+ bool? shouldDelete = await showCupertinoDialog(
+ context: context,
+ builder: (context) {
+ return CupertinoAlertDialog(
+ title: const Text('Spiel löschen?'),
+ content: Text(
+ 'Bist du sicher, dass du die Runde "$gameTitle" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'),
+ actions: [
+ CupertinoDialogAction(
+ onPressed: () {
+ Navigator.pop(context, false);
+ },
+ child: const Text('Abbrechen'),
+ ),
+ CupertinoDialogAction(
+ onPressed: () {
+ Navigator.pop(context, true);
+ },
+ child: const Text('Löschen'),
+ ),
+ ],
+ );
+ },
+ ) ??
+ false;
+ return shouldDelete;
+ }
+
+ @override
+ void dispose() {
+ gameManager.removeListener(_updateView);
+ super.dispose();
+ }
}
diff --git a/lib/views/mode_selection_view.dart b/lib/views/mode_selection_view.dart
index 13a4b99..f4edf37 100644
--- a/lib/views/mode_selection_view.dart
+++ b/lib/views/mode_selection_view.dart
@@ -1,9 +1,9 @@
-import 'package:cabo_counter/utility/styles.dart';
-import 'package:cabo_counter/utility/theme.dart' as theme;
+import 'package:cabo_counter/utility/custom_theme.dart';
import 'package:flutter/cupertino.dart';
class ModeSelectionMenu extends StatelessWidget {
- const ModeSelectionMenu({super.key});
+ final int pointLimit;
+ const ModeSelectionMenu({super.key, required this.pointLimit});
@override
Widget build(BuildContext context) {
@@ -16,10 +16,10 @@ class ModeSelectionMenu extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
child: CupertinoListTile(
- title: Text('101 Punkte', style: Styles.modeTitle),
- subtitle: const Text(
- 'Es wird solange gespielt, bis einer Spieler mehr als 100 Punkte erreicht',
- style: Styles.modeDescription,
+ title: Text('$pointLimit Punkte', style: CustomTheme.modeTitle),
+ subtitle: Text(
+ 'Es wird solange gespielt, bis einer Spieler mehr als $pointLimit Punkte erreicht',
+ style: CustomTheme.modeDescription,
maxLines: 3,
),
onTap: () {
@@ -30,11 +30,11 @@ class ModeSelectionMenu extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: CupertinoListTile(
- title: Text('Unbegrenzt', style: theme.modeTitle),
+ title: Text('Unbegrenzt', style: CustomTheme.modeTitle),
subtitle: const Text(
'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange '
'gespielt, bis Ihr keine Lust mehr habt.',
- style: Styles.modeDescription,
+ style: CustomTheme.modeDescription,
maxLines: 3,
),
onTap: () {
diff --git a/lib/views/round_view.dart b/lib/views/round_view.dart
index 9520aa5..988adbf 100644
--- a/lib/views/round_view.dart
+++ b/lib/views/round_view.dart
@@ -1,5 +1,6 @@
import 'package:cabo_counter/data/game_session.dart';
-import 'package:cabo_counter/utility/theme.dart' as theme;
+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';
@@ -52,9 +53,12 @@ class _RoundViewState extends State {
_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();
}
@@ -71,7 +75,10 @@ class _RoundViewState extends State {
previousPageTitle: 'Übersicht',
leading: CupertinoButton(
padding: EdgeInsets.zero,
- onPressed: () => Navigator.pop(context, widget.gameSession),
+ onPressed: () => {
+ LocalStorageService.saveGameSessions(),
+ Navigator.pop(context, widget.gameSession)
+ },
child: const Text('Abbrechen'),
),
),
@@ -86,7 +93,7 @@ class _RoundViewState extends State {
children: [
const SizedBox(height: 40),
Text('Runde ${widget.roundNumber}',
- style: theme.roundTitle),
+ style: CustomTheme.roundTitle),
const SizedBox(height: 10),
const Text(
'Wer hat CABO gesagt?',
@@ -101,8 +108,8 @@ class _RoundViewState extends State {
child: SizedBox(
height: 40,
child: CupertinoSegmentedControl(
- unselectedColor: theme.backgroundTintColor,
- selectedColor: theme.primaryColor,
+ unselectedColor: CustomTheme.backgroundTintColor,
+ selectedColor: CustomTheme.primaryColor,
groupValue: _caboPlayerIndex,
children: Map.fromEntries(widget.gameSession.players
.asMap()
@@ -158,9 +165,9 @@ class _RoundViewState extends State {
SizedBox(
width: 100,
child: Center(child: Text('Punkte'))),
- SizedBox(width: 28),
+ SizedBox(width: 20),
SizedBox(
- width: 70,
+ width: 80,
child: Center(child: Text('Kamikaze'))),
],
),
@@ -209,6 +216,7 @@ class _RoundViewState extends State {
textAlign: TextAlign.center,
onSubmitted: (_) =>
_focusNextTextfield(index),
+ onChanged: (_) => setState(() {}),
),
),
const SizedBox(width: 50),
@@ -267,7 +275,7 @@ class _RoundViewState extends State {
return Container(
height: 80,
padding: const EdgeInsets.only(bottom: 20),
- color: theme.backgroundTintColor,
+ color: CustomTheme.backgroundTintColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@@ -275,6 +283,7 @@ class _RoundViewState extends State {
onPressed: _areRoundInputsValid()
? () {
_finishRound();
+ LocalStorageService.saveGameSessions();
Navigator.pop(context, widget.gameSession);
}
: null,
@@ -284,11 +293,12 @@ class _RoundViewState extends State {
onPressed: _areRoundInputsValid()
? () {
_finishRound();
+ LocalStorageService.saveGameSessions();
if (widget.gameSession.isGameFinished == true) {
Navigator.pop(context, widget.gameSession);
} else {
- Navigator.pushReplacement(
- context,
+ Navigator.of(context, rootNavigator: true)
+ .pushReplacement(
CupertinoPageRoute(
builder: (context) => RoundView(
gameSession: widget.gameSession,
diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart
new file mode 100644
index 0000000..8f0dcf4
--- /dev/null
+++ b/lib/views/settings_view.dart
@@ -0,0 +1,230 @@
+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 createState() => _SettingsViewState();
+}
+
+class _SettingsViewState extends State {
+ UniqueKey _stepperKey1 = UniqueKey();
+ UniqueKey _stepperKey2 = UniqueKey();
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoPageScaffold(
+ navigationBar: const CupertinoNavigationBar(
+ middle: Text('Einstellungen'),
+ ),
+ child: SafeArea(
+ child: Stack(
+ children: [
+ Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
+ child: Text(
+ 'Punkte',
+ style: CustomTheme.rowTitle,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
+ child: CupertinoListTile(
+ padding: EdgeInsets.zero,
+ title: const Text('Cabo-Strafe'),
+ subtitle: const Text('... für falsches Cabo sagen'),
+ 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: const Text('Punkte-Limit'),
+ subtitle: const Text('... hier ist Schluss'),
+ 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: const Text('Standard zurücksetzten'),
+ ),
+ )),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
+ child: Text(
+ 'Spieldaten',
+ 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(
+ 'Daten exportieren',
+ style:
+ TextStyle(color: CustomTheme.backgroundColor),
+ ),
+ onPressed: () async {
+ print('Export pressed');
+ final success =
+ await LocalStorageService.exportJsonFile();
+ if (!success && context.mounted) {
+ showCupertinoDialog(
+ context: context,
+ builder: (context) => CupertinoAlertDialog(
+ title: const Text('Fehler'),
+ content: const Text(
+ 'Datei konnte nicht exportiert werden.'),
+ actions: [
+ CupertinoDialogAction(
+ child: const Text('OK'),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ ),
+ );
+ }
+ },
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ CupertinoButton(
+ color: CustomTheme.primaryColor,
+ sizeStyle: CupertinoButtonSize.medium,
+ child: Text(
+ 'Daten importieren',
+ style:
+ TextStyle(color: CustomTheme.backgroundColor),
+ ),
+ onPressed: () async {
+ print('Import pressed');
+ final success =
+ await LocalStorageService.importJsonFile();
+ if (!success && context.mounted) {
+ showCupertinoDialog(
+ context: context,
+ builder: (context) => CupertinoAlertDialog(
+ title: const Text('Fehler'),
+ content: const Text(
+ 'Datei konnte nicht importiert werden.'),
+ actions: [
+ CupertinoDialogAction(
+ child: const Text('OK'),
+ onPressed: () =>
+ Navigator.pop(context),
+ ),
+ ],
+ ));
+ }
+ }),
+ ],
+ )),
+ )
+ ],
+ ),
+ Positioned(
+ bottom: 30,
+ left: 0,
+ right: 0,
+ child: Column(
+ children: [
+ const Center(
+ child: Text('Fehler gefunden?'),
+ ),
+ 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: const Text('Issue erstellen'),
+ ),
+ ),
+ ),
+ FutureBuilder(
+ future: _getPackageInfo(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ return Text(
+ '${Globals.appDevPhase} ${snapshot.data!.version} '
+ '(Build ${snapshot.data!.buildNumber})',
+ textAlign: TextAlign.center,
+ );
+ } else if (snapshot.hasError) {
+ return const Text(
+ 'App-Version -.-.- (Build -)',
+ textAlign: TextAlign.center,
+ );
+ }
+ return const Text(
+ 'Lade Version...',
+ textAlign: TextAlign.center,
+ );
+ },
+ )
+ ],
+ )),
+ ],
+ )),
+ );
+ }
+
+ Future _getPackageInfo() async {
+ return await PackageInfo.fromPlatform();
+ }
+}
diff --git a/lib/views/tab_view.dart b/lib/views/tab_view.dart
new file mode 100644
index 0000000..a0b5821
--- /dev/null
+++ b/lib/views/tab_view.dart
@@ -0,0 +1,47 @@
+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:flutter/cupertino.dart';
+
+class TabView extends StatefulWidget {
+ const TabView({super.key});
+
+ @override
+ // ignore: library_private_types_in_public_api
+ _TabViewState createState() => _TabViewState();
+}
+
+class _TabViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoTabScaffold(
+ tabBar: CupertinoTabBar(
+ backgroundColor: CustomTheme.backgroundTintColor,
+ iconSize: 27,
+ height: 55,
+ items: const [
+ BottomNavigationBarItem(
+ icon: Icon(
+ CupertinoIcons.house_fill,
+ ),
+ label: 'Home',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(
+ CupertinoIcons.info,
+ ),
+ label: 'About',
+ ),
+ ]),
+ tabBuilder: (BuildContext context, int index) {
+ return CupertinoTabView(builder: (BuildContext context) {
+ if (index == 0) {
+ return const MainMenuView();
+ } else {
+ return const InformationView();
+ }
+ });
+ },
+ );
+ }
+}
diff --git a/lib/widgets/stepper.dart b/lib/widgets/stepper.dart
new file mode 100644
index 0000000..879235e
--- /dev/null
+++ b/lib/widgets/stepper.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/cupertino.dart'; // Für iOS-Style
+
+class Stepper extends StatefulWidget {
+ final int minValue;
+ final int maxValue;
+ final int? initialValue;
+ final int step;
+ final ValueChanged onChanged;
+ const Stepper({
+ super.key,
+ required this.minValue,
+ required this.maxValue,
+ required this.step,
+ required this.onChanged,
+ this.initialValue,
+ });
+
+ @override
+ // ignore: library_private_types_in_public_api
+ _StepperState createState() => _StepperState();
+}
+
+class _StepperState extends State {
+ late int _value;
+
+ @override
+ void initState() {
+ super.initState();
+ final start = widget.initialValue ?? widget.minValue;
+ _value = start.clamp(widget.minValue, widget.maxValue);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ CupertinoButton(
+ padding: const EdgeInsets.all(8),
+ onPressed: _decrement,
+ child: const Icon(CupertinoIcons.minus),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: Text('$_value', style: const TextStyle(fontSize: 18)),
+ ),
+ CupertinoButton(
+ padding: const EdgeInsets.all(8),
+ onPressed: _increment,
+ child: const Icon(CupertinoIcons.add),
+ ),
+ ],
+ );
+ }
+
+ void _increment() {
+ if (_value + widget.step <= widget.maxValue) {
+ setState(() {
+ _value += widget.step;
+ widget.onChanged.call(_value);
+ });
+ }
+ }
+
+ void _decrement() {
+ if (_value - widget.step >= widget.minValue) {
+ setState(() {
+ _value -= widget.step;
+ widget.onChanged.call(_value);
+ });
+ }
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 326e7ee..0000be1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: cabo_counter
description: "Mobile app for the card game Cabo"
publish_to: 'none'
-version: 0.1.3+65
+version: 0.2.5+195
environment:
sdk: ^3.5.4
@@ -11,20 +11,26 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
- font_awesome_flutter: ^10.8.0
- url_launcher: any
- package_info_plus: any
+ file_picker: ^10.1.2
+ file_saver: ^0.2.6
flutter_keyboard_visibility: ^6.0.0
+ font_awesome_flutter: ^10.8.0
+ package_info_plus: any
+ path_provider: ^2.1.1
+ typed_data: ^1.3.2
+ url_launcher: any
+ json_schema: ^5.2.1
+ shared_preferences: ^2.5.3
+ logger: ^2.5.0
dev_dependencies:
flutter_test:
sdk: flutter
-
flutter_lints: ^5.0.0
test: ^1.25.15
-
flutter:
uses-material-design: false
assets:
- - assets/cabo-counter-logo_rounded.png
+ - assets/cabo_counter-logo_rounded.png
+ - assets/schema.json
diff --git a/test/data/game_session_test.dart b/test/data/game_session_test.dart
new file mode 100644
index 0000000..0e9bfa1
--- /dev/null
+++ b/test/data/game_session_test.dart
@@ -0,0 +1,164 @@
+import 'package:cabo_counter/data/game_session.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late GameSession session;
+ final testPlayers = ['Alice', 'Bob', 'Charlie'];
+ final testDate = DateTime(2023, 1, 1);
+ const testTitle = 'Test Game';
+
+ setUp(() {
+ session = GameSession(
+ createdAt: testDate,
+ gameTitle: testTitle,
+ players: testPlayers,
+ pointLimit: 100,
+ caboPenalty: 5,
+ isPointsLimitEnabled: true,
+ );
+ });
+
+ group('Initialization & JSON', () {
+ test('Initialization', () {
+ expect(session.gameTitle, testTitle);
+ expect(session.players, testPlayers);
+ expect(session.playerScores, [0, 0, 0]);
+ expect(session.roundNumber, 1);
+ expect(session.isGameFinished, isFalse);
+ expect(session.winner, isEmpty);
+ });
+
+ test('toJson and fromJson', () {
+ // Add some rounds to test serialization
+ session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
+ session.addRoundScoresToList(2, [15, 25, 35], [5, 5, 5], 1);
+
+ final json = session.toJson();
+ final fromJsonSession = GameSession.fromJson(json);
+
+ expect(fromJsonSession.gameTitle, testTitle);
+ expect(fromJsonSession.players, testPlayers);
+ expect(fromJsonSession.roundList.length, 2);
+ });
+
+ test('null values in JSON', () {
+ expect(
+ () => GameSession.fromJson({
+ 'createdAt': testDate.toIso8601String(),
+ 'gameTitle': null, // Invalid
+ 'players': testPlayers,
+ 'pointLimit': 100,
+ 'caboPenalty': 50,
+ 'isPointsLimitEnabled': true,
+ 'isGameFinished': false,
+ 'winner': '',
+ 'roundNumber': 1,
+ 'playerScores': [0, 0, 0],
+ 'roundList': [],
+ }),
+ throwsA(isA()));
+ });
+ });
+
+ group('Helper Functions', () {
+ test('getLengthOfPlayerNames', () {
+ expect(session.getLengthOfPlayerNames(),
+ equals(15)); // Alice(5) + Bob(3) + Charlie(7)
+ });
+
+ test('increaseRound', () {
+ expect(session.roundNumber, 1);
+ session.increaseRound();
+ expect(session.roundNumber, 2);
+ });
+
+ test('getLowestScoreIndex', () {
+ List lowestScoreIndex;
+
+ lowestScoreIndex = session.testingGetLowestScoreIndex([5, 10, 15]);
+ expect(lowestScoreIndex, [0]);
+
+ lowestScoreIndex = session.testingGetLowestScoreIndex([5, 5, 15]);
+ expect(lowestScoreIndex, [0, 1]);
+
+ lowestScoreIndex = session.testingGetLowestScoreIndex([5, 5, 5]);
+ expect(lowestScoreIndex, [0, 1, 2]);
+ });
+ });
+
+ group('Game Functions', () {
+ test('applyKamikaze', () {
+ session.applyKamikaze(1, 0); // Alice has kamikaze
+ expect(session.roundList[0].scoreUpdates, [0, 50, 50]);
+ expect(session.roundList[0].scores, [0, 0, 0]);
+ expect(session.roundList[0].kamikazePlayerIndex, 0);
+ expect(session.roundList[0].caboPlayerIndex, 0);
+ });
+
+ test('calculateScoredPoints - CABO player has lowest', () {
+ session.calculateScoredPoints(1, [3, 5, 8], 0); // Alice has lowest
+ expect(session.roundList[0].scoreUpdates, equals([0, 5, 8]));
+ });
+
+ test('calculateScoredPoints - CABO player not lowest', () {
+ session.calculateScoredPoints(1, [5, 3, 8], 0); // Bob has lowest
+ expect(session.roundList[0].scoreUpdates, [10, 0, 8]);
+ });
+
+ test('addRoundScoresToList', () {
+ session.addRoundScoresToList(1, [3, 5, 8], [0, 5, 8], 0);
+ expect(session.roundList.length, 1);
+ expect(session.roundList[0].roundNum, 1);
+ expect(session.roundList[0].scoreUpdates, [0, 5, 8]);
+ expect(session.roundList[0].scores, [3, 5, 8]);
+ expect(session.roundList[0].kamikazePlayerIndex, isNull);
+ expect(session.roundList[0].caboPlayerIndex, 0);
+ });
+
+ test('updatePoints - game not finished', () async {
+ session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
+ await session.updatePoints();
+ expect(session.isGameFinished, isFalse);
+ });
+
+ test('updatePoints - game finished', () async {
+ session.addRoundScoresToList(1, [101, 20, 30], [101, 20, 30], 0);
+ await session.updatePoints();
+ expect(session.isGameFinished, isTrue);
+ });
+
+ test('_assignPoints', () {
+ // Alice said Cabo and has the lowest score
+ session.testingAssignPoints(1, [5, 10, 15], 0, [0]);
+ expect(session.roundList[0].scoreUpdates, [0, 10, 15]);
+
+ // Alice said Cabo and has not the lowest score
+ session.testingAssignPoints(1, [5, 10, 15], 0, [1], 0);
+ expect(session.roundList[0].scoreUpdates, [10, 0, 15]);
+
+ // Bob and Charlie have the lowest score, Alice said Cabo
+ session.testingAssignPoints(1, [15, 5, 5], 0, [1, 2], 0);
+ expect(session.roundList[0].scoreUpdates, [20, 0, 0]);
+ });
+
+ test('_sumPoints', () async {
+ session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
+ session.addRoundScoresToList(2, [5, 5, 5], [5, 5, 5], 1);
+ session.testingSumPoints();
+ expect(session.playerScores, [15, 25, 35]);
+ });
+
+ test('_checkHundredPointsReached via updatePoints', () {
+ session.addRoundScoresToList(1, [50, 5, 15], [50, 0, 15], 1);
+ session.addRoundScoresToList(2, [50, 5, 15], [50, 0, 15], 1);
+ session.updatePoints();
+ expect(session.playerScores, equals([50, 0, 30]));
+ });
+
+ test('_setWinner via updatePoints', () async {
+ session.addRoundScoresToList(1, [101, 20, 30], [101, 0, 30], 1);
+ await session.updatePoints();
+ expect(session.winner, 'Bob'); // Bob has lowest score (20)
+ });
+ });
+}
diff --git a/test/data/round_test.dart b/test/data/round_test.dart
new file mode 100644
index 0000000..3e5ef4e
--- /dev/null
+++ b/test/data/round_test.dart
@@ -0,0 +1,117 @@
+import 'package:cabo_counter/data/round.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late Round round;
+ const testRoundNum = 1;
+ const testCaboPlayerIndex = 0;
+ const testKamikazePlayerIndex = 1;
+ const testScores = [10, 20, 30];
+ const testScoreUpdates = [5, 15, 25];
+
+ setUp(() {
+ round = Round(
+ roundNum: testRoundNum,
+ caboPlayerIndex: testCaboPlayerIndex,
+ kamikazePlayerIndex: testKamikazePlayerIndex,
+ scores: testScores,
+ scoreUpdates: testScoreUpdates,
+ );
+ });
+
+ group('Constructor Tests', () {
+ test('Constructor sets correct values', () {
+ expect(round.roundNum, testRoundNum);
+ expect(round.caboPlayerIndex, testCaboPlayerIndex);
+ expect(round.kamikazePlayerIndex, testKamikazePlayerIndex);
+ expect(round.scores, testScores);
+ expect(round.scoreUpdates, testScoreUpdates);
+ });
+
+ test('Constructor with null kamikazePlayerIndex', () {
+ final roundWithoutKamikaze = Round(
+ roundNum: testRoundNum,
+ caboPlayerIndex: testCaboPlayerIndex,
+ kamikazePlayerIndex: null,
+ scores: testScores,
+ scoreUpdates: testScoreUpdates,
+ );
+
+ expect(roundWithoutKamikaze.kamikazePlayerIndex, isNull);
+ });
+ });
+
+ group('JSON Methods', () {
+ test('toJson() returns correct map', () {
+ final jsonMap = round.toJson();
+
+ expect(jsonMap['roundNum'], equals(testRoundNum));
+ expect(jsonMap['caboPlayerIndex'], equals(testCaboPlayerIndex));
+ expect(jsonMap['kamikazePlayerIndex'], equals(testKamikazePlayerIndex));
+ expect(jsonMap['scores'], equals(testScores));
+ expect(jsonMap['scoreUpdates'], equals(testScoreUpdates));
+ });
+
+ test('fromJson() creates correct Round object', () {
+ final jsonMap = {
+ 'roundNum': testRoundNum,
+ 'caboPlayerIndex': testCaboPlayerIndex,
+ 'kamikazePlayerIndex': testKamikazePlayerIndex,
+ 'scores': testScores,
+ 'scoreUpdates': testScoreUpdates,
+ };
+
+ final fromJsonRound = Round.fromJson(jsonMap);
+
+ expect(fromJsonRound.roundNum, testRoundNum);
+ expect(fromJsonRound.caboPlayerIndex, testCaboPlayerIndex);
+ expect(fromJsonRound.kamikazePlayerIndex, testKamikazePlayerIndex);
+ expect(fromJsonRound.scores, testScores);
+ expect(fromJsonRound.scoreUpdates, testScoreUpdates);
+ });
+
+ test('fromJson() with null kamikazePlayerIndex', () {
+ final jsonMap = {
+ 'roundNum': testRoundNum,
+ 'caboPlayerIndex': testCaboPlayerIndex,
+ 'kamikazePlayerIndex': null,
+ 'scores': testScores,
+ 'scoreUpdates': testScoreUpdates,
+ };
+
+ final fromJsonRound = Round.fromJson(jsonMap);
+
+ expect(fromJsonRound.kamikazePlayerIndex, isNull);
+ });
+ });
+
+ group('toString()', () {
+ test('toString() returns correct string representation', () {
+ final expectedString = 'Round $testRoundNum, '
+ 'caboPlayerIndex: $testCaboPlayerIndex, '
+ 'kamikazePlayerIndex: $testKamikazePlayerIndex, '
+ 'scores: $testScores, '
+ 'scoreUpdates: $testScoreUpdates, ';
+
+ expect(round.toString(), equals(expectedString));
+ });
+
+ test('toString() with null kamikazePlayerIndex', () {
+ final roundWithoutKamikaze = Round(
+ roundNum: testRoundNum,
+ caboPlayerIndex: testCaboPlayerIndex,
+ kamikazePlayerIndex: null,
+ scores: testScores,
+ scoreUpdates: testScoreUpdates,
+ );
+
+ final expectedString = 'Round $testRoundNum, '
+ 'caboPlayerIndex: $testCaboPlayerIndex, '
+ 'kamikazePlayerIndex: null, '
+ 'scores: $testScores, '
+ 'scoreUpdates: $testScoreUpdates, ';
+
+ expect(roundWithoutKamikaze.toString(), expectedString);
+ });
+ });
+}