commit 7e4dd30599f81a5a7fc5d23a706a104e8736d5fa Author: Patryk Hegenberg Date: Fri Nov 28 15:59:06 2025 +0100 initial commit with working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..08cb0a9 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: android + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: ios + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: linux + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: macos + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: web + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: windows + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..36741c3 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# slrpg_app + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options 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..573ab75 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("com.android.application") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.slrpg.app" + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + defaultConfig { + applicationId = "com.slrpg.app" + minSdk = flutter.minSdkVersion + targetSdk = 36 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") +} 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..4428126 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/slrpg_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/slrpg_app/MainActivity.kt new file mode 100644 index 0000000..3a77703 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/slrpg_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.slrpg_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/kotlin/com/slrpg/app/MainActivity.kt b/android/app/src/main/kotlin/com/slrpg/app/MainActivity.kt new file mode 100644 index 0000000..fd123a2 --- /dev/null +++ b/android/app/src/main/kotlin/com/slrpg/app/MainActivity.kt @@ -0,0 +1,6 @@ +package com.slrpg.app + +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..f74085f --- /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..304732f --- /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-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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..8a87c7b --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,57 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.6.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.layout.buildDirectory.set(file("../build")) + +subprojects { + project.layout.buildDirectory.set(file("${rootProject.layout.buildDirectory.get()}/${project.name}")) +} + +subprojects { + // 1. Fix ZUERST registrieren (bevor das Projekt evaluiert wird) + afterEvaluate { + // Prüfen, ob das Subprojekt eine Android-Erweiterung hat (Library oder App) + val android = project.extensions.findByName("android") + if (android != null) { + // Wir versuchen, auf die 'namespace'-Eigenschaft zuzugreifen. + // Da wir hier im Root-Skript sind und die Typen dynamisch sind, nutzen wir Reflection/Dynamik. + try { + val namespaceProp = android.javaClass.getMethod("getNamespace") + val currentNamespace = namespaceProp.invoke(android) + + if (currentNamespace == null) { + // Wenn kein Namespace gesetzt ist, setzen wir ihn auf den Gruppennamen oder Package-Namen + val setNamespace = android.javaClass.getMethod("setNamespace", String::class.java) + val newNamespace = project.group.toString() + setNamespace.invoke(android, newNamespace) + println("FIX: Namespace für '${project.name}' auf '$newNamespace' gesetzt.") + } + } catch (e: Exception) { + println("WARNUNG: Konnte Namespace für ${project.name} nicht automatisch setzen: $e") + } + } + } + + // 2. DANACH erst die Abhängigkeit zur App definieren + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..7be7577 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..effa7d8 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,9 @@ +#org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +#android.useAndroidX=true + +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d79662d --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,11 @@ +#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 + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/isar_namespace_fix.gradle b/android/isar_namespace_fix.gradle new file mode 100644 index 0000000..9ba1c07 --- /dev/null +++ b/android/isar_namespace_fix.gradle @@ -0,0 +1,5 @@ +afterEvaluate { + android { + namespace = android.namespace ?: "com.isar.isar_flutter_libs" + } +} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..b1093d4 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,27 @@ +pluginManagement { + val flutterSdkPath = try { + 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 + } catch (e: Exception) { + throw GradleException("flutter.sdk not found in local.properties") + } + + 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.6.0" apply false // Version kann variieren + id("org.jetbrains.kotlin.android") version "2.1.0" apply false // Version kann variieren +} + +include(":app") diff --git a/assets/images/Gemini_Generated_Image_28mkwy28mkwy28mk.png b/assets/images/Gemini_Generated_Image_28mkwy28mkwy28mk.png new file mode 100644 index 0000000..307f7a5 Binary files /dev/null and b/assets/images/Gemini_Generated_Image_28mkwy28mkwy28mk.png differ diff --git a/assets/images/Gemini_Generated_Image_73mnv573mnv573mn.png b/assets/images/Gemini_Generated_Image_73mnv573mnv573mn.png new file mode 100644 index 0000000..499c58c Binary files /dev/null and b/assets/images/Gemini_Generated_Image_73mnv573mnv573mn.png differ diff --git a/assets/images/Gemini_Generated_Image_cqjvewcqjvewcqjv.png b/assets/images/Gemini_Generated_Image_cqjvewcqjvewcqjv.png new file mode 100644 index 0000000..250c37f Binary files /dev/null and b/assets/images/Gemini_Generated_Image_cqjvewcqjvewcqjv.png differ diff --git a/assets/images/Gemini_Generated_Image_khre1hkhre1hkhre.png b/assets/images/Gemini_Generated_Image_khre1hkhre1hkhre.png new file mode 100644 index 0000000..de9f256 Binary files /dev/null and b/assets/images/Gemini_Generated_Image_khre1hkhre1hkhre.png differ diff --git a/assets/images/Gemini_Generated_Image_r3pzwrr3pzwrr3pz.png b/assets/images/Gemini_Generated_Image_r3pzwrr3pzwrr3pz.png new file mode 100644 index 0000000..b6c69db Binary files /dev/null and b/assets/images/Gemini_Generated_Image_r3pzwrr3pzwrr3pz.png differ diff --git a/assets/images/Gemini_Generated_Image_un5wrpun5wrpun5w.png b/assets/images/Gemini_Generated_Image_un5wrpun5wrpun5w.png new file mode 100644 index 0000000..3687dab Binary files /dev/null and b/assets/images/Gemini_Generated_Image_un5wrpun5wrpun5w.png differ diff --git a/assets/images/Gemini_Generated_Image_vba2csvba2csvba2.png b/assets/images/Gemini_Generated_Image_vba2csvba2csvba2.png new file mode 100644 index 0000000..a85dd39 Binary files /dev/null and b/assets/images/Gemini_Generated_Image_vba2csvba2csvba2.png differ diff --git a/assets/images/avatars/female.png b/assets/images/avatars/female.png new file mode 100644 index 0000000..9ea4ec9 Binary files /dev/null and b/assets/images/avatars/female.png differ diff --git a/assets/images/avatars/female/1.png b/assets/images/avatars/female/1.png new file mode 100644 index 0000000..c0901b5 Binary files /dev/null and b/assets/images/avatars/female/1.png differ diff --git a/assets/images/avatars/female/1.png~ b/assets/images/avatars/female/1.png~ new file mode 100644 index 0000000..2dae4a3 Binary files /dev/null and b/assets/images/avatars/female/1.png~ differ diff --git a/assets/images/avatars/female/2.png b/assets/images/avatars/female/2.png new file mode 100644 index 0000000..0a82f5a Binary files /dev/null and b/assets/images/avatars/female/2.png differ diff --git a/assets/images/avatars/female/2.png~ b/assets/images/avatars/female/2.png~ new file mode 100644 index 0000000..7396a5f Binary files /dev/null and b/assets/images/avatars/female/2.png~ differ diff --git a/assets/images/avatars/female/3.png b/assets/images/avatars/female/3.png new file mode 100644 index 0000000..831c8e6 Binary files /dev/null and b/assets/images/avatars/female/3.png differ diff --git a/assets/images/avatars/female/3.png~ b/assets/images/avatars/female/3.png~ new file mode 100644 index 0000000..290ff54 Binary files /dev/null and b/assets/images/avatars/female/3.png~ differ diff --git a/assets/images/avatars/female/4.png b/assets/images/avatars/female/4.png new file mode 100644 index 0000000..d2914b8 Binary files /dev/null and b/assets/images/avatars/female/4.png differ diff --git a/assets/images/avatars/female/4.png~ b/assets/images/avatars/female/4.png~ new file mode 100644 index 0000000..6509772 Binary files /dev/null and b/assets/images/avatars/female/4.png~ differ diff --git a/assets/images/avatars/female/5.png b/assets/images/avatars/female/5.png new file mode 100644 index 0000000..999245e Binary files /dev/null and b/assets/images/avatars/female/5.png differ diff --git a/assets/images/avatars/female/5.png~ b/assets/images/avatars/female/5.png~ new file mode 100644 index 0000000..67d6f0e Binary files /dev/null and b/assets/images/avatars/female/5.png~ differ diff --git a/assets/images/avatars/female/6.png b/assets/images/avatars/female/6.png new file mode 100644 index 0000000..afd81ad Binary files /dev/null and b/assets/images/avatars/female/6.png differ diff --git a/assets/images/avatars/female/6.png~ b/assets/images/avatars/female/6.png~ new file mode 100644 index 0000000..4d98bf2 Binary files /dev/null and b/assets/images/avatars/female/6.png~ differ diff --git a/assets/images/avatars/female/7.png b/assets/images/avatars/female/7.png new file mode 100644 index 0000000..d727b92 Binary files /dev/null and b/assets/images/avatars/female/7.png differ diff --git a/assets/images/avatars/female/7.png~ b/assets/images/avatars/female/7.png~ new file mode 100644 index 0000000..76bbd18 Binary files /dev/null and b/assets/images/avatars/female/7.png~ differ diff --git a/assets/images/avatars/female/8.png b/assets/images/avatars/female/8.png new file mode 100644 index 0000000..9977d88 Binary files /dev/null and b/assets/images/avatars/female/8.png differ diff --git a/assets/images/avatars/female/8.png~ b/assets/images/avatars/female/8.png~ new file mode 100644 index 0000000..0f8c42f Binary files /dev/null and b/assets/images/avatars/female/8.png~ differ diff --git a/assets/images/avatars/male.png b/assets/images/avatars/male.png new file mode 100644 index 0000000..ad7efdd Binary files /dev/null and b/assets/images/avatars/male.png differ diff --git a/assets/images/avatars/male/1.png b/assets/images/avatars/male/1.png new file mode 100644 index 0000000..f85c3e8 Binary files /dev/null and b/assets/images/avatars/male/1.png differ diff --git a/assets/images/avatars/male/2.png b/assets/images/avatars/male/2.png new file mode 100644 index 0000000..6b23864 Binary files /dev/null and b/assets/images/avatars/male/2.png differ diff --git a/assets/images/avatars/male/3.png b/assets/images/avatars/male/3.png new file mode 100644 index 0000000..50661b9 Binary files /dev/null and b/assets/images/avatars/male/3.png differ diff --git a/assets/images/avatars/male/4.png b/assets/images/avatars/male/4.png new file mode 100644 index 0000000..c92ed53 Binary files /dev/null and b/assets/images/avatars/male/4.png differ diff --git a/assets/images/avatars/male/5.png b/assets/images/avatars/male/5.png new file mode 100644 index 0000000..73b1649 Binary files /dev/null and b/assets/images/avatars/male/5.png differ diff --git a/assets/images/avatars/male/6.png b/assets/images/avatars/male/6.png new file mode 100644 index 0000000..934f96c Binary files /dev/null and b/assets/images/avatars/male/6.png differ diff --git a/assets/images/avatars/male/7.png b/assets/images/avatars/male/7.png new file mode 100644 index 0000000..1ecec0e Binary files /dev/null and b/assets/images/avatars/male/7.png differ diff --git a/assets/images/avatars/male/8.png b/assets/images/avatars/male/8.png new file mode 100644 index 0000000..3505e11 Binary files /dev/null and b/assets/images/avatars/male/8.png differ diff --git a/assets/images/backgrounds/splash.png b/assets/images/backgrounds/splash.png new file mode 100644 index 0000000..85b5b5a Binary files /dev/null and b/assets/images/backgrounds/splash.png differ diff --git a/assets/images/backgrounds/street_park_day.png b/assets/images/backgrounds/street_park_day.png new file mode 100644 index 0000000..16c4258 Binary files /dev/null and b/assets/images/backgrounds/street_park_day.png differ diff --git a/assets/images/backgrounds/underground_gym.png b/assets/images/backgrounds/underground_gym.png new file mode 100644 index 0000000..13f4be7 Binary files /dev/null and b/assets/images/backgrounds/underground_gym.png differ diff --git a/assets/images/enemies/gravity_demon.png b/assets/images/enemies/gravity_demon.png new file mode 100644 index 0000000..72ba318 Binary files /dev/null and b/assets/images/enemies/gravity_demon.png differ diff --git a/assets/images/enemies/gravity_demon.png~ b/assets/images/enemies/gravity_demon.png~ new file mode 100644 index 0000000..9cdced2 Binary files /dev/null and b/assets/images/enemies/gravity_demon.png~ differ diff --git a/assets/images/enemies/iron_golem.png b/assets/images/enemies/iron_golem.png new file mode 100644 index 0000000..a92c4c0 Binary files /dev/null and b/assets/images/enemies/iron_golem.png differ diff --git a/assets/images/enemies/iron_golem.png~ b/assets/images/enemies/iron_golem.png~ new file mode 100644 index 0000000..d0714ba Binary files /dev/null and b/assets/images/enemies/iron_golem.png~ differ diff --git a/assets/images/enemies/pressure_phantom.png b/assets/images/enemies/pressure_phantom.png new file mode 100644 index 0000000..b586369 Binary files /dev/null and b/assets/images/enemies/pressure_phantom.png differ diff --git a/assets/images/enemies/pressure_phantom.png~ b/assets/images/enemies/pressure_phantom.png~ new file mode 100644 index 0000000..3107852 Binary files /dev/null and b/assets/images/enemies/pressure_phantom.png~ differ diff --git a/assets/images/gravity_demon.png b/assets/images/gravity_demon.png new file mode 100644 index 0000000..b98a2e0 Binary files /dev/null and b/assets/images/gravity_demon.png differ diff --git a/assets/images/iron_golem.png b/assets/images/iron_golem.png new file mode 100644 index 0000000..307f7a5 Binary files /dev/null and b/assets/images/iron_golem.png differ diff --git a/assets/images/pressure_phantom.png b/assets/images/pressure_phantom.png new file mode 100644 index 0000000..5ae7ec5 Binary files /dev/null and b/assets/images/pressure_phantom.png differ diff --git a/assets/images/splash.png b/assets/images/splash.png new file mode 100644 index 0000000..85b5b5a Binary files /dev/null and b/assets/images/splash.png differ diff --git a/assets/images/splash_back.png b/assets/images/splash_back.png new file mode 100644 index 0000000..18e9ed1 Binary files /dev/null and b/assets/images/splash_back.png differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3108cdf --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + 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 */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..99905ee --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Slrpg App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + slrpg_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..4d578ca --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'src/app.dart'; +import 'src/shared/data/local/collections/user_collection.dart'; +import 'src/shared/data/local/collections/cycle_collection.dart'; +import 'src/shared/data/local/collections/workout_collection.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Lock orientation to portrait + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + // Initialize Isar database + final dir = await getApplicationDocumentsDirectory(); + final isar = await Isar.open( + [UserCollectionSchema, CycleCollectionSchema, WorkoutCollectionSchema], + directory: dir.path, + name: 'slrpg_db', + ); + + runApp( + ProviderScope( + overrides: [isarProvider.overrideWithValue(isar)], + child: const SLRPGApp(), + ), + ); +} + +// Global Isar provider +final isarProvider = Provider((ref) => throw UnimplementedError()); diff --git a/lib/src/app.dart b/lib/src/app.dart new file mode 100644 index 0000000..4e18f19 --- /dev/null +++ b/lib/src/app.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'core/theme/app_theme.dart'; +import 'core/routing/app_router.dart'; + +class SLRPGApp extends ConsumerWidget { + const SLRPGApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + + return MaterialApp.router( + title: 'SLRPG - Streetlifting RPG', + debugShowCheckedModeBanner: false, + theme: AppTheme.darkTheme, + routerConfig: router, + ); + } +} + diff --git a/lib/src/core/constants/app_constants.dart b/lib/src/core/constants/app_constants.dart new file mode 100644 index 0000000..42f7109 --- /dev/null +++ b/lib/src/core/constants/app_constants.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +class AppConstants { + // API Configuration + static const String apiBaseUrl = 'http://10.0.2.2:8090'; // Android emulator + static const String apiVersion = 'v1'; + + // Wendler 5/3/1 Constants + static const double trainingMaxPercentage = 0.9; + static const double upperBodyIncrement = 2.5; // kg + static const double lowerBodyIncrement = 5.0; // kg + + // XP System + static const int baseXP = 1000; + static const double xpMultiplier = 1.15; + static const int maxLevel = 100; + + // XP Rewards + static const int workoutCompleteXP = 100; + static const double volumeXPRate = 0.1; // XP per kg + static const int amrapBonusXPPerRep = 25; + static const int prBonusXP = 500; + static const int cycleCompleteXP = 500; + + // Rounding Steps + static const double squatRoundingStep = 2.5; + static const double calisthenicsRoundingStep = 1.25; + + // Default Inventory + static const double defaultBarWeight = 20.0; + static const List defaultPlates = [ + 25, + 25, + 20, + 20, + 15, + 15, + 10, + 10, + 5, + 5, + 2.5, + 2.5, + 1.25, + 1.25 + ]; + + // Resistance Bands (Color: Resistance in KG approx) + // Negative values imply assistance force + static const Map defaultBands = { + 'Blue': 30.0, + 'Green': 20.0, + 'Orange': 10.0, + 'Red': 5.0, + }; + + // Bodyweight Limits + static const double minBodyweight = 40.0; + static const double maxBodyweight = 200.0; + + // Animation Durations + static const Duration shortAnimation = Duration(milliseconds: 200); + static const Duration mediumAnimation = Duration(milliseconds: 400); + static const Duration longAnimation = Duration(milliseconds: 600); + + // Storage Keys + static const String keyAuthToken = 'auth_token'; + static const String keyUserId = 'user_id'; + static const String keyLastSync = 'last_sync'; + static const String keyIsFirstLaunch = 'is_first_launch'; +} + +class ApiEndpoints { + static const String login = '/api/collections/users/auth-with-password'; + static const String register = '/api/collections/users/records'; + static const String sync = '/api/v1/sync'; + static const String cycleCreate = '/api/v1/cycle/create'; + static const String cycleFinish = '/api/v1/cycle/finish'; + static const String cycleCurrent = '/api/v1/cycle/current'; + static const String statsHistory = '/api/v1/stats/history'; + static const String statsSummary = '/api/v1/stats/summary'; + static const String profileBodyweight = '/api/v1/profile/bodyweight'; + static const String profileInventory = '/api/v1/profile/inventory'; + static const String userUpdate = '/api/collections/users/records'; // + /:id + static const String userDelete = '/api/collections/users/records'; // + /:id + static const String profileReset = '/api/v1/profile/reset'; +} + +class ExerciseIds { + static const String squat = 'squat'; + static const String pullup = 'pullup_weighted'; + static const String dip = 'dip_weighted'; +} + +class ExerciseNames { + static const String squat = 'Back Squat'; + static const String pullup = 'Weighted Pull-up'; + static const String dip = 'Weighted Dip'; +} diff --git a/lib/src/core/constants/asset_paths.dart b/lib/src/core/constants/asset_paths.dart new file mode 100644 index 0000000..7ad0fa4 --- /dev/null +++ b/lib/src/core/constants/asset_paths.dart @@ -0,0 +1,135 @@ +// class AssetPaths { +// // Backgrounds +// static const String bgStreetParkDay = +// 'assets/images/backgrounds/street_park_day.png'; +// static const String bgStreetParkNight = +// 'assets/images/backgrounds/street_park_night.png'; +// static const String bgUndergroundGym = +// 'assets/images/backgrounds/underground_gym.png'; +// static const String bgCommercialGym = +// 'assets/images/backgrounds/commercial_gym.png'; + +// // Avatars +// static const String avatarMaleBase = 'assets/images/avatars/male_base.png'; +// static const String avatarFemaleBase = +// 'assets/images/avatars/female_base.png'; + +// // Plates +// static const String plate25kg = 'assets/images/plates/plate_25kg.png'; +// static const String plate20kg = 'assets/images/plates/plate_20kg.png'; +// static const String plate15kg = 'assets/images/plates/plate_15kg.png'; +// static const String plate10kg = 'assets/images/plates/plate_10kg.png'; +// static const String plate5kg = 'assets/images/plates/plate_5kg.png'; +// static const String plate2_5kg = 'assets/images/plates/plate_2_5kg.png'; +// static const String plate1_25kg = 'assets/images/plates/plate_1_25kg.png'; + +// // Enemies +// static const String enemyIronGolem = 'assets/images/enemies/iron_golem.png'; +// static const String enemyGravityDemon = +// 'assets/images/enemies/gravity_demon.png'; +// static const String enemyPressurePhantom = +// 'assets/images/enemies/pressure_phantom.png'; + +// // Icons +// static const String iconXP = 'assets/images/icons/xp.png'; +// static const String iconLevel = 'assets/images/icons/level.png'; +// } + +// class PlateColors { +// static final Map colors = { +// 25.0: 0xFFD32F2F, // Red +// 20.0: 0xFF1976D2, // Blue +// 15.0: 0xFFFBC02D, // Yellow +// 10.0: 0xFF388E3C, // Green +// 5.0: 0xFFFAFAFA, // White +// 2.5: 0xFF212121, // Black +// 1.25: 0xFF9E9E9E, // Silver +// }; +// } +import 'dart:ui'; + +class AssetPaths { + // Backgrounds + static const String bgSplash = 'assets/images/backgrounds/splash.png'; + static const String bgStreetParkDay = + 'assets/images/backgrounds/street_park_day.png'; + static const String bgStreetParkNight = + 'assets/images/backgrounds/street_park_night.png'; + static const String bgUndergroundGym = + 'assets/images/backgrounds/underground_gym.png'; + static const String bgCommercialGym = + 'assets/images/backgrounds/commercial_gym.png'; + + // Avatars - Bases + static const String avatarMaleBase = 'assets/images/avatars/base/male.png'; + static const String avatarFemaleBase = + 'assets/images/avatars/base/female.png'; + + // Avatars - Hair (Beispiele) + static const String hairShort = 'assets/images/avatars/hair/short.png'; + static const String hairLong = 'assets/images/avatars/hair/long.png'; + static const String hairBald = + 'assets/images/avatars/hair/bald.png'; // Transparent/Empty + + // Avatars - Clothing (Beispiele) + static const String outfitBasicTee = + 'assets/images/avatars/clothing/basic_tee.png'; + static const String outfitHoodie = + 'assets/images/avatars/clothing/hoodie.png'; + static const String outfitTank = 'assets/images/avatars/clothing/tank.png'; + + // Plates + static const String plate25kg = 'assets/images/plates/plate_25kg.png'; + static const String plate20kg = 'assets/images/plates/plate_20kg.png'; + static const String plate15kg = 'assets/images/plates/plate_15kg.png'; + static const String plate10kg = 'assets/images/plates/plate_10kg.png'; + static const String plate5kg = 'assets/images/plates/plate_5kg.png'; + static const String plate2_5kg = 'assets/images/plates/plate_2_5kg.png'; + static const String plate1_25kg = 'assets/images/plates/plate_1_25kg.png'; + + // Enemies & Icons (wie vorher...) + static const String enemyIronGolem = 'assets/images/enemies/iron_golem.png'; + static const String enemyGravityDemon = + 'assets/images/enemies/gravity_demon.png'; + static const String enemyPressurePhantom = + 'assets/images/enemies/pressure_phantom.png'; + static const String iconXP = 'assets/images/icons/xp.png'; + static const String iconLevel = 'assets/images/icons/level.png'; + static String getAvatarPath(String gender, int variant) { + return 'assets/images/avatars/$gender/$variant.png'; + } +} + +class PlateColors { + static final Map colors = { + 25.0: 0xFFD32F2F, + 20.0: 0xFF1976D2, + 15.0: 0xFFFBC02D, + 10.0: 0xFF388E3C, + 5.0: 0xFFFAFAFA, + 2.5: 0xFF212121, + 1.25: 0xFF9E9E9E, + }; +} + +class AvatarConstants { + static const Map skinTones = { + 'pale': Color(0xFFFFDFC4), + 'fair': Color(0xFFF0D5BE), + 'medium': Color(0xFFD1A384), + 'olive': Color(0xFF9E7C63), + 'dark': Color(0xFF5C4033), + }; + + static const Map hairStyles = { + 'short_01': AssetPaths.hairShort, + 'long_01': AssetPaths.hairLong, + 'bald': AssetPaths.hairBald, + }; + + static const Map clothing = { + 'basic_tee': AssetPaths.outfitBasicTee, + 'hoodie': AssetPaths.outfitHoodie, + 'tank': AssetPaths.outfitTank, + }; +} diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart new file mode 100644 index 0000000..fb30b07 --- /dev/null +++ b/lib/src/core/routing/app_router.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/authentication/presentation/screens/login_screen.dart'; +import '../../features/authentication/presentation/screens/profile_screen.dart'; +import '../../features/authentication/presentation/screens/register_screen.dart'; +import '../../features/onboarding/presentation/screens/avatar_setup_screen.dart'; +import '../../features/onboarding/presentation/screens/welcome_screen.dart'; +import '../../features/onboarding/presentation/screens/bodyweight_input_screen.dart'; +import '../../features/onboarding/presentation/screens/strength_test_screen.dart'; +import '../../features/onboarding/presentation/screens/inventory_setup_screen.dart'; +import '../../features/dashboard/presentation/screens/hub_screen.dart'; +import '../../features/workout_runner/presentation/screens/battle_screen.dart'; +import '../../features/inventory/presentation/screens/inventory_screen.dart'; +import '../../features/history/presentation/screens/history_screen.dart'; +import '../../shared/data/repositories/user_repository.dart'; +import '../../features/stats/presentation/screens/stats_screen.dart'; +import '../constants/asset_paths.dart'; +import '../../features/gamification/presentation/screens/codex_screen.dart'; + +final routerProvider = Provider((ref) { + final userRepo = ref.watch(userRepositoryProvider); + + return GoRouter( + initialLocation: '/splash', + routes: [ + // Splash / Initial Route + GoRoute( + path: '/splash', + builder: (context, state) => const SplashScreen(), + ), + + // Authentication + GoRoute( + path: '/login', + name: 'login', + builder: (context, state) => const LoginScreen(), + ), + GoRoute( + path: '/register', + name: 'register', + builder: (context, state) => const RegisterScreen(), + ), + + // Onboarding Flow + GoRoute( + path: '/onboarding/welcome', + name: 'welcome', + builder: (context, state) => const WelcomeScreen(), + ), + GoRoute( + path: '/onboarding/bodyweight', + name: 'bodyweight', + builder: (context, state) => const BodyweightInputScreen(), + ), + GoRoute( + path: '/onboarding/strength-test', + name: 'strength-test', + builder: (context, state) => const StrengthTestScreen(), + ), + GoRoute( + path: '/onboarding/inventory', + name: 'inventory-setup', + builder: (context, state) => const InventorySetupScreen(), + ), + GoRoute( + path: '/onboarding/avatar', + name: 'avatar-setup', + builder: (context, state) => const AvatarSetupScreen(), + ), + + // Main App + GoRoute( + path: '/hub', + name: 'hub', + builder: (context, state) => const HubScreen(), + ), + GoRoute( + path: '/battle', + name: 'battle', + builder: (context, state) { + final extra = state.extra as Map?; + return BattleScreen( + week: extra?['week'] ?? 1, + day: extra?['day'] ?? 1, + workoutId: extra?['workoutId'], + ); + }, + ), + GoRoute( + path: '/inventory', + name: 'inventory', + builder: (context, state) => const InventoryScreen(), + ), + GoRoute( + path: '/history', + name: 'history', + builder: (context, state) => const HistoryScreen(), + ), + GoRoute( + path: '/stats', + name: 'stats', + builder: (context, state) => const StatsScreen(), + ), + GoRoute( + path: '/profile', + name: 'profile', + builder: (context, state) => const ProfileScreen(), + ), + GoRoute( + path: '/codex', + name: 'codex', + builder: (context, state) => const CodexScreen(), + ), + ], + ); +}); + +// Splash Screen to determine initial route +class SplashScreen extends ConsumerStatefulWidget { + const SplashScreen({super.key}); + + @override + ConsumerState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + _checkInitialRoute(); + } + + Future _checkInitialRoute() async { + await Future.delayed(const Duration(seconds: 1)); + + if (!mounted) return; + + final userRepo = ref.read(userRepositoryProvider); + final user = await userRepo.getLocalUser(); + + if (user == null) { + // No user, go to login + context.go('/login'); + } else { + // User exists, go to hub + context.go('/hub'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // 1. Hintergrundbild + Positioned.fill( + child: Image.asset( + AssetPaths.bgSplash, // Nutzt den Splash + fit: BoxFit.cover, + ), + ), + + // 2. Overlay (Dunkel), damit Text lesbar ist + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.5), + ), + ), + + // 3. Inhalt + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo Container (mit leichtem Glow) + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: const Color(0xFF00E5FF).withOpacity(0.9), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: const Color(0xFF00E5FF).withOpacity(0.6), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: const Icon( + Icons.fitness_center, + size: 64, + color: Colors.black, + ), + ), + const SizedBox(height: 24), + Text( + 'S.L.R.P.G.', + style: Theme.of(context).textTheme.displayLarge?.copyWith( + color: Colors.white, + shadows: [ + const Shadow( + color: Colors.black, + blurRadius: 10, + offset: Offset(0, 4)), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Streetlifting RPG', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 48), + const CircularProgressIndicator( + color: Color(0xFF00E5FF), + ), + ], + ), + ), + ], + ), + ); + } + // @override + // Widget build(BuildContext context) { + // return Scaffold( + // backgroundColor: const Color(0xFF121212), + // body: Center( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // // Logo placeholder + // Container( + // width: 120, + // height: 120, + // decoration: BoxDecoration( + // color: const Color(0xFF00E5FF), + // borderRadius: BorderRadius.circular(24), + // ), + // child: const Icon( + // Icons.fitness_center, + // size: 64, + // color: Colors.black, + // ), + // ), + // const SizedBox(height: 24), + // Text( + // 'S.L.R.P.G.', + // style: Theme.of(context).textTheme.displayLarge, + // ), + // const SizedBox(height: 8), + // Text( + // 'Streetlifting RPG', + // style: Theme.of(context).textTheme.bodyMedium, + // ), + // const SizedBox(height: 48), + // const CircularProgressIndicator( + // color: Color(0xFF00E5FF), + // ), + // ], + // ), + // ), + // ); + // } +} diff --git a/lib/src/core/theme/app_theme.dart b/lib/src/core/theme/app_theme.dart new file mode 100644 index 0000000..6895a39 --- /dev/null +++ b/lib/src/core/theme/app_theme.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + // Color Palette (Dark RPG Theme) + static const Color primaryColor = Color(0xFF00E5FF); // Cyan + static const Color secondaryColor = Color(0xFFFF6E40); // Deep Orange + static const Color backgroundColor = Color(0xFF121212); + static const Color surfaceColor = Color(0xFF1E1E1E); + static const Color errorColor = Color(0xFFCF6679); + static const Color successColor = Color(0xFF4CAF50); + + // XP Bar Colors + static const Color xpBarBackground = Color(0xFF2C2C2C); + static const Color xpBarFill = Color(0xFF00E5FF); + + // Text Colors + static const Color textPrimary = Color(0xFFFFFFFF); + static const Color textSecondary = Color(0xFFB0B0B0); + static const Color textDisabled = Color(0xFF666666); + + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + primaryColor: primaryColor, + scaffoldBackgroundColor: backgroundColor, + colorScheme: const ColorScheme.dark( + primary: primaryColor, + secondary: secondaryColor, + surface: surfaceColor, + error: errorColor, + onPrimary: Colors.black, + onSecondary: Colors.white, + onSurface: textPrimary, + onError: Colors.white, + ), + textTheme: GoogleFonts.robotoTextTheme().copyWith( + displayLarge: GoogleFonts.orbitron( + fontSize: 32, + fontWeight: FontWeight.bold, + color: textPrimary, + ), + displayMedium: GoogleFonts.orbitron( + fontSize: 24, + fontWeight: FontWeight.bold, + color: textPrimary, + ), + headlineMedium: GoogleFonts.orbitron( + fontSize: 20, + fontWeight: FontWeight.w600, + color: textPrimary, + ), + bodyLarge: GoogleFonts.roboto( + fontSize: 16, + color: textPrimary, + ), + bodyMedium: GoogleFonts.roboto( + fontSize: 14, + color: textSecondary, + ), + labelLarge: GoogleFonts.orbitron( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 8, + textStyle: GoogleFonts.orbitron( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + ), + cardTheme: CardThemeData( + // CardTheme -> CardThemeData + color: surfaceColor, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: primaryColor.withOpacity(0.3), + width: 1, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: surfaceColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primaryColor.withOpacity(0.5)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primaryColor.withOpacity(0.3)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: errorColor), + ), + ), + appBarTheme: AppBarTheme( + backgroundColor: backgroundColor, + elevation: 0, + centerTitle: true, + titleTextStyle: GoogleFonts.orbitron( + fontSize: 20, + fontWeight: FontWeight.bold, + color: textPrimary, + ), + ), + ); + } +} diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart new file mode 100644 index 0000000..e8a4005 --- /dev/null +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../core/theme/app_theme.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + bool _obscurePassword = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + + try { + final userRepo = ref.read(userRepositoryProvider); + await userRepo.login( + _emailController.text.trim(), + _passwordController.text, + ); + + if (mounted) { + context.go('/hub'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Login failed: ${e.toString()}'), + backgroundColor: AppTheme.errorColor, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.fitness_center, + size: 56, + color: Colors.black, + ), + ), + const SizedBox(height: 24), + + // Title + Text( + 'WELCOME BACK', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Time to level up your strength', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Email Field + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Password Field + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() => _obscurePassword = !_obscurePassword); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your password'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + const SizedBox(height: 32), + + // Login Button + ElevatedButton( + onPressed: _isLoading ? null : _handleLogin, + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text('LOGIN'), + ), + const SizedBox(height: 16), + + // Register Link + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Don't have an account? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: () => context.go('/register'), + child: const Text('REGISTER'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} + diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart new file mode 100644 index 0000000..e1a963c --- /dev/null +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/local/collections/user_collection.dart'; +import '../../../gamification/domain/entities/avatar_config.dart'; +import '../../../gamification/presentation/widgets/avatar_editor.dart'; +import '../../../gamification/presentation/widgets/avatar_renderer.dart'; +import 'dart:convert'; // Für jsonDecode + +class ProfileScreen extends ConsumerStatefulWidget { + const ProfileScreen({super.key}); + + @override + ConsumerState createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends ConsumerState { + bool _isLoading = false; + double _currentBodyweight = 80.0; + bool _hasChanges = false; + UserCollection? _user; + AvatarConfig? _tempAvatarConfig; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + Future _loadUser() async { + final user = await ref.read(userRepositoryProvider).getLocalUser(); + if (user != null && mounted) { + setState(() { + _user = user; + _currentBodyweight = user.currentBodyweight; + }); + } + } + // Future _loadUser() async { + // final user = await ref.read(userRepositoryProvider).getLocalUser(); + // if (user != null && mounted) { + // setState(() { + // _currentBodyweight = user.currentBodyweight; + // }); + // } + // } + + Future _saveBodyweight() async { + setState(() => _isLoading = true); + try { + await ref + .read(userRepositoryProvider) + .updateBodyweight(_currentBodyweight); + if (mounted) { + setState(() => _hasChanges = false); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Bodyweight updated')), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _showChangePasswordDialog() { + final oldPassCtrl = TextEditingController(); + final newPassCtrl = TextEditingController(); + final confirmPassCtrl = TextEditingController(); + final formKey = GlobalKey(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Change Password'), + content: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: oldPassCtrl, + obscureText: true, + decoration: const InputDecoration(labelText: 'Old Password'), + validator: (v) => v?.isEmpty == true ? 'Required' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: newPassCtrl, + obscureText: true, + decoration: const InputDecoration(labelText: 'New Password'), + validator: (v) => (v?.length ?? 0) < 8 ? 'Min 8 chars' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: confirmPassCtrl, + obscureText: true, + decoration: const InputDecoration(labelText: 'Confirm New'), + validator: (v) => v != newPassCtrl.text ? 'Mismatch' : null, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + ElevatedButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + Navigator.pop(context); + setState(() => _isLoading = true); + try { + await ref.read(userRepositoryProvider).changePassword( + oldPassCtrl.text, + newPassCtrl.text, + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password changed successfully')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: AppTheme.errorColor), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + }, + child: const Text('UPDATE'), + ), + ], + ), + ); + } + + void _confirmDangerAction( + String title, String content, VoidCallback onConfirm) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title, style: const TextStyle(color: AppTheme.errorColor)), + content: Text(content), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + ElevatedButton( + style: + ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), + onPressed: () { + Navigator.pop(context); + onConfirm(); + }, + child: const Text('CONFIRM'), + ), + ], + ), + ); + } + + void _showAvatarEditor() { + final currentConfig = _user?.avatarConfigJson != null + ? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!)) + : const AvatarConfig(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, // Wichtig für Fullscreen-Feeling + useSafeArea: true, + backgroundColor: AppTheme.backgroundColor, + builder: (context) => Scaffold( + appBar: AppBar( + title: const Text('Edit Appearance'), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + actions: [ + TextButton( + onPressed: () { + // Speichern wird hier ausgelöst durch den Save-Callback im Editor Wrapper + // Aber da der Editor im BottomSheet state hat, müssen wir die Config rausbekommen. + // Einfacher: Wir wrappen den Editor in ein Stateful Widget im Dialog oder übergeben einen Callback. + // Da wir hier im ProfileScreen sind, können wir eine temporäre Variable nutzen und beim Schließen speichern. + Navigator.pop(context, _tempAvatarConfig); + }, + child: const Text('SAVE', + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor)), + ), + ], + ), + body: AvatarEditor( + initialConfig: currentConfig, + onChanged: (conf) => _tempAvatarConfig = + conf, // _tempAvatarConfig muss in State definiert werden + ), + ), + ).then((result) async { + if (result is AvatarConfig) { + setState(() => _isLoading = true); + // Speichern + _user!.avatarConfigJson = jsonEncode(result.toJson()); + _user!.isDirty = true; + await ref.read(userRepositoryProvider).saveLocalUser(_user!); + setState(() => _isLoading = false); + } + }); + } + + @override + Widget build(BuildContext context) { + final userRepo = ref.watch(userRepositoryProvider); + final avatarConfig = _user?.avatarConfigJson != null + ? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!)) + : const AvatarConfig(); + + return Scaffold( + appBar: AppBar( + title: const Text('Edit Profile'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/hub'), + ), + actions: [ + if (_hasChanges) + TextButton( + onPressed: _isLoading ? null : _saveBodyweight, + child: const Text('SAVE', + style: TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold)), + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( + child: Stack( + children: [ + AvatarRenderer( + config: avatarConfig, + size: 120, + ), + Positioned( + bottom: 0, + right: 0, + child: CircleAvatar( + backgroundColor: AppTheme.surfaceColor, + radius: 18, + child: IconButton( + icon: const Icon(Icons.edit, size: 16), + onPressed: _showAvatarEditor, + // onPressed: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text( + // 'Avatar customization coming soon!')), + // ); + // }, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + // Bodyweight Section + Text('Physical Stats', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary)), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Current Bodyweight', + style: Theme.of(context).textTheme.bodyMedium), + Row( + children: [ + Expanded( + child: Slider( + value: _currentBodyweight, + min: 40, + max: 150, + divisions: 220, // 0.5 steps + label: _currentBodyweight.toStringAsFixed(1), + activeColor: AppTheme.primaryColor, + onChanged: (val) => setState(() { + _currentBodyweight = val; + _hasChanges = true; + }), + ), + ), + Text( + '${_currentBodyweight.toStringAsFixed(1)} kg', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 32), + + // Account Actions + Text('Account Security', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary)), + const SizedBox(height: 8), + ListTile( + leading: const Icon(Icons.lock_outline), + title: const Text('Change Password'), + trailing: const Icon(Icons.chevron_right), + onTap: _showChangePasswordDialog, + ), + const Divider(), + + // Danger Zone + const SizedBox(height: 24), + Text('Danger Zone', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.errorColor)), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + border: + Border.all(color: AppTheme.errorColor.withOpacity(0.5)), + borderRadius: BorderRadius.circular(12), + color: AppTheme.errorColor.withOpacity(0.05), + ), + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.refresh, + color: AppTheme.errorColor), + title: const Text('Reset Progress', + style: TextStyle(color: AppTheme.errorColor)), + subtitle: + const Text('Resets Level, XP and Training History'), + onTap: () => _confirmDangerAction( + 'Reset Progress?', + 'This will delete all your workouts and reset your Level to 1. This cannot be undone.', + () async { + setState(() => _isLoading = true); + await userRepo.resetProgress(); + if (mounted) { + setState(() => _isLoading = false); + context.go('/hub'); + } + }, + ), + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.delete_forever, + color: AppTheme.errorColor), + title: const Text('Delete Account', + style: TextStyle(color: AppTheme.errorColor)), + subtitle: const Text( + 'Permanently delete your account and data'), + onTap: () => _confirmDangerAction( + 'Delete Account?', + 'Are you sure you want to delete your account? All data will be lost forever.', + () async { + setState(() => _isLoading = true); + try { + await userRepo.deleteAccount(); + if (mounted) context.go('/login'); + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } + } + }, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + OutlinedButton.icon( + onPressed: () async { + await userRepo.logout(); + if (mounted) context.go('/login'); + }, + icon: const Icon(Icons.logout), + label: const Text('LOGOUT'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/features/authentication/presentation/screens/register_screen.dart b/lib/src/features/authentication/presentation/screens/register_screen.dart new file mode 100644 index 0000000..13fe485 --- /dev/null +++ b/lib/src/features/authentication/presentation/screens/register_screen.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/app_constants.dart'; +import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart'; + +class RegisterScreen extends ConsumerStatefulWidget { + const RegisterScreen({super.key}); + + @override + ConsumerState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + void _handleRegister() { + if (!_formKey.currentState!.validate()) return; + + ref.read(onboardingDataProvider.notifier).update((state) => { + 'email': _emailController.text.trim(), + 'password': _passwordController.text, + }); + + context.go('/onboarding/welcome'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/login'), + ), + ), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'CREATE ACCOUNT', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Begin your strength journey', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() => _obscurePassword = !_obscurePassword); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + labelText: 'Confirm Password', + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() => _obscureConfirmPassword = + !_obscureConfirmPassword); + }, + ), + ), + validator: (value) { + if (value != _passwordController.text) { + return 'Passwords do not match'; + } + return null; + }, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: _handleRegister, + child: const Text('CONTINUE'), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Already have an account? ', + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: () => context.go('/login'), + child: const Text('LOGIN'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart new file mode 100644 index 0000000..59bd10a --- /dev/null +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -0,0 +1,488 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/asset_paths.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../../shared/domain/logic/xp_calculator.dart'; +import '../widgets/xp_bar_widget.dart'; +import '../widgets/level_display.dart'; +import '../widgets/start_raid_button.dart'; +import '../../../../shared/data/local/collections/user_collection.dart'; +import '../../../../shared/data/local/collections/cycle_collection.dart'; +import '../../../../shared/data/repositories/workout_repository.dart'; +import '../../../../shared/domain/entities/exercise.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; +import '../../../../shared/data/remote/sync_service.dart'; +import '../../../../shared/domain/entities/workout_set.dart'; +import '../../../gamification/domain/entities/avatar_config.dart'; +import '../../../gamification/presentation/widgets/avatar_renderer.dart'; + +class HubScreen extends ConsumerStatefulWidget { + const HubScreen({super.key}); + + @override + ConsumerState createState() => _HubScreenState(); +} + +class _HubScreenState extends ConsumerState { + bool _isSyncing = false; + + Future _runSync() async { + setState(() => _isSyncing = true); + await ref.read(syncServiceProvider).sync(); + if (mounted) setState(() => _isSyncing = false); + } + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _runSync(); + }); + } + + List _generateExercises({ + required int week, + required int day, + required Map trainingMaxes, + required double bodyweight, + }) { + final exercises = []; + + void addExercise(String id, String name, ExerciseType type, bool isMain) { + final tm = trainingMaxes[id] ?? 0.0; + List sets; + + if (isMain) { + sets = WendlerCalculator.generateSets( + week: week, + trainingMax: tm, + exerciseType: type, + currentBodyweight: bodyweight, + ); + } else { + if (week == 4) return; + + sets = WendlerCalculator.generateFSLSets( + trainingMax: tm, + exerciseType: type, + currentBodyweight: bodyweight, + ); + } + + if (sets.isNotEmpty) { + exercises.add(Exercise( + exerciseId: id, + exerciseName: isMain ? name : '$name (FSL)', + bodyweightAtSession: bodyweight, + sets: sets, + )); + } + } + + if (day == 1) { + addExercise('squat', 'Back Squat', ExerciseType.squat, true); + addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, false); + } else if (day == 2) { + addExercise('dip', 'Weighted Dip', ExerciseType.dip, true); + addExercise('squat', 'Back Squat', ExerciseType.squat, false); + } else if (day == 3) { + addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, true); + addExercise('dip', 'Weighted Dip', ExerciseType.dip, false); + } + + return exercises; + } + + Future _startNextWorkout( + CycleCollection cycle, UserCollection user) async { + try { + final workoutRepo = ref.read(workoutRepositoryProvider); + final cycleRepo = ref.read(cycleRepositoryProvider); + + int targetWeek = 1; + int targetDay = 1; + bool found = false; + + final cycleRefId = cycle.serverId ?? cycle.id.toString(); + final localCycleId = cycle.id.toString(); + + for (int w = 1; w <= 4; w++) { + for (int d = 1; d <= 3; d++) { + final exists = await workoutRepo.getWorkoutByWeekDay( + cycleId: cycleRefId, localCycleId: localCycleId, week: w, day: d); + + if (exists == null || exists.completedAt == null) { + targetWeek = w; + targetDay = d; + found = true; + break; + } + } + if (found) break; + } + + if (!found) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Cycle complete! Finish it in stats.')), + ); + } + return; + } + + final trainingMaxes = cycleRepo.getCurrentTrainingMaxes(); + + var workout = await workoutRepo.getWorkoutByWeekDay( + cycleId: cycleRefId, + localCycleId: localCycleId, + week: targetWeek, + day: targetDay, + ); + + if (workout == null) { + final exercises = _generateExercises( + week: targetWeek, + day: targetDay, + trainingMaxes: trainingMaxes, + bodyweight: user.currentBodyweight, + ); + + final userId = user.serverId ?? user.id.toString(); + + workout = await workoutRepo.createWorkout( + userId: userId, + cycleId: cycleRefId, + week: targetWeek, + day: targetDay, + exercisesJson: jsonEncode(exercises.map((e) => e.toJson()).toList()), + ); + } + + if (mounted) { + context.go('/battle', extra: { + 'week': targetWeek, + 'day': targetDay, + 'workoutId': workout!.id, + }); + } + } catch (e) { + debugPrint('Failed to start workout: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final userRepo = ref.watch(userRepositoryProvider); + final cycleRepo = ref.watch(cycleRepositoryProvider); + + return Scaffold( + body: SafeArea( + child: FutureBuilder( + future: Future.wait([ + userRepo.getLocalUser(), + cycleRepo.getCurrentCycle(), + ]), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + final user = snapshot.data![0] as UserCollection?; + final cycle = snapshot.data![1] as CycleCollection?; + final avatarConfig = user?.avatarConfigJson != null + ? AvatarConfig.fromJson(jsonDecode(user!.avatarConfigJson!)) + : const AvatarConfig(); + + if (user == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.go('/login'); + }); + return const SizedBox(); + } + + final xpProgress = XPCalculator.xpProgressPercentage( + user.xp, + user.level, + ); + final nextLevelXP = XPCalculator.xpForNextLevel(user.level); + + return Stack( + children: [ + Positioned.fill( + child: Image.asset( + AssetPaths.bgStreetParkDay, // Das düstere Gym + fit: BoxFit.cover, + ), + ), + // Dunkler Overlay, damit die UI-Elemente gut lesbar sind + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.6), // Oben etwas heller + Colors.black.withOpacity( + 0.85), // Unten fast schwarz für Buttons + ], + ), + ), + ), + ), + // Container( + // decoration: BoxDecoration( + // gradient: LinearGradient( + // begin: Alignment.topCenter, + // end: Alignment.bottomCenter, + // colors: [ + // const Color(0xFF1A237E), + // AppTheme.backgroundColor, + // ], + // ), + // ), + // ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.person_outline), + onPressed: () => context.go('/profile'), + ), + IconButton( + icon: _isSyncing + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white)) + : Icon( + user.isDirty + ? Icons.cloud_upload_outlined + : Icons.cloud_done_outlined, + color: user.isDirty + ? AppTheme.secondaryColor + : AppTheme.successColor, + ), + onPressed: _runSync, + ), + ], + ), + ), + const Spacer(flex: 1), + AvatarRenderer( + config: avatarConfig, + size: 160, // Etwas größer für den Hub + ), + // Container( + // width: 120, + // height: 120, + // decoration: BoxDecoration( + // color: AppTheme.primaryColor.withOpacity(0.2), + // shape: BoxShape.circle, + // border: + // Border.all(color: AppTheme.primaryColor, width: 3), + // ), + // child: const Icon( + // Icons.fitness_center, + // size: 64, + // color: AppTheme.primaryColor, + // ), + // ), + const SizedBox(height: 24), + LevelDisplay(level: user.level), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: XPBarWidget( + currentXP: user.xp, + level: user.level, + progress: xpProgress, + nextLevelXP: nextLevelXP, + ), + ), + const Spacer(flex: 2), + if (cycle != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: StartRaidButton( + onPressed: () => _startNextWorkout(cycle, user), + ), + ) + else + Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Text( + 'No active cycle', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.push('/onboarding/strength-test'); + }, + child: const Text('Create New Cycle'), + ), + ], + ), + ), + const SizedBox(height: 24), + if (cycle != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _StatBox( + label: 'Cycle', value: '#${cycle.cycleNumber}'), + _StatBox(label: 'Active', value: 'Yes'), + ], + ), + ), + const Spacer(flex: 1), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + decoration: BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _NavButton( + icon: Icons.history, + label: 'History', + onTap: () => context.go('/history'), + ), + _NavButton( + icon: Icons.inventory_2_outlined, + label: 'Inventory', + onTap: () => context.go('/inventory'), + ), + _NavButton( + icon: Icons.bar_chart, + label: 'Stats', + onTap: () { + context.go('/stats'); + }, + ), + _NavButton( + icon: Icons.auto_stories, // Buch Icon + label: 'Codex', + onTap: () => context.go('/codex'), + ), + ], + ), + ), + ], + ), + ], + ); + }, + ), + ), + ); + } +} + +class _StatBox extends StatelessWidget { + final String label; + final String value; + + const _StatBox({ + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.3), + ), + ), + child: Column( + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 4), + Text( + value, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} + +class _NavButton extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _NavButton({ + required this.icon, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: AppTheme.primaryColor), + const SizedBox(height: 4), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/dashboard/presentation/widgets/level_display.dart b/lib/src/features/dashboard/presentation/widgets/level_display.dart new file mode 100644 index 0000000..73a651c --- /dev/null +++ b/lib/src/features/dashboard/presentation/widgets/level_display.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; + +class LevelDisplay extends StatelessWidget { + final int level; + + const LevelDisplay({ + super.key, + required this.level, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.primaryColor, + AppTheme.secondaryColor, + ], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.5), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.military_tech, + color: Colors.black, + size: 28, + ), + const SizedBox(width: 8), + Text( + 'LEVEL', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + ), + ), + const SizedBox(width: 8), + Text( + level.toString(), + style: Theme.of(context).textTheme.displayMedium?.copyWith( + color: Colors.black, + fontSize: 32, + ), + ), + ], + ), + ); + } +} + diff --git a/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart new file mode 100644 index 0000000..e46d41e --- /dev/null +++ b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; + +class StartRaidButton extends StatefulWidget { + final VoidCallback onPressed; + + const StartRaidButton({ + super.key, + required this.onPressed, + }); + + @override + State createState() => _StartRaidButtonState(); +} + +class _StartRaidButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _glowAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _scaleAnimation = Tween(begin: 1.0, end: 1.05).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + + _glowAnimation = Tween(begin: 0.3, end: 0.6).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(_glowAnimation.value), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: ElevatedButton( + onPressed: widget.onPressed, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 48, + vertical: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.flash_on, + size: 32, + color: Colors.black, + ), + const SizedBox(width: 12), + Text( + 'START RAID', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontSize: 20, + color: Colors.black, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + diff --git a/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart b/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart new file mode 100644 index 0000000..3b5e5fb --- /dev/null +++ b/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; + +class XPBarWidget extends StatelessWidget { + final int currentXP; + final int level; + final double progress; + final int nextLevelXP; + + const XPBarWidget({ + super.key, + required this.currentXP, + required this.level, + required this.progress, + required this.nextLevelXP, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // XP Text + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'XP', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '$currentXP / $nextLevelXP', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + + // Progress Bar + Stack( + children: [ + // Background + Container( + height: 32, + decoration: BoxDecoration( + color: AppTheme.xpBarBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.3), + width: 2, + ), + ), + ), + + // Fill + FractionallySizedBox( + widthFactor: progress.clamp(0.0, 1.0), + child: Container( + height: 32, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.primaryColor, + AppTheme.primaryColor.withOpacity(0.7), + ], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.5), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + ), + ), + + // Percentage Text + Container( + height: 32, + alignment: Alignment.center, + child: Text( + '${(progress * 100).toStringAsFixed(0)}%', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + const Shadow( + color: Colors.black, + blurRadius: 4, + ), + ], + ), + ), + ), + ], + ), + ], + ); + } +} + diff --git a/lib/src/features/gamification/domain/entities/avatar_config.dart b/lib/src/features/gamification/domain/entities/avatar_config.dart new file mode 100644 index 0000000..e7ab4a0 --- /dev/null +++ b/lib/src/features/gamification/domain/entities/avatar_config.dart @@ -0,0 +1,72 @@ +// import 'package:flutter/material.dart'; +// import '../../../../core/constants/asset_paths.dart'; + +// class AvatarConfig { +// final String gender; +// final String skinTone; +// final String hairStyle; +// final String clothing; + +// const AvatarConfig({ +// this.gender = 'male', +// this.skinTone = 'medium', +// this.hairStyle = 'short_01', +// this.clothing = 'basic_tee', +// }); + +// factory AvatarConfig.fromJson(Map json) { +// return AvatarConfig( +// gender: json['gender'] ?? 'male', +// skinTone: json['skin_tone'] ?? 'medium', +// hairStyle: json['hair_style'] ?? 'short_01', +// clothing: json['clothing'] ?? 'basic_tee', +// ); +// } + +// Map toJson() { +// return { +// 'gender': gender, +// 'skin_tone': skinTone, +// 'hair_style': hairStyle, +// 'clothing': clothing, +// }; +// } + +// // Helper getters +// String get baseAsset => gender == 'female' +// ? AssetPaths.avatarFemaleBase +// : AssetPaths.avatarMaleBase; + +// Color get skinColor => AvatarConstants.skinTones[skinTone] ?? const Color(0xFFD1A384); + +// String? get hairAsset => AvatarConstants.hairStyles[hairStyle]; + +// String? get clothingAsset => AvatarConstants.clothing[clothing]; +// } +import '../../../../core/constants/asset_paths.dart'; + +class AvatarConfig { + final String gender; // 'male' or 'female' + final int variant; // 1 to 8 + + const AvatarConfig({ + this.gender = 'male', + this.variant = 1, + }); + + factory AvatarConfig.fromJson(Map json) { + return AvatarConfig( + gender: json['gender'] ?? 'male', + variant: json['variant'] ?? 1, + ); + } + + Map toJson() { + return { + 'gender': gender, + 'variant': variant, + }; + } + + String get assetPath => AssetPaths.getAvatarPath(gender, variant); +} diff --git a/lib/src/features/gamification/presentation/screens/codex_screen.dart b/lib/src/features/gamification/presentation/screens/codex_screen.dart new file mode 100644 index 0000000..a1a1944 --- /dev/null +++ b/lib/src/features/gamification/presentation/screens/codex_screen.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/asset_paths.dart'; + +class CodexScreen extends StatelessWidget { + const CodexScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Creature Codex'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/hub'), + ), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: const [ + _LoreCard( + name: 'Iron Golem', + title: 'The Weight of the Earth', + description: + 'Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. ' + 'It embodies the unrelenting force of gravity acting on a heavy load.\n\n' + 'It respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.', + assetPath: AssetPaths.enemyIronGolem, + exercise: 'Squat Nemesis', + color: Colors.redAccent, + ), + SizedBox(height: 24), + _LoreCard( + name: 'Gravity Demon', + title: 'The Abyssal Pull', + description: + 'A spirit of pure downward force that clings to the back of adventurers. ' + 'It whispers lies of weakness into your ear while dragging you towards the abyss.\n\n' + 'Only those with a back of steel and the will to pull themselves up can escape its grasp.', + assetPath: AssetPaths.enemyGravityDemon, + exercise: 'Pull-up Nemesis', + color: Colors.purpleAccent, + ), + SizedBox(height: 24), + _LoreCard( + name: 'Pressure Phantom', + title: 'The Invisible Crusher', + description: + 'An ethereal entity that compresses the very air around you. ' + 'It seeks to collapse the chest and shoulders of any who dare to push against it.\n\n' + 'Defeat it by pushing through the pain with explosive dipping power.', + assetPath: AssetPaths.enemyPressurePhantom, + exercise: 'Dip Nemesis', + color: Colors.cyanAccent, + ), + ], + ), + ); + } +} + +class _LoreCard extends StatelessWidget { + final String name; + final String title; + final String description; + final String assetPath; + final String exercise; + final Color color; + + const _LoreCard({ + required this.name, + required this.title, + required this.description, + required this.assetPath, + required this.exercise, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.antiAlias, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: color.withOpacity(0.5), width: 1), + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.surfaceColor, + color.withOpacity(0.1), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header Image + Container( + height: 200, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black26, + image: DecorationImage( + image: AssetImage(AssetPaths.bgUndergroundGym), // Hintergrund für Atmosphäre + fit: BoxFit.cover, + opacity: 0.3, + ), + ), + child: Center( + child: Image.asset( + assetPath, + fit: BoxFit.contain, + color: Colors.white.withOpacity(0.9), + colorBlendMode: BlendMode.modulate, + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + name.toUpperCase(), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: color, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + Chip( + label: Text(exercise, style: const TextStyle(fontSize: 10, color: Colors.white)), + backgroundColor: Colors.black54, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + ], + ), + Text( + title, + style: TextStyle( + color: AppTheme.textSecondary, + fontStyle: FontStyle.italic, + ), + ), + const Divider(height: 24), + Text( + description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + height: 1.5, + color: Colors.white70, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/gamification/presentation/widgets/avatar_editor.dart b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart new file mode 100644 index 0000000..6c926af --- /dev/null +++ b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart @@ -0,0 +1,335 @@ +// import 'package:flutter/material.dart'; +// import '../../../../core/theme/app_theme.dart'; +// import '../../../../core/constants/asset_paths.dart'; +// import '../../domain/entities/avatar_config.dart'; +// import 'avatar_renderer.dart'; + +// class AvatarEditor extends StatefulWidget { +// final AvatarConfig initialConfig; +// final ValueChanged onChanged; + +// const AvatarEditor({ +// super.key, +// required this.initialConfig, +// required this.onChanged, +// }); + +// @override +// State createState() => _AvatarEditorState(); +// } + +// class _AvatarEditorState extends State { +// late AvatarConfig _config; +// String _selectedTab = 'Body'; // Body, Hair, Style + +// @override +// void initState() { +// super.initState(); +// _config = widget.initialConfig; +// } + +// void _updateConfig(AvatarConfig newConfig) { +// setState(() => _config = newConfig); +// widget.onChanged(newConfig); +// } + +// @override +// Widget build(BuildContext context) { +// return Column( +// children: [ +// // 1. Live Preview +// Container( +// height: 220, +// alignment: Alignment.center, +// decoration: BoxDecoration( +// color: AppTheme.surfaceColor, +// border: const Border(bottom: BorderSide(color: Colors.white10)), +// ), +// child: AvatarRenderer(config: _config, size: 180), +// ), + +// // 2. Tabs +// Row( +// children: [ +// _buildTab('Body'), +// _buildTab('Hair'), +// _buildTab('Style'), +// ], +// ), + +// // 3. Options Grid +// Expanded( +// child: Container( +// color: AppTheme.backgroundColor, +// child: _buildOptionsGrid(), +// ), +// ), +// ], +// ); +// } + +// Widget _buildTab(String label) { +// final isSelected = _selectedTab == label; +// return Expanded( +// child: GestureDetector( +// onTap: () => setState(() => _selectedTab = label), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 16), +// decoration: BoxDecoration( +// border: Border( +// bottom: BorderSide( +// color: isSelected ? AppTheme.primaryColor : Colors.transparent, +// width: 2, +// ), +// ), +// ), +// child: Text( +// label, +// textAlign: TextAlign.center, +// style: TextStyle( +// color: isSelected ? AppTheme.primaryColor : Colors.grey, +// fontWeight: FontWeight.bold, +// ), +// ), +// ), +// ), +// ); +// } + +// Widget _buildOptionsGrid() { +// switch (_selectedTab) { +// case 'Body': +// return ListView( +// padding: const EdgeInsets.all(16), +// children: [ +// const Text('Gender', style: TextStyle(color: Colors.grey)), +// const SizedBox(height: 8), +// Row( +// children: [ +// _buildChip('Male', _config.gender == 'male', () { +// _updateConfig(AvatarConfig( +// gender: 'male', skinTone: _config.skinTone, +// hairStyle: _config.hairStyle, clothing: _config.clothing)); +// }), +// const SizedBox(width: 8), +// _buildChip('Female', _config.gender == 'female', () { +// _updateConfig(AvatarConfig( +// gender: 'female', skinTone: _config.skinTone, +// hairStyle: _config.hairStyle, clothing: _config.clothing)); +// }), +// ], +// ), +// const SizedBox(height: 24), +// const Text('Skin Tone', style: TextStyle(color: Colors.grey)), +// const SizedBox(height: 12), +// Wrap( +// spacing: 12, +// runSpacing: 12, +// children: AvatarConstants.skinTones.entries.map((e) { +// return GestureDetector( +// onTap: () { +// _updateConfig(AvatarConfig( +// gender: _config.gender, skinTone: e.key, +// hairStyle: _config.hairStyle, clothing: _config.clothing)); +// }, +// child: Container( +// width: 48, +// height: 48, +// decoration: BoxDecoration( +// color: e.value, +// shape: BoxShape.circle, +// border: Border.all( +// color: _config.skinTone == e.key ? AppTheme.primaryColor : Colors.transparent, +// width: 3, +// ), +// ), +// ), +// ); +// }).toList(), +// ), +// ], +// ); +// case 'Hair': +// return _buildGrid(AvatarConstants.hairStyles.keys.toList(), (key) { +// _updateConfig(AvatarConfig( +// gender: _config.gender, skinTone: _config.skinTone, +// hairStyle: key, clothing: _config.clothing)); +// }, _config.hairStyle); +// case 'Style': +// return _buildGrid(AvatarConstants.clothing.keys.toList(), (key) { +// _updateConfig(AvatarConfig( +// gender: _config.gender, skinTone: _config.skinTone, +// hairStyle: _config.hairStyle, clothing: key)); +// }, _config.clothing); +// default: +// return const SizedBox(); +// } +// } + +// Widget _buildGrid(List items, Function(String) onSelect, String current) { +// return GridView.builder( +// padding: const EdgeInsets.all(16), +// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( +// crossAxisCount: 3, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 1.0), +// itemCount: items.length, +// itemBuilder: (context, index) { +// final key = items[index]; +// final isSelected = current == key; +// return GestureDetector( +// onTap: () => onSelect(key), +// child: Container( +// decoration: BoxDecoration( +// color: AppTheme.surfaceColor, +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: isSelected ? AppTheme.primaryColor : Colors.transparent, +// width: 2, +// ), +// ), +// child: Center( +// // Für MVP zeigen wir den Text-Key, später könnte hier ein Icon hin +// child: Text( +// key.replaceAll('_', ' ').toUpperCase(), +// textAlign: TextAlign.center, +// style: TextStyle( +// color: isSelected ? AppTheme.primaryColor : Colors.grey, +// fontSize: 10, +// fontWeight: FontWeight.bold +// ), +// ), +// ), +// ), +// ); +// }, +// ); +// } + +// Widget _buildChip(String label, bool isSelected, VoidCallback onTap) { +// return ActionChip( +// label: Text(label), +// backgroundColor: isSelected ? AppTheme.primaryColor.withOpacity(0.2) : null, +// side: BorderSide(color: isSelected ? AppTheme.primaryColor : Colors.grey), +// labelStyle: TextStyle( +// color: isSelected ? AppTheme.primaryColor : Colors.white, +// fontWeight: FontWeight.bold, +// ), +// onPressed: onTap, +// ); +// } +// } +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../domain/entities/avatar_config.dart'; +import 'avatar_renderer.dart'; + +class AvatarEditor extends StatefulWidget { + final AvatarConfig initialConfig; + final ValueChanged onChanged; + + const AvatarEditor({ + super.key, + required this.initialConfig, + required this.onChanged, + }); + + @override + State createState() => _AvatarEditorState(); +} + +class _AvatarEditorState extends State { + late String _gender; + late int _variant; + + @override + void initState() { + super.initState(); + _gender = widget.initialConfig.gender; + _variant = widget.initialConfig.variant; + } + + void _update(String gender, int variant) { + setState(() { + _gender = gender; + _variant = variant; + }); + widget.onChanged(AvatarConfig(gender: _gender, variant: _variant)); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Preview + Container( + height: 200, + alignment: Alignment.center, + color: AppTheme.surfaceColor, + child: AvatarRenderer( + config: AvatarConfig(gender: _gender, variant: _variant), + size: 160, + ), + ), + + // Gender Switch + Padding( + padding: const EdgeInsets.all(16), + child: SegmentedButton( + segments: const [ + ButtonSegment(value: 'male', label: Text('Male')), + ButtonSegment(value: 'female', label: Text('Female')), + ], + selected: {_gender}, + onSelectionChanged: (Set newSelection) { + _update( + newSelection.first, 1); // Reset to variant 1 on gender switch + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? AppTheme.primaryColor + : Colors.transparent), + ), + ), + ), + + // Variants Grid + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: 8, // Wir haben 8 Varianten pro Sheet + itemBuilder: (context, index) { + final variantNum = index + 1; + final isSelected = _variant == variantNum; + + return GestureDetector( + onTap: () => _update(_gender, variantNum), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? AppTheme.primaryColor + : Colors.transparent, + width: 3, + ), + borderRadius: BorderRadius.circular(8), + color: Colors.white10, + ), + padding: const EdgeInsets.all(4), + child: Image.asset( + 'assets/images/avatars/$_gender/$variantNum.png', + fit: BoxFit.contain, + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart new file mode 100644 index 0000000..d06f584 --- /dev/null +++ b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart @@ -0,0 +1,128 @@ +// import 'package:flutter/material.dart'; +// import '../../domain/entities/avatar_config.dart'; + +// class AvatarRenderer extends StatelessWidget { +// final AvatarConfig config; +// final double size; + +// const AvatarRenderer({ +// super.key, +// required this.config, +// this.size = 120.0, +// }); + +// @override +// Widget build(BuildContext context) { +// return SizedBox( +// width: size, +// height: size, +// child: Stack( +// alignment: Alignment.center, +// children: [ +// // 1. Layer: Background Circle (Optional, for glow) +// Container( +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// boxShadow: [ +// BoxShadow( +// color: Colors.black.withOpacity(0.3), +// blurRadius: 10, +// spreadRadius: 2, +// ), +// ], +// ), +// ), + +// // 2. Layer: Body Base (Tinted with Skin Tone) +// _buildLayer( +// assetPath: config.baseAsset, +// color: config.skinColor, +// blendMode: BlendMode.modulate, // Färbt das weiße Pixel-Art ein +// ), + +// // 3. Layer: Eyes/Face (Könnte separat sein, hier Teil der Base angenommen oder weggelassen) + +// // 4. Layer: Clothing +// if (config.clothingAsset != null) +// _buildLayer(assetPath: config.clothingAsset!), + +// // 5. Layer: Hair +// if (config.hairAsset != null) +// _buildLayer(assetPath: config.hairAsset!), +// ], +// ), +// ); +// } + +// Widget _buildLayer({ +// required String assetPath, +// Color? color, +// BlendMode? blendMode, +// }) { +// return Image.asset( +// assetPath, +// width: size, +// height: size, +// fit: BoxFit.contain, +// color: color, +// colorBlendMode: blendMode, +// // Fallback, falls Assets fehlen (damit die App nicht abstürzt) +// errorBuilder: (context, error, stackTrace) { +// return Container( +// width: size, +// height: size, +// decoration: BoxDecoration( +// color: Colors.grey.withOpacity(0.2), +// shape: BoxShape.circle, +// border: Border.all(color: Colors.red.withOpacity(0.3)), +// ), +// child: const Center( +// child: Icon(Icons.broken_image, size: 20, color: Colors.white24), +// ), +// ); +// }, +// ); +// } +// } +import 'package:flutter/material.dart'; +import '../../domain/entities/avatar_config.dart'; + +class AvatarRenderer extends StatelessWidget { + final AvatarConfig config; + final double size; + + const AvatarRenderer({ + super.key, + required this.config, + this.size = 120.0, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Image.asset( + config.assetPath, + fit: BoxFit.contain, + errorBuilder: (c, o, s) => Container( + decoration: BoxDecoration( + color: Colors.grey[800], + shape: BoxShape.circle, + ), + child: const Icon(Icons.person, color: Colors.white54), + ), + ), + ); + } +} diff --git a/lib/src/features/history/presentation/screens/history_screen.dart b/lib/src/features/history/presentation/screens/history_screen.dart new file mode 100644 index 0000000..20425b5 --- /dev/null +++ b/lib/src/features/history/presentation/screens/history_screen.dart @@ -0,0 +1,431 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:go_router/go_router.dart'; +// import 'package:intl/intl.dart'; + +// import '../../../../core/theme/app_theme.dart'; +// import '../../../../shared/data/repositories/workout_repository.dart'; +// import '../../../../shared/data/local/collections/workout_collection.dart'; + +// class HistoryScreen extends ConsumerStatefulWidget { +// const HistoryScreen({super.key}); + +// @override +// ConsumerState createState() => _HistoryScreenState(); +// } + +// class _HistoryScreenState extends ConsumerState { +// @override +// Widget build(BuildContext context) { +// final workoutRepo = ref.watch(workoutRepositoryProvider); + +// return Scaffold( +// appBar: AppBar( +// title: const Text('Workout History'), +// leading: IconButton( +// icon: const Icon(Icons.arrow_back), +// onPressed: () => context.go('/hub'), +// ), +// ), +// body: FutureBuilder>( +// future: workoutRepo.getCompletedWorkouts(), +// builder: (context, snapshot) { +// if (snapshot.connectionState == ConnectionState.waiting) { +// return const Center(child: CircularProgressIndicator()); +// } + +// if (!snapshot.hasData || snapshot.data!.isEmpty) { +// return Center( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Icon( +// Icons.history, +// size: 80, +// color: AppTheme.primaryColor.withOpacity(0.5), +// ), +// const SizedBox(height: 16), +// Text( +// 'No workout history yet', +// style: Theme.of(context).textTheme.headlineMedium, +// ), +// const SizedBox(height: 8), +// Text( +// 'Complete your first workout to see it here', +// style: Theme.of(context).textTheme.bodyMedium, +// ), +// ], +// ), +// ); +// } + +// // Sort by date descending +// final workouts = snapshot.data! +// ..sort((a, b) => b.completedAt!.compareTo(a.completedAt!)); + +// return ListView.builder( +// padding: const EdgeInsets.all(16), +// itemCount: workouts.length, +// itemBuilder: (context, index) { +// final workout = workouts[index]; +// final dateStr = DateFormat.yMMMd().format(workout.completedAt!); +// final timeStr = DateFormat.jm().format(workout.completedAt!); + +// return Card( +// margin: const EdgeInsets.only(bottom: 16), +// child: ExpansionTile( +// leading: Container( +// width: 48, +// height: 48, +// decoration: BoxDecoration( +// color: AppTheme.primaryColor.withOpacity(0.2), +// borderRadius: BorderRadius.circular(8), +// ), +// child: Center( +// child: Text( +// 'W${workout.week}\nD${workout.day}', +// textAlign: TextAlign.center, +// style: const TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 12, +// ), +// ), +// ), +// ), +// title: Text( +// dateStr, +// style: Theme.of(context).textTheme.titleMedium?.copyWith( +// color: AppTheme.primaryColor, +// ), +// ), +// subtitle: Text('$timeStr • ${workout.xpEarned} XP'), +// children: [ +// Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// if (workout.notes.isNotEmpty) ...[ +// Text( +// 'Notes:', +// style: Theme.of(context).textTheme.labelLarge, +// ), +// Text(workout.notes), +// const SizedBox(height: 16), +// ], +// // We could parse exercisesJson here to show details +// // For MVP, just showing basic completion info +// Text( +// 'Workout Completed', +// style: TextStyle(color: AppTheme.successColor), +// ), +// ], +// ), +// ), +// ], +// ), +// ); +// }, +// ); +// }, +// ), +// ); +// } +// } +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/workout_repository.dart'; +import '../../../../shared/data/local/collections/workout_collection.dart'; +import '../../../../shared/domain/entities/exercise.dart'; +import '../../../../shared/domain/entities/workout_set.dart'; + +class HistoryScreen extends ConsumerStatefulWidget { + const HistoryScreen({super.key}); + + @override + ConsumerState createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends ConsumerState { + Future> _loadHistory() async { + final userRepo = ref.read(userRepositoryProvider); + final workoutRepo = ref.read(workoutRepositoryProvider); + + final user = await userRepo.getLocalUser(); + if (user == null) return []; + + final userId = user.serverId ?? user.id.toString(); + return workoutRepo.getCompletedWorkouts(userId); // ID übergeben + } + + @override + Widget build(BuildContext context) { + final workoutRepo = ref.watch(workoutRepositoryProvider); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'Quest Log'), // "Quest Log" passt besser zum RPG Theme als "History" + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/hub'), + ), + ), + body: FutureBuilder>( + // future: workoutRepo.getCompletedWorkouts(), + future: _loadHistory(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.history_edu, + size: 80, + color: AppTheme.primaryColor.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + 'No completed quests yet', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + Text( + 'Complete a workout to fill your journal', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ); + } + + // Sort by date descending (newest first) + final workouts = snapshot.data! + ..sort((a, b) => b.completedAt!.compareTo(a.completedAt!)); + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: workouts.length, + itemBuilder: (context, index) { + final workout = workouts[index]; + return _WorkoutHistoryCard(workout: workout); + }, + ); + }, + ), + ); + } +} + +class _WorkoutHistoryCard extends StatelessWidget { + final WorkoutCollection workout; + + const _WorkoutHistoryCard({required this.workout}); + + List _parseExercises() { + try { + final List jsonList = jsonDecode(workout.exercisesJson); + return jsonList.map((json) => Exercise.fromJson(json)).toList(); + } catch (e) { + debugPrint('Error parsing workout history: $e'); + return []; + } + } + + @override + Widget build(BuildContext context) { + final dateStr = DateFormat.yMMMd().format(workout.completedAt!); + final timeStr = DateFormat.jm().format(workout.completedAt!); + final exercises = _parseExercises(); + + // Zusammenfassung der trainierten Muskelgruppen/Übungen für den Titel + final summary = exercises + .map((e) => + e.exerciseName.replaceAll('Weighted ', '').replaceAll('Back ', '')) + .toSet() + .join(' & '); + + return Card( + margin: const EdgeInsets.only(bottom: 16), + child: ExpansionTile( + tilePadding: const EdgeInsets.all(16), + leading: _buildDateBadge(context, workout), + title: Text( + summary.isEmpty ? 'Unknown Workout' : summary, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon(Icons.access_time, size: 14, color: AppTheme.textSecondary), + const SizedBox(width: 4), + Text('$dateStr • $timeStr', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: AppTheme.textSecondary)), + const Spacer(), + Text('+${workout.xpEarned} XP', + style: TextStyle( + color: AppTheme.secondaryColor, + fontWeight: FontWeight.bold)), + ], + ), + ), + children: [ + if (workout.notes.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white10), + ), + child: Text( + '📝 "${workout.notes}"', + style: const TextStyle( + fontStyle: FontStyle.italic, color: AppTheme.textPrimary), + ), + ), + ), + ...exercises + .map((exercise) => _ExerciseDetailRow(exercise: exercise)) + .toList(), + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildDateBadge(BuildContext context, WorkoutCollection workout) { + return Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'W${workout.week}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + ), + Text( + 'D${workout.day}', + style: TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 16), + ), + ], + ), + ); + } +} + +class _ExerciseDetailRow extends StatelessWidget { + final Exercise exercise; + + const _ExerciseDetailRow({required this.exercise}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 4, + height: 16, + color: AppTheme.primaryColor, + margin: const EdgeInsets.only(right: 8), + ), + Text( + exercise.exerciseName, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, color: AppTheme.textSecondary), + ), + ], + ), + const SizedBox(height: 8), + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), // Set + 1: FlexColumnWidth(2), // Weight + 2: FlexColumnWidth(2), // Reps + 3: FlexColumnWidth(1), // Type (AMRAP/FSL) + }, + children: exercise.sets.where((s) => s.completed).map((set) { + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + '#${set.setNumber}', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + Text( + '${set.targetWeightTotal} kg', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Row( + children: [ + Text( + '${set.repsActual}', + style: TextStyle( + color: set.isAmrap + ? AppTheme.secondaryColor + : (set.repsActual >= set.repsTarget + ? AppTheme.successColor + : AppTheme.errorColor), + fontWeight: FontWeight.bold, + ), + ), + Text( + ' / ${set.repsTarget}', + style: + const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + if (set.isAmrap) + const Icon(Icons.local_fire_department, + size: 16, color: AppTheme.secondaryColor) + else + const SizedBox(), + ], + ); + }).toList(), + ), + const Divider(), + ], + ), + ); + } +} diff --git a/lib/src/features/inventory/presentation/screens/inventory_screen.dart b/lib/src/features/inventory/presentation/screens/inventory_screen.dart new file mode 100644 index 0000000..1372aad --- /dev/null +++ b/lib/src/features/inventory/presentation/screens/inventory_screen.dart @@ -0,0 +1,336 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/app_constants.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../widgets/plate_counter.dart'; + +class InventoryScreen extends ConsumerStatefulWidget { + const InventoryScreen({super.key}); + + @override + ConsumerState createState() => _InventoryScreenState(); +} + +class _InventoryScreenState extends ConsumerState { + double _barWeight = 20.0; + Map _plateInventory = {}; + Map _bandInventory = {}; + bool _isLoading = true; + bool _hasChanges = false; + + @override + void initState() { + super.initState(); + _loadCurrentInventory(); + } + + Future _loadCurrentInventory() async { + final userRepo = ref.read(userRepositoryProvider); + final inventory = userRepo.getInventorySettings(); + + final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0; + + final platesList = (inventory['plates'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList() ?? + []; + final plateMap = { + 25.0: 0, + 20.0: 0, + 15.0: 0, + 10.0: 0, + 5.0: 0, + 2.5: 0, + 1.25: 0 + }; + + for (var p in platesList) { + if (plateMap.containsKey(p)) { + plateMap[p] = (plateMap[p] ?? 0) + 1; + } + } + + final bandsList = (inventory['bands'] as List?) ?? []; + final bandMap = { + 'Blue': false, + 'Green': false, + 'Orange': false, + 'Red': false + }; + + for (var b in bandsList) { + final color = b['color'] as String; + if (bandMap.containsKey(color)) { + bandMap[color] = true; + } + } + + if (mounted) { + setState(() { + _barWeight = barWeight; + _plateInventory = plateMap; + _bandInventory = bandMap; + _isLoading = false; + }); + } + } + + void _applyPreset(String preset) { + setState(() { + _hasChanges = true; + switch (preset) { + case 'home': + _plateInventory = { + 25.0: 0, + 20.0: 2, + 15.0: 0, + 10.0: 2, + 5.0: 2, + 2.5: 2, + 1.25: 2 + }; + break; + case 'commercial': + _plateInventory = { + 25.0: 4, + 20.0: 4, + 15.0: 2, + 10.0: 4, + 5.0: 4, + 2.5: 4, + 1.25: 2 + }; + break; + case 'minimal': + _plateInventory = { + 25.0: 0, + 20.0: 2, + 15.0: 0, + 10.0: 0, + 5.0: 2, + 2.5: 0, + 1.25: 0 + }; + break; + } + }); + } + + Future _saveChanges() async { + setState(() => _isLoading = true); + try { + final userRepo = ref.read(userRepositoryProvider); + + final platesList = []; + _plateInventory.forEach((weight, count) { + for (int i = 0; i < count; i++) platesList.add(weight); + }); + + final bandsList = >[]; + _bandInventory.forEach((color, isSelected) { + if (isSelected) { + bandsList.add({ + 'color': color, + 'resistance_kg': AppConstants.defaultBands[color] ?? 0.0, + 'count': 1, + }); + } + }); + + final newSettings = { + 'bar_weight': _barWeight, + 'plates': platesList, + 'bands': bandsList, + }; + + await userRepo.updateInventory(newSettings); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Inventory updated successfully')), + ); + setState(() { + _hasChanges = false; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error saving: $e'), + backgroundColor: AppTheme.errorColor), + ); + } + } + } + + Color _getBandColor(String name) { + switch (name) { + case 'Blue': + return Colors.blue; + case 'Green': + return Colors.green; + case 'Orange': + return Colors.orange; + case 'Red': + return Colors.red; + default: + return Colors.grey; + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading && !_hasChanges) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Manage Equipment'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/hub'), + ), + actions: [ + if (_hasChanges) + TextButton( + onPressed: _saveChanges, + child: const Text('SAVE', + style: TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold)), + ) + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Barbell Weight', + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + Row( + children: [ + const Icon(Icons.fitness_center, + size: 32, color: AppTheme.primaryColor), + Expanded( + child: Slider( + value: _barWeight, + min: 10, + max: 25, + divisions: 6, + label: '$_barWeight kg', + activeColor: AppTheme.primaryColor, + onChanged: (val) => setState(() { + _barWeight = val; + _hasChanges = true; + }), + ), + ), + Text('${_barWeight.toStringAsFixed(1)} kg', + style: + const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Text('Quick Presets', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: AppTheme.textSecondary)), + const SizedBox(height: 8), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ActionChip( + label: const Text('Home Gym'), + onPressed: () => _applyPreset('home')), + const SizedBox(width: 8), + ActionChip( + label: const Text('Commercial'), + onPressed: () => _applyPreset('commercial')), + const SizedBox(width: 8), + ActionChip( + label: const Text('Minimal'), + onPressed: () => _applyPreset('minimal')), + ], + ), + ), + const SizedBox(height: 24), + Text('Plates Available', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: AppTheme.textSecondary)), + const SizedBox(height: 8), + ..._plateInventory.entries.map((entry) { + return PlateCounter( + weight: entry.key, + count: entry.value, + onChanged: (newCount) { + setState(() { + _plateInventory[entry.key] = newCount; + _hasChanges = true; + }); + }, + ); + }), + const SizedBox(height: 24), + Text('Resistance Bands (Assistance)', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: AppTheme.textSecondary)), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _bandInventory.entries.map((entry) { + final resistance = AppConstants.defaultBands[entry.key] ?? 0; + return FilterChip( + label: Text('${entry.key} (~${resistance.toInt()}kg)'), + selected: entry.value, + onSelected: (bool selected) { + setState(() { + _bandInventory[entry.key] = selected; + _hasChanges = true; + }); + }, + selectedColor: _getBandColor(entry.key).withOpacity(0.3), + checkmarkColor: _getBandColor(entry.key), + side: BorderSide(color: _getBandColor(entry.key)), + ); + }).toList(), + ), + const SizedBox(height: 40), + if (_hasChanges) + ElevatedButton( + onPressed: _isLoading ? null : _saveChanges, + child: _isLoading + ? const CircularProgressIndicator(color: Colors.black) + : const Text('SAVE CHANGES'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/inventory/presentation/widgets/plate_counter.dart b/lib/src/features/inventory/presentation/widgets/plate_counter.dart new file mode 100644 index 0000000..1b8e941 --- /dev/null +++ b/lib/src/features/inventory/presentation/widgets/plate_counter.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/asset_paths.dart'; + +class PlateCounter extends StatelessWidget { + final double weight; + final int count; + final ValueChanged onChanged; + + const PlateCounter({ + super.key, + required this.weight, + required this.count, + required this.onChanged, + }); + + Color _getPlateColor() { + final colorValue = PlateColors.colors[weight]; + return colorValue != null ? Color(colorValue) : Colors.grey; + } + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // Plate Visual + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _getPlateColor(), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white24, + width: 2, + ), + ), + child: Center( + child: Text( + weight == weight.toInt() + ? '${weight.toInt()}' + : weight.toStringAsFixed(2), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + const SizedBox(width: 16), + + // Weight Label + Expanded( + child: Text( + '${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + + // Counter + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: count > 0 ? () => onChanged(count - 1) : null, + color: AppTheme.primaryColor, + ), + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), // Hintergrund + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), + ), + child: Text( + count.toString(), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, // Explizit Weiß + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + // SizedBox( + // width: 32, + // child: Text( + // count.toString(), + // style: Theme.of(context).textTheme.titleLarge, + // textAlign: TextAlign.center, + // ), + // ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: count < 20 + ? () => onChanged(count + 1) + : null, // Limit erhöht auf 20 + color: AppTheme.primaryColor, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart new file mode 100644 index 0000000..efe1dc5 --- /dev/null +++ b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/app_constants.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../gamification/domain/entities/avatar_config.dart'; +import '../../../gamification/presentation/widgets/avatar_editor.dart'; +import 'bodyweight_input_screen.dart'; // Für den Provider + +class AvatarSetupScreen extends ConsumerStatefulWidget { + const AvatarSetupScreen({super.key}); + + @override + ConsumerState createState() => _AvatarSetupScreenState(); +} + +class _AvatarSetupScreenState extends ConsumerState { + AvatarConfig _config = const AvatarConfig(); + bool _isLoading = false; + + // Future _handleFinish() async { + // setState(() => _isLoading = true); + + // try { + // final onboardingData = ref.read(onboardingDataProvider); + // final userRepo = ref.read(userRepositoryProvider); + + // // Inventory Settings aus dem Provider holen (muss dort gespeichert worden sein) + // final inventorySettings = onboardingData['inventory_settings'] as Map; + + // // Registrierung durchführen + // final user = await userRepo.register( + // email: onboardingData['email'] ?? '', + // password: onboardingData['password'] ?? '', + // bodyweight: onboardingData['bodyweight'] ?? 80.0, + // inventorySettings: inventorySettings, + // ); + + // // Avatar speichern (separates Update, da register meist nur Basisdaten nimmt, + // // oder wir packen es direkt in register rein, wenn die API es erlaubt. + // // Hier machen wir es sicherheitshalber als Update, falls register streng ist). + // // Update: UserRepo.register unterstützt avatarConfig laut Code! + // // Aber wir haben UserRepo.register schon aufgerufen. Da wir den User jetzt lokal haben, + // // können wir das avatarConfigJson updaten und speichern. + + // // Update local user with avatar config + // user.avatarConfigJson = jsonEncode(_config.toJson()); + // user.isDirty = true; + // await userRepo.saveLocalUser(user); + // // Optional: Sofort syncen, oder einfach auf Background Sync warten. + + // // Cycle erstellen + // final trainingMaxes = onboardingData['training_maxes'] as Map?; + // if (trainingMaxes != null) { + // final cycleRepo = ref.read(cycleRepositoryProvider); + // final tmMap = { + // 'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0, + // 'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0, + // 'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0, + // }; + // await cycleRepo.createCycle(tmMap); + // } + + // if (mounted) { + // ref.read(onboardingDataProvider.notifier).state = {}; // Cleanup + // context.go('/hub'); + // } + // } catch (e) { + // if (mounted) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Setup failed: $e'), backgroundColor: AppTheme.errorColor), + // ); + // } + // } finally { + // if (mounted) setState(() => _isLoading = false); + // } + // } + + Future _handleFinish() async { + setState(() => _isLoading = true); + + try { + final onboardingData = ref.read(onboardingDataProvider); + final userRepo = ref.read(userRepositoryProvider); + final inventorySettings = + onboardingData['inventory_settings'] as Map; + + // PRÜFUNG: Sind wir schon eingeloggt? (Reset Fall) + var user = await userRepo.getLocalUser(); + + if (user == null) { + // FALL A: Neuer User -> Registrieren + user = await userRepo.register( + email: onboardingData['email'] ?? '', + password: onboardingData['password'] ?? '', + bodyweight: onboardingData['bodyweight'] ?? 80.0, + inventorySettings: inventorySettings, + ); + } else { + // FALL B: Existierender User (Reset) -> Nur Updaten + user.currentBodyweight = + onboardingData['bodyweight'] ?? user.currentBodyweight; + user.inventorySettingsJson = jsonEncode(inventorySettings); + user.isDirty = true; + await userRepo.saveLocalUser(user); + + // Server Update triggern (via Repo Methoden die API rufen) + try { + await userRepo.updateBodyweight(user.currentBodyweight); + await userRepo.updateInventory(inventorySettings); + } catch (e) { + // Sync macht das später + } + } + + // Avatar speichern (für beide Fälle gleich) + user!.avatarConfigJson = jsonEncode(_config.toJson()); + user.isDirty = true; + await userRepo.saveLocalUser(user); + + // Cycle erstellen (für beide Fälle gleich) + final trainingMaxes = + onboardingData['training_maxes'] as Map?; + if (trainingMaxes != null) { + final cycleRepo = ref.read(cycleRepositoryProvider); + final tmMap = { + 'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0, + 'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0, + 'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0, + }; + await cycleRepo.createCycle(tmMap); + } + + if (mounted) { + ref.read(onboardingDataProvider.notifier).state = {}; + context.go('/hub'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Setup failed: $e'), + backgroundColor: AppTheme.errorColor), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // title: const Text('Create Character'), + title: const Text('Choose Your Hero'), // Statt "Create Character" + actions: [ + TextButton( + onPressed: _isLoading ? null : _handleFinish, + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('FINISH', + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor)), + ) + ], + ), + body: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: AppTheme.surfaceColor, + width: double.infinity, + child: const Text( + 'This is how the legends will remember you.', + textAlign: TextAlign.center, + style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey), + ), + ), + Expanded( + child: AvatarEditor( + initialConfig: _config, + onChanged: (newConfig) => _config = newConfig, + ), + ), + ], + ), + // body: AvatarEditor( + // initialConfig: _config, + // onChanged: (newConfig) => _config = newConfig, + // ), + ); + } +} diff --git a/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart b/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart new file mode 100644 index 0000000..f5569d4 --- /dev/null +++ b/lib/src/features/onboarding/presentation/screens/bodyweight_input_screen.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/app_constants.dart'; + +// Provider to store onboarding data +final onboardingDataProvider = + StateProvider>((ref) => {}); + +class BodyweightInputScreen extends ConsumerStatefulWidget { + const BodyweightInputScreen({super.key}); + + @override + ConsumerState createState() => + _BodyweightInputScreenState(); +} + +class _BodyweightInputScreenState + extends ConsumerState { + double _bodyweight = 80.0; + bool _useKg = true; + + void _handleContinue() { + // Store bodyweight + ref.read(onboardingDataProvider.notifier).update((state) => { + ...state, + 'bodyweight': _bodyweight, + }); + + context.go('/onboarding/strength-test'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Setup Profile'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/onboarding/welcome'), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Progress Indicator + LinearProgressIndicator( + value: 0.25, + backgroundColor: AppTheme.xpBarBackground, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 32), + + // Title + Text( + 'What\'s your current bodyweight?', + style: Theme.of(context).textTheme.displayMedium, + ), + const SizedBox(height: 16), + Text( + 'We need this to calculate your weighted calisthenics exercises', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 48), + + // Unit Toggle + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SegmentedButton( + segments: const [ + ButtonSegment(value: true, label: Text('KG')), + ButtonSegment(value: false, label: Text('LBS')), + ], + selected: {_useKg}, + onSelectionChanged: (Set newSelection) { + setState(() { + _useKg = newSelection.first; + if (_useKg) { + _bodyweight = _bodyweight / 2.20462; + } else { + _bodyweight = _bodyweight * 2.20462; + } + }); + }, + ), + ], + ), + const SizedBox(height: 32), + + // Bodyweight Display + Center( + child: Column( + children: [ + Text( + _bodyweight.toStringAsFixed(1), + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontSize: 72, + color: AppTheme.primaryColor, + ), + ), + Text( + _useKg ? 'kg' : 'lbs', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + const SizedBox(height: 32), + + // Slider + Slider( + value: _bodyweight, + min: _useKg + ? AppConstants.minBodyweight + : AppConstants.minBodyweight * 2.20462, + max: _useKg + ? AppConstants.maxBodyweight + : AppConstants.maxBodyweight * 2.20462, + divisions: _useKg ? 160 : 352, + activeColor: AppTheme.primaryColor, + onChanged: (value) { + setState(() => _bodyweight = value); + }, + ), + + const Spacer(), + + // Continue Button + ElevatedButton( + onPressed: _handleContinue, + child: const Text('CONTINUE'), + ), + ], + ), + ), + ), + ); + } +} + diff --git a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart new file mode 100644 index 0000000..a6a1065 --- /dev/null +++ b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/app_constants.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../../core/constants/asset_paths.dart'; +import '../../../inventory/presentation/widgets/plate_counter.dart'; +import 'bodyweight_input_screen.dart'; + +class InventorySetupScreen extends ConsumerStatefulWidget { + const InventorySetupScreen({super.key}); + + @override + ConsumerState createState() => + _InventorySetupScreenState(); +} + +class _InventorySetupScreenState extends ConsumerState { + double _barWeight = AppConstants.defaultBarWeight; + Map _plateInventory = { + 25.0: 2, + 20.0: 2, + 15.0: 0, + 10.0: 2, + 5.0: 2, + 2.5: 2, + 1.25: 2, + }; + + // Band selection state - Default configuration based on standard colors + final Map _bandInventory = { + 'Blue': true, + 'Green': true, + 'Orange': false, + 'Red': false, + }; + + bool _isLoading = false; + + void _applyPreset(String preset) { + setState(() { + switch (preset) { + case 'home': + _plateInventory = { + 25.0: 0, + 20.0: 2, + 15.0: 0, + 10.0: 2, + 5.0: 2, + 2.5: 2, + 1.25: 2, + }; + break; + case 'commercial': + _plateInventory = { + 25.0: 4, + 20.0: 4, + 15.0: 2, + 10.0: 4, + 5.0: 4, + 2.5: 4, + 1.25: 2, + }; + break; + case 'minimal': + _plateInventory = { + 25.0: 0, + 20.0: 2, + 15.0: 0, + 10.0: 0, + 5.0: 2, + 2.5: 0, + 1.25: 0, + }; + break; + } + }); + } + + void _handleNext() { + // Listen bauen (wie vorher) + final platesList = []; + _plateInventory.forEach((weight, count) { + for (int i = 0; i < count; i++) platesList.add(weight); + }); + + final bandsList = >[]; + _bandInventory.forEach((color, isSelected) { + if (isSelected) { + bandsList.add({ + 'color': color, + 'resistance_kg': AppConstants.defaultBands[color] ?? 0.0, + 'count': 1, + }); + } + }); + + final inventorySettings = { + 'bar_weight': _barWeight, + 'plates': platesList, + 'bands': bandsList, + }; + + // Im Provider speichern für den nächsten Screen + ref.read(onboardingDataProvider.notifier).update((state) => { + ...state, + 'inventory_settings': inventorySettings, + }); + + context.push('/onboarding/avatar'); // Neue Route! + } + + Future _handleFinish() async { + setState(() => _isLoading = true); + + try { + final onboardingData = ref.read(onboardingDataProvider); + final userRepo = ref.read(userRepositoryProvider); + + // Build plates list for DB + final platesList = []; + _plateInventory.forEach((weight, count) { + for (int i = 0; i < count; i++) { + platesList.add(weight); + } + }); + + // Build bands list for DB + final bandsList = >[]; + _bandInventory.forEach((color, isSelected) { + if (isSelected) { + bandsList.add({ + 'color': color, + 'resistance_kg': AppConstants.defaultBands[color] ?? 0.0, + 'count': 1, + }); + } + }); + + final inventorySettings = { + 'bar_weight': _barWeight, + 'plates': platesList, + 'bands': bandsList, + }; + + // Register user with all data + final user = await userRepo.register( + email: onboardingData['email'] ?? '', + password: onboardingData['password'] ?? '', + bodyweight: onboardingData['bodyweight'] ?? 80.0, + inventorySettings: inventorySettings, + ); + + debugPrint('✅ User registered: ${user.serverId}'); + + // Create first cycle + final trainingMaxes = + onboardingData['training_maxes'] as Map?; + + if (trainingMaxes != null) { + final cycleRepo = ref.read(cycleRepositoryProvider); + final tmMap = { + 'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0, + 'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0, + 'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0, + }; + + debugPrint('📊 Creating cycle with TMs: $tmMap'); + await cycleRepo.createCycle(tmMap); + debugPrint('✅ Cycle created successfully'); + } + + if (mounted) { + // Clear onboarding data + ref.read(onboardingDataProvider.notifier).state = {}; + + // Navigate to hub + context.go('/hub'); + } + } catch (e, stackTrace) { + debugPrint('❌ Setup failed: $e'); + debugPrint('Stack trace: $stackTrace'); + + if (mounted) { + String message = 'Setup failed: ${e.toString()}'; + // Catch unique constraint error (PocketBase returns 400 usually) + if (e.toString().toLowerCase().contains('unique') || + e.toString().toLowerCase().contains('email')) { + message = 'Email already exists. Please login or use another email.'; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: AppTheme.errorColor, + duration: const Duration(seconds: 5), + ), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + Color _getBandColor(String name) { + switch (name) { + case 'Blue': + return Colors.blue; + case 'Green': + return Colors.green; + case 'Orange': + return Colors.orange; + case 'Red': + return Colors.red; + default: + return Colors.grey; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Equipment Setup'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/onboarding/strength-test'), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Progress Indicator + LinearProgressIndicator( + value: 0.75, + backgroundColor: AppTheme.xpBarBackground, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 32), + + // Title + Text( + 'Equipment Inventory', + style: Theme.of(context).textTheme.displayMedium, + ), + const SizedBox(height: 8), + Text( + 'Tell us what equipment you have available', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + + // Bar Weight Selector + Text( + 'Barbell Weight', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary), + ), + const SizedBox(height: 12), + Slider( + value: _barWeight, + min: 10, + max: 25, + divisions: 3, + label: '${_barWeight.toStringAsFixed(0)} kg', + activeColor: AppTheme.primaryColor, + onChanged: (value) { + setState(() => _barWeight = value); + }, + ), + Text( + '${_barWeight.toStringAsFixed(0)} kg', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // Presets + Text( + 'Quick Presets', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _applyPreset('home'), + child: const Text('Home Gym'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () => _applyPreset('commercial'), + child: const Text('Commercial'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () => _applyPreset('minimal'), + child: const Text('Minimal'), + ), + ), + ], + ), + const SizedBox(height: 32), + + // Plate Selection + Text( + 'Available Plates', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary), + ), + const SizedBox(height: 12), + ..._plateInventory.entries.map((entry) { + return PlateCounter( + weight: entry.key, + count: entry.value, + onChanged: (newCount) { + setState(() { + _plateInventory[entry.key] = newCount; + }); + }, + ); + }).toList(), + + const SizedBox(height: 32), + + Text( + 'Resistance Bands (Assistance)', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary), + ), + const SizedBox(height: 8), + Text( + 'Select bands you have for pullup/dip assistance', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: AppTheme.textSecondary), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: _bandInventory.entries.map((entry) { + final resistance = AppConstants.defaultBands[entry.key] ?? 0; + return FilterChip( + label: Text('${entry.key} (~${resistance.toInt()}kg)'), + selected: entry.value, + onSelected: (bool selected) { + setState(() { + _bandInventory[entry.key] = selected; + }); + }, + selectedColor: _getBandColor(entry.key).withOpacity(0.3), + checkmarkColor: _getBandColor(entry.key), + labelStyle: TextStyle( + color: entry.value ? Colors.white : Colors.grey, + ), + side: BorderSide( + color: _getBandColor(entry.key), + ), + ); + }).toList(), + ), + + const SizedBox(height: 32), + + ElevatedButton( + onPressed: _handleNext, + child: const Text('NEXT STEP'), + ), + // // Finish Button + // ElevatedButton( + // onPressed: _isLoading ? null : _handleFinish, + // child: _isLoading + // ? const SizedBox( + // height: 20, + // width: 20, + // child: CircularProgressIndicator( + // strokeWidth: 2, + // color: Colors.black, + // ), + // ) + // : const Text('FINISH SETUP'), + // ), + ], + ), + ), + ), + ); + } +} + +// class _PlateCounter extends StatelessWidget { +// final double weight; +// final int count; +// final ValueChanged onChanged; + +// const _PlateCounter({ +// required this.weight, +// required this.count, +// required this.onChanged, +// }); + +// Color _getPlateColor() { +// final colorValue = PlateColors.colors[weight]; +// return colorValue != null ? Color(colorValue) : Colors.grey; +// } + +// @override +// Widget build(BuildContext context) { +// return Card( +// margin: const EdgeInsets.only(bottom: 8), +// child: Padding( +// padding: const EdgeInsets.all(12), +// child: Row( +// children: [ +// // Plate Visual +// Container( +// width: 48, +// height: 48, +// decoration: BoxDecoration( +// color: _getPlateColor(), +// shape: BoxShape.circle, +// border: Border.all( +// color: Colors.white24, +// width: 2, +// ), +// ), +// child: Center( +// child: Text( +// weight == weight.toInt() +// ? '${weight.toInt()}' +// : weight.toStringAsFixed(2), +// style: const TextStyle( +// color: Colors.white, +// fontWeight: FontWeight.bold, +// fontSize: 12, +// ), +// ), +// ), +// ), +// const SizedBox(width: 16), + +// // Weight Label +// Expanded( +// child: Text( +// '${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg', +// style: Theme.of(context).textTheme.bodyLarge, +// ), +// ), + +// // Counter +// IconButton( +// icon: const Icon(Icons.remove_circle_outline), +// onPressed: count > 0 ? () => onChanged(count - 1) : null, +// color: AppTheme.primaryColor, +// ), +// SizedBox( +// width: 32, +// child: Text( +// count.toString(), +// style: Theme.of(context).textTheme.titleLarge, +// textAlign: TextAlign.center, +// ), +// ), +// IconButton( +// icon: const Icon(Icons.add_circle_outline), +// onPressed: count < 10 ? () => onChanged(count + 1) : null, +// color: AppTheme.primaryColor, +// ), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart new file mode 100644 index 0000000..8e451ca --- /dev/null +++ b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; +import 'bodyweight_input_screen.dart'; + +class StrengthTestScreen extends ConsumerStatefulWidget { + const StrengthTestScreen({super.key}); + + @override + ConsumerState createState() => _StrengthTestScreenState(); +} + +class _StrengthTestScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + // Controllers for each exercise + final _squatWeightController = TextEditingController(text: '100'); + final _squatRepsController = TextEditingController(text: '5'); + final _pullupWeightController = TextEditingController(text: '0'); + final _pullupRepsController = TextEditingController(text: '8'); + final _dipWeightController = TextEditingController(text: '0'); + final _dipRepsController = TextEditingController(text: '10'); + + Map _calculated1RMs = {}; + Map _calculatedTMs = {}; + + @override + void initState() { + super.initState(); + _calculateAll(); + } + + @override + void dispose() { + _squatWeightController.dispose(); + _squatRepsController.dispose(); + _pullupWeightController.dispose(); + _pullupRepsController.dispose(); + _dipWeightController.dispose(); + _dipRepsController.dispose(); + super.dispose(); + } + + void _calculateAll() { + final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0; + + // Squat + final squatWeight = double.tryParse(_squatWeightController.text) ?? 0; + final squatReps = int.tryParse(_squatRepsController.text) ?? 1; + final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps); + final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM); + + // Pullup (bodyweight + additional weight) + final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0; + final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1; + final pullupTotal = bodyweight + pullupAdditional; + final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps); + final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM); + + // Dip (bodyweight + additional weight) + final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0; + final dipReps = int.tryParse(_dipRepsController.text) ?? 1; + final dipTotal = bodyweight + dipAdditional; + final dip1RM = WendlerCalculator.calculate1RM(dipTotal, dipReps); + final dipTM = WendlerCalculator.calculateTrainingMax(dip1RM); + + setState(() { + _calculated1RMs = { + 'squat': squat1RM, + 'pullup': pullup1RM, + 'dip': dip1RM, + }; + _calculatedTMs = { + 'squat': squatTM, + 'pullup': pullupTM, + 'dip': dipTM, + }; + }); + } + + void _handleContinue() { + if (!_formKey.currentState!.validate()) return; + + // Store training maxes + ref.read(onboardingDataProvider.notifier).update((state) => { + ...state, + 'training_maxes': _calculatedTMs, + }); + + context.go('/onboarding/inventory'); + } + + @override + Widget build(BuildContext context) { + final bodyweight = ref.watch(onboardingDataProvider)['bodyweight'] ?? 80.0; + + return Scaffold( + appBar: AppBar( + title: const Text('Strength Test'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/onboarding/bodyweight'), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Progress Indicator + LinearProgressIndicator( + value: 0.5, + backgroundColor: AppTheme.xpBarBackground, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 32), + + // Title + Text( + 'Combat Calibration', // Statt "Strength Assessment" + style: Theme.of(context).textTheme.displayMedium, + ), + const SizedBox(height: 8), + Text( + 'We need to assess your current power level to assign the correct monsters.', // Flavor + style: Theme.of(context).textTheme.bodyMedium, + ), + // Text( + // 'Strength Assessment', + // style: Theme.of(context).textTheme.displayMedium, + // ), + const SizedBox(height: 8), + Text( + 'Enter your recent best performance for each exercise', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + + // Squat + _ExerciseCard( + exerciseName: 'Back Squat', + icon: Icons.accessibility_new, + weightController: _squatWeightController, + repsController: _squatRepsController, + isBodyweight: false, + calculated1RM: _calculated1RMs['squat'] ?? 0, + calculatedTM: _calculatedTMs['squat'] ?? 0, + onChanged: _calculateAll, + ), + const SizedBox(height: 16), + + // Pullup + _ExerciseCard( + exerciseName: 'Weighted Pull-up', + icon: Icons.north, + weightController: _pullupWeightController, + repsController: _pullupRepsController, + isBodyweight: true, + bodyweight: bodyweight, + calculated1RM: _calculated1RMs['pullup'] ?? 0, + calculatedTM: _calculatedTMs['pullup'] ?? 0, + onChanged: _calculateAll, + ), + const SizedBox(height: 16), + + // Dip + _ExerciseCard( + exerciseName: 'Weighted Dip', + icon: Icons.south, + weightController: _dipWeightController, + repsController: _dipRepsController, + isBodyweight: true, + bodyweight: bodyweight, + calculated1RM: _calculated1RMs['dip'] ?? 0, + calculatedTM: _calculatedTMs['dip'] ?? 0, + onChanged: _calculateAll, + ), + const SizedBox(height: 32), + + // Info Box + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.3), + ), + ), + child: Row( + children: [ + const Icon( + Icons.info_outline, + color: AppTheme.primaryColor, + ), + const SizedBox(width: 12), + Expanded( + // child: Text( + // 'Training Max (TM) = 90% of your estimated 1RM. This is what we\'ll use for programming.', + // style: Theme.of(context).textTheme.bodySmall, + // ), + child: Text( + 'Your "Training Max" (TM) is your base combat power. We calculate it as 90% of your max potential to ensure long-term survival.', // Flavor + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: AppTheme.textSecondary), + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + // Continue Button + ElevatedButton( + onPressed: _handleContinue, + child: const Text('CONTINUE'), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _ExerciseCard extends StatelessWidget { + final String exerciseName; + final IconData icon; + final TextEditingController weightController; + final TextEditingController repsController; + final bool isBodyweight; + final double bodyweight; + final double calculated1RM; + final double calculatedTM; + final VoidCallback onChanged; + + const _ExerciseCard({ + required this.exerciseName, + required this.icon, + required this.weightController, + required this.repsController, + this.isBodyweight = false, + this.bodyweight = 0, + required this.calculated1RM, + required this.calculatedTM, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: AppTheme.primaryColor), + ), + const SizedBox(width: 12), + Text( + exerciseName, + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + const SizedBox(height: 16), + + // Input Fields + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + controller: weightController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d+\.?\d{0,2}')), + ], + decoration: InputDecoration( + labelText: isBodyweight + ? 'Additional Weight (kg)' + : 'Weight (kg)', + isDense: true, + ), + onChanged: (_) => onChanged(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + return null; + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: repsController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + decoration: const InputDecoration( + labelText: 'Reps', + isDense: true, + ), + onChanged: (_) => onChanged(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + final reps = int.tryParse(value); + if (reps == null || reps < 1 || reps > 20) { + return '1-20'; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Calculations + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + if (isBodyweight) + _ResultRow( + label: 'Total Weight', + value: + '${(bodyweight + (double.tryParse(weightController.text) ?? 0)).toStringAsFixed(1)} kg', + ), + _ResultRow( + label: 'Estimated 1RM', + value: '${calculated1RM.toStringAsFixed(1)} kg', + ), + _ResultRow( + label: 'Training Max (90%)', + value: '${calculatedTM.toStringAsFixed(1)} kg', + highlight: true, + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _ResultRow extends StatelessWidget { + final String label; + final String value; + final bool highlight; + + const _ResultRow({ + required this.label, + required this.value, + this.highlight = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: highlight ? FontWeight.bold : null, + ), + ), + Text( + value, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: highlight ? AppTheme.primaryColor : null, + fontWeight: highlight ? FontWeight.bold : null, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/features/onboarding/presentation/screens/welcome_screen.dart b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart new file mode 100644 index 0000000..645a9c7 --- /dev/null +++ b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/asset_paths.dart'; + +class WelcomeScreen extends StatelessWidget { + const WelcomeScreen({super.key}); + + // @override + // Widget build(BuildContext context) { + // return Scaffold( + // body: SafeArea( + // child: Padding( + // padding: const EdgeInsets.all(24), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // const Spacer(), + + // // Logo + // Container( + // width: 120, + // height: 120, + // decoration: BoxDecoration( + // color: AppTheme.primaryColor, + // borderRadius: BorderRadius.circular(24), + // ), + // child: const Icon( + // Icons.fitness_center, + // size: 64, + // color: Colors.black, + // ), + // ), + // const SizedBox(height: 32), + + // // Title + // Text( + // 'WELCOME TO', + // style: Theme.of(context).textTheme.headlineMedium, + // textAlign: TextAlign.center, + // ), + // Text( + // 'SLRPG', + // style: Theme.of(context).textTheme.displayLarge, + // textAlign: TextAlign.center, + // ), + // const SizedBox(height: 16), + + // // Description + // Text( + // 'Transform your training into an epic RPG adventure', + // style: Theme.of(context).textTheme.bodyLarge, + // textAlign: TextAlign.center, + // ), + // const SizedBox(height: 48), + + // // Features + // _FeatureItem( + // icon: Icons.trending_up, + // title: 'Progressive Overload', + // description: 'Wendler 5/3/1 periodization', + // ), + // const SizedBox(height: 16), + // _FeatureItem( + // icon: Icons.videogame_asset, + // title: 'Gamified Training', + // description: 'Level up, earn XP, unlock achievements', + // ), + // const SizedBox(height: 16), + // _FeatureItem( + // icon: Icons.offline_bolt, + // title: 'Offline First', + // description: 'Train anywhere, sync when ready', + // ), + + // const Spacer(), + + // // Continue Button + // ElevatedButton( + // onPressed: () => context.go('/onboarding/bodyweight'), + // child: const Text('GET STARTED'), + // ), + // const SizedBox(height: 16), + + // // Skip to Login + // TextButton( + // onPressed: () => context.go('/login'), + // child: const Text('Already have an account? Login'), + // ), + // ], + // ), + // ), + // ), + // ); + // } + // // ... imports + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // 1. Hintergrund (Street Park) + Positioned.fill( + child: Image.asset( + AssetPaths.bgStreetParkDay, + fit: BoxFit.cover, + ), + ), + // 2. Overlay (Dunkel für Lesbarkeit) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.7), + ), + ), + + // 3. Inhalt + SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Spacer(), + + // Logo (Optional: Kann bleiben oder weg) + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.5), + blurRadius: 20) + ], + ), + child: const Icon(Icons.fitness_center, + size: 56, color: Colors.black), + ), + const SizedBox(height: 32), + + // RPG Title + Text( + 'ENTER THE ARENA', // Statt "WELCOME TO" + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.white70, + letterSpacing: 2, + ), + textAlign: TextAlign.center, + ), + Text( + 'S.L.R.P.G.', + style: Theme.of(context).textTheme.displayLarge?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + shadows: [ + const Shadow(color: Colors.black, blurRadius: 10) + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // RPG Description + const Text( + 'The Iron Golems have awakened. The Gravity Demons are pulling the world into the abyss.\n\n' + 'Only a true Streetlifter can stop them. Are you ready to forge your body into a weapon?', + style: TextStyle( + fontSize: 16, height: 1.5, color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Features (Umformuliert) + _FeatureItem( + icon: Icons.shield, // Statt trending_up + title: 'Build Your Armor', + description: 'Progressive overload based on Wendler 5/3/1.', + ), + const SizedBox(height: 16), + _FeatureItem( + icon: Icons.videogame_asset, + // icon: Icons + // .swords, // Statt videogame_asset (wenn Icon verfügbar, sonst Flash/Star) + title: 'Slay Monsters', + description: + 'Turn every rep into damage against epic foes.', + ), + const SizedBox(height: 16), + _FeatureItem( + icon: Icons.inventory_2, // Statt offline_bolt + title: 'Gather Loot', + description: 'Earn XP, level up, and unlock new gear.', + ), + + const Spacer(), + + // Button + ElevatedButton( + onPressed: () => context.go('/onboarding/bodyweight'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + padding: const EdgeInsets.symmetric(vertical: 20), + ), + child: const Text('BEGIN YOUR JOURNEY', + style: TextStyle( + fontWeight: FontWeight.bold, letterSpacing: 1)), + ), + const SizedBox(height: 16), + + TextButton( + onPressed: () => context.go('/login'), + child: const Text('Already a hero? Login here', + style: TextStyle(color: Colors.white54)), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _FeatureItem extends StatelessWidget { + final IconData icon; + final String title; + final String description; + + const _FeatureItem({ + required this.icon, + required this.title, + required this.description, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: AppTheme.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + description, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/features/stats/domain/entities/stats_data_point.dart b/lib/src/features/stats/domain/entities/stats_data_point.dart new file mode 100644 index 0000000..9db0626 --- /dev/null +++ b/lib/src/features/stats/domain/entities/stats_data_point.dart @@ -0,0 +1,22 @@ +class StatsDataPoint { + final DateTime date; + final double trainingMax; + final double estimated1RM; + final double totalVolume; + + StatsDataPoint({ + required this.date, + required this.trainingMax, + required this.estimated1RM, + required this.totalVolume, + }); + + factory StatsDataPoint.fromJson(Map json) { + return StatsDataPoint( + date: DateTime.parse(json['date']), + trainingMax: (json['training_max'] as num).toDouble(), + estimated1RM: (json['estimated_1rm'] as num).toDouble(), + totalVolume: (json['total_volume'] as num).toDouble(), + ); + } +} diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart new file mode 100644 index 0000000..96736dc --- /dev/null +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -0,0 +1,590 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../../shared/data/local/collections/cycle_collection.dart'; +import '../../../../shared/data/remote/api_client.dart'; // Zugriff auf API +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/workout_repository.dart'; +import '../../../../shared/domain/entities/exercise.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; +import '../../domain/entities/stats_data_point.dart'; +import '../widgets/progress_chart.dart'; + +class StatsScreen extends ConsumerStatefulWidget { + const StatsScreen({super.key}); + + @override + ConsumerState createState() => _StatsScreenState(); +} + +class _StatsScreenState extends ConsumerState { + bool _isLoading = false; + + String _selectedExercise = 'squat'; // squat, pullup, dip + String _selectedRange = '3m'; // 1m, 3m, 1y, all + // Daten + List _chartData = []; + bool _isChartLoading = true; + + @override + void initState() { + super.initState(); + _loadStats(); + } + + Future _loadStats() async { + setState(() => _isChartLoading = true); + + try { + final workoutRepo = ref.read(workoutRepositoryProvider); + final userRepo = ref.read(userRepositoryProvider); + + final user = await userRepo.getLocalUser(); + if (user == null) { + setState(() => _isChartLoading = false); + return; + } + final userId = user.serverId ?? user.id.toString(); + // 1. Alle abgeschlossenen Workouts laden (Lokal aus Isar) + final allWorkouts = await workoutRepo.getCompletedWorkouts(userId); + + final points = []; + + for (var workout in allWorkouts) { + if (workout.completedAt == null) continue; + + // 2. Exercises parsen + List exercisesJson = []; + try { + exercisesJson = jsonDecode(workout.exercisesJson); + } catch (e) { + continue; + } + + double max1RM = 0.0; + double sessionVolume = 0.0; + bool foundExercise = false; + double trainingMax = + 0.0; // Versuchen wir aus den Daten zu raten oder nehmen 0 + + // 3. Durch Übungen iterieren + for (var exJson in exercisesJson) { + final exercise = Exercise.fromJson(exJson); + + // Nur die ausgewählte Übung betrachten + if (exercise.exerciseId == _selectedExercise) { + foundExercise = true; + + for (var set in exercise.sets) { + if (!set.completed || set.repsActual <= 0) continue; + + // Volumen summieren + sessionVolume += set.targetWeightTotal * set.repsActual; + + // 1RM berechnen (Epley Formel) + // Wir nutzen den WendlerCalculator, der die Logik schon hat + final e1rm = WendlerCalculator.calculate1RM( + set.targetWeightTotal, set.repsActual); + + // Wir nehmen das beste Set des Tages als Wert für den Graphen + if (e1rm > max1RM) { + max1RM = e1rm; + } + + // Versuchen, das TM aus dem Prozentsatz rückzurechnen (optional) + // TM = Weight / Percentage. + if (set.targetPercentage > 0 && trainingMax == 0) { + trainingMax = + set.targetWeightTotal / (set.targetPercentage / 100.0); + } + } + } + } + + // 4. Datenpunkt erstellen, wenn Übung in diesem Workout vorkam + if (foundExercise && max1RM > 0) { + points.add(StatsDataPoint( + date: workout.completedAt!, + trainingMax: + trainingMax, // Ist ggf. ungenau durch Rückrechnung, aber für Graph ok + estimated1RM: max1RM, + totalVolume: sessionVolume, + )); + } + } + + // 5. Sortieren & Filtern (Zeitraum) + points.sort((a, b) => a.date.compareTo(b.date)); + + // Filter nach Datum (Range) + final now = DateTime.now(); + final filteredPoints = points.where((p) { + if (_selectedRange == '1m') { + return p.date.isAfter(now.subtract(const Duration(days: 30))); + } else if (_selectedRange == '3m') { + return p.date.isAfter(now.subtract(const Duration(days: 90))); + } else if (_selectedRange == '1y') { + return p.date.isAfter(now.subtract(const Duration(days: 365))); + } + return true; // 'all' + }).toList(); + + if (mounted) { + setState(() { + _chartData = filteredPoints; + _isChartLoading = false; + }); + } + } catch (e) { + debugPrint('Failed to calculate local stats: $e'); + if (mounted) { + setState(() { + _chartData = []; + _isChartLoading = false; + }); + } + } + } + // Future _loadStats() async { + // setState(() => _isChartLoading = true); + // try { + // final apiClient = ref + // .read(apiClientProvider); // Braucht Provider in user_repository.dart + + // // Hier rufen wir die echte API auf + // // Hinweis: Wenn Offline, müssten wir hier lokal aus Isar aggregieren. + // // Für MVP nutzen wir den API Endpoint wie im TDD spezifiziert. + // try { + // final response = await apiClient.getStatsHistory( + // exercise: _selectedExercise, + // range: _selectedRange, + // ); + + // final pointsJson = response['data_points'] as List? ?? []; + // final points = + // pointsJson.map((json) => StatsDataPoint.fromJson(json)).toList(); + + // if (mounted) { + // setState(() { + // _chartData = points; + // _isChartLoading = false; + // }); + // } + // } catch (e) { + // // Fallback/Error Handling (z.B. Offline) + // debugPrint('Failed to load stats: $e'); + // if (mounted) { + // setState(() { + // _chartData = []; // Leer anzeigen + // _isChartLoading = false; + // }); + // } + // } + // } catch (e) { + // // ... + // } + // } + + void _onFilterChanged(String exercise, String range) { + setState(() { + _selectedExercise = exercise; + _selectedRange = range; + }); + _loadStats(); + } + + Future _handleFinishCycle(CycleCollection currentCycle) async { + setState(() => _isLoading = true); + try { + final cycleRepo = ref.read(cycleRepositoryProvider); + + // 1. Alte TMs merken für den Vergleich + final oldTMs = + jsonDecode(currentCycle.trainingMaxesJson) as Map; + + // 2. Zyklus abschließen (Stall Handling Logic läuft hier) + final newCycle = await cycleRepo.finishCycle(); + final newTMs = + jsonDecode(newCycle.trainingMaxesJson) as Map; + + if (mounted) { + // 3. Ergebnis anzeigen + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _CycleFinishDialog( + oldTMs: oldTMs, + newTMs: newTMs, + newCycleNumber: newCycle.cycleNumber, + ), + ); + + // UI aktualisieren + setState(() {}); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error finishing cycle: $e'), + backgroundColor: AppTheme.errorColor), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + final cycleRepo = ref.watch(cycleRepositoryProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Statistics & Cycles'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/hub'), + ), + ), + body: FutureBuilder( + future: cycleRepo.getCurrentCycle(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + final currentCycle = snapshot.data; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Cycle Card (bleibt wie vorher) + if (currentCycle != null) ...[ + _CurrentCycleCard( + cycle: currentCycle, + onFinish: _isLoading + ? null + : () => _handleFinishCycle(currentCycle), + ), + const SizedBox(height: 24), + ], + + Text( + 'Progress Analysis', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textPrimary), + ), + const SizedBox(height: 16), + + // --- FILTER CHIPS --- + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _FilterChip( + label: 'Squat', + isSelected: _selectedExercise == 'squat', + onTap: () => _onFilterChanged('squat', _selectedRange), + ), + const SizedBox(width: 8), + _FilterChip( + label: 'Pull-up', + isSelected: _selectedExercise == 'pullup', + onTap: () => _onFilterChanged('pullup', _selectedRange), + ), + const SizedBox(width: 8), + _FilterChip( + label: 'Dip', + isSelected: _selectedExercise == 'dip', + onTap: () => _onFilterChanged('dip', _selectedRange), + ), + ], + ), + ), + const SizedBox(height: 16), + + // --- CHART --- + _isChartLoading + ? const SizedBox( + height: 250, + child: Center(child: CircularProgressIndicator())) + : ProgressChart(data: _chartData), + + // (Optional: Range Selector unten drunter '1M', '3M', '1Y'...) + ], + ), + // return SingleChildScrollView( + // padding: const EdgeInsets.all(16), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // if (currentCycle != null) ...[ + // _CurrentCycleCard( + // cycle: currentCycle, + // onFinish: _isLoading + // ? null + // : () => _handleFinishCycle(currentCycle), + // ), + // const SizedBox(height: 24), + // ] else + // const Card( + // child: Padding( + // padding: EdgeInsets.all(16), + // child: Text('No active cycle found.'), + // ), + // ), + + // // Platzhalter für zukünftige Graphen + // Text( + // 'Progress Charts', + // style: Theme.of(context).textTheme.titleLarge, + // ), + // const SizedBox(height: 8), + // Container( + // height: 200, + // decoration: BoxDecoration( + // color: AppTheme.surfaceColor, + // borderRadius: BorderRadius.circular(12), + // border: Border.all(color: Colors.white10), + // ), + // child: Center( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Icon(Icons.bar_chart, + // size: 48, + // color: AppTheme.primaryColor.withOpacity(0.5)), + // const SizedBox(height: 8), + // Text( + // 'Coming Soon', + // style: Theme.of(context).textTheme.bodyMedium, + // ), + // ], + // ), + // ), + // ), + // ], + // ), + ); + }, + ), + ); + } +} + +class _CurrentCycleCard extends StatelessWidget { + final CycleCollection cycle; + final VoidCallback? onFinish; + + const _CurrentCycleCard({required this.cycle, required this.onFinish}); + + @override + Widget build(BuildContext context) { + final tms = jsonDecode(cycle.trainingMaxesJson) as Map; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'CYCLE ${cycle.cycleNumber}', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + fontSize: 24, + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppTheme.successColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'ACTIVE', + style: TextStyle( + color: AppTheme.successColor, + fontWeight: FontWeight.bold, + fontSize: 12), + ), + ), + ], + ), + const Divider(height: 32), + Text('Current Training Maxes (TM)', + style: Theme.of(context).textTheme.labelLarge), + const SizedBox(height: 16), + _StatRow(label: 'Squat', value: '${tms['squat']} kg'), + _StatRow(label: 'Pull-up', value: '${tms['pullup']} kg'), + _StatRow(label: 'Dip', value: '${tms['dip']} kg'), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onFinish, + icon: const Icon(Icons.upgrade), + label: const Text('FINISH CYCLE & LEVEL UP'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.secondaryColor, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _StatRow extends StatelessWidget { + final String label; + final String value; + + const _StatRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: Theme.of(context).textTheme.bodyLarge), + Text(value, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, color: AppTheme.textSecondary)), + ], + ), + ); + } +} + +class _CycleFinishDialog extends StatelessWidget { + final Map oldTMs; + final Map newTMs; + final int newCycleNumber; + + const _CycleFinishDialog({ + required this.oldTMs, + required this.newTMs, + required this.newCycleNumber, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + // title: Text('Cycle $newCycleNumber Started!'), + title: const Text('Dungeon Cleared!'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'You have defeated the guardians of this cycle. But deeper in the dungeon, stronger foes await...'), // Story + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + const Text('Your Training Maxes have increased:', + style: TextStyle(fontWeight: FontWeight.bold)), + // const Text( + // 'Based on your performance in Week 3, your Training Maxes have been updated:'), + const SizedBox(height: 16), + _DiffRow( + name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']), + _DiffRow( + name: 'Pull-up', + oldVal: oldTMs['pullup'], + newVal: newTMs['pullup']), + _DiffRow(name: 'Dip', oldVal: oldTMs['dip'], newVal: newTMs['dip']), + ], + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('ENTER NEXT LEVEL'), + // child: const Text('LET\'S GO!'), + ), + ], + ); + } +} + +class _DiffRow extends StatelessWidget { + final String name; + final double oldVal; + final double newVal; + + const _DiffRow( + {required this.name, required this.oldVal, required this.newVal}); + + @override + Widget build(BuildContext context) { + final diff = newVal - oldVal; + final isPositive = diff > 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded(child: Text(name)), + Text('${oldVal.toStringAsFixed(1)} → ', + style: const TextStyle(color: Colors.grey)), + Text( + newVal.toStringAsFixed(1), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + if (isPositive) + Text('+${diff.toStringAsFixed(1)}', + style: const TextStyle( + color: AppTheme.successColor, fontWeight: FontWeight.bold)) + else + const Text('STALLED', + style: TextStyle(color: AppTheme.secondaryColor, fontSize: 12)), + ], + ), + ); + } +} + +class _FilterChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _FilterChip( + {required this.label, required this.isSelected, required this.onTap}); + + @override + Widget build(BuildContext context) { + return ChoiceChip( + label: Text(label), + selected: isSelected, + onSelected: (_) => onTap(), + selectedColor: AppTheme.primaryColor.withOpacity(0.2), + labelStyle: TextStyle( + color: isSelected ? AppTheme.primaryColor : Colors.grey, + fontWeight: FontWeight.bold, + ), + side: BorderSide( + color: + isSelected ? AppTheme.primaryColor : Colors.grey.withOpacity(0.3), + ), + ); + } +} diff --git a/lib/src/features/stats/presentation/widgets/progress_chart.dart b/lib/src/features/stats/presentation/widgets/progress_chart.dart new file mode 100644 index 0000000..a1cb2ff --- /dev/null +++ b/lib/src/features/stats/presentation/widgets/progress_chart.dart @@ -0,0 +1,357 @@ +// import 'package:fl_chart/fl_chart.dart'; +// import 'package:flutter/material.dart'; +// import 'package:intl/intl.dart'; +// import '../../../../core/theme/app_theme.dart'; +// import '../../domain/entities/stats_data_point.dart'; + +// class ProgressChart extends StatelessWidget { +// final List data; +// final bool isEmpty; + +// const ProgressChart({ +// super.key, +// required this.data, +// }) : isEmpty = data.isEmpty; + +// @override +// Widget build(BuildContext context) { +// if (isEmpty) { +// return Container( +// height: 250, +// decoration: BoxDecoration( +// color: AppTheme.surfaceColor, +// borderRadius: BorderRadius.circular(16), +// ), +// child: Center( +// child: Text( +// 'No data available yet', +// style: TextStyle(color: AppTheme.textSecondary), +// ), +// ), +// ); +// } + +// // Daten sortieren +// final points = List.from(data) +// ..sort((a, b) => a.date.compareTo(b.date)); + +// // Min/Max für Y-Achse berechnen (mit etwas Puffer) +// double maxY = points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b); +// double minY = points.map((e) => e.estimated1RM).reduce((a, b) => a < b ? a : b); + +// // Puffer hinzufügen (z.B. +/- 5kg), damit die Linie nicht am Rand klebt +// maxY += 5; +// minY = (minY - 5).clamp(0, double.infinity); + +// return Container( +// height: 250, +// padding: const EdgeInsets.fromLTRB(16, 24, 16, 0), +// decoration: BoxDecoration( +// color: AppTheme.surfaceColor, +// borderRadius: BorderRadius.circular(16), +// border: Border.all(color: AppTheme.primaryColor.withOpacity(0.1)), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Text( +// 'Estimated 1RM Progress', +// style: Theme.of(context).textTheme.titleSmall?.copyWith( +// color: AppTheme.textSecondary, +// ), +// textAlign: TextAlign.center, +// ), +// const SizedBox(height: 24), +// Expanded( +// child: LineChart( +// LineChartData( +// gridData: FlGridData( +// show: true, +// drawVerticalLine: false, +// horizontalInterval: 5, +// getDrawingHorizontalLine: (value) => FlLine( +// color: Colors.white10, +// strokeWidth: 1, +// ), +// ), +// titlesData: FlTitlesData( +// show: true, +// topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), +// rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), +// bottomTitles: AxisTitles( +// sideTitles: SideTitles( +// showTitles: true, +// reservedSize: 30, +// interval: (points.length / 3).ceil().toDouble(), // Zeige nicht jedes Datum +// getTitlesWidget: (value, meta) { +// final index = value.toInt(); +// if (index >= 0 && index < points.length) { +// return Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: Text( +// DateFormat.Md().format(points[index].date), +// style: const TextStyle( +// color: Colors.grey, +// fontSize: 10, +// ), +// ), +// ); +// } +// return const Text(''); +// }, +// ), +// ), +// leftTitles: AxisTitles( +// sideTitles: SideTitles( +// showTitles: true, +// reservedSize: 40, +// interval: 5, // Alle 5kg eine Beschriftung +// getTitlesWidget: (value, meta) { +// return Text( +// value.toInt().toString(), +// style: const TextStyle( +// color: Colors.grey, +// fontSize: 10, +// ), +// ); +// }, +// ), +// ), +// ), +// borderData: FlBorderData(show: false), +// minX: 0, +// maxX: (points.length - 1).toDouble(), +// minY: minY, +// maxY: maxY, +// lineBarsData: [ +// LineChartBarData( +// spots: points.asMap().entries.map((e) { +// return FlSpot(e.key.toDouble(), e.value.estimated1RM); +// }).toList(), +// isCurved: true, // Kurve glätten +// color: AppTheme.primaryColor, +// barWidth: 3, +// isStrokeCapRound: true, +// dotData: FlDotData( +// show: true, +// getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( +// radius: 4, +// color: AppTheme.backgroundColor, +// strokeWidth: 2, +// strokeColor: AppTheme.primaryColor, +// ), +// ), +// belowBarData: BarAreaData( +// show: true, +// color: AppTheme.primaryColor.withOpacity(0.1), +// ), +// ), +// ], +// lineTouchData: LineTouchData( +// touchTooltipData: LineTouchTooltipData( +// getTooltipColor: (touchedSpot) => AppTheme.surfaceColor, +// getTooltipItems: (touchedSpots) { +// return touchedSpots.map((spot) { +// final date = points[spot.x.toInt()].date; +// return LineTooltipItem( +// '${spot.y.toStringAsFixed(1)} kg\n${DateFormat.yMMMd().format(date)}', +// const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), +// ); +// }).toList(); +// }, +// ), +// ), +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../domain/entities/stats_data_point.dart'; + +class ProgressChart extends StatelessWidget { + final List data; + + // FIX 1: 'const' Konstruktor erlaubt, da wir keine Berechnung mehr hier machen + const ProgressChart({ + super.key, + required this.data, + }); + + // FIX 1: isEmpty als Getter (wird bei Zugriff berechnet) + bool get isEmpty => data.isEmpty; + + @override + Widget build(BuildContext context) { + if (isEmpty) { + return Container( + height: 250, + decoration: BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Text( + 'No data available yet', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + // Kleine Anpassung für Typ-Sicherheit + color: AppTheme.textSecondary, + ), + ), + ), + ); + } + + // Daten sortieren + final points = List.from(data) + ..sort((a, b) => a.date.compareTo(b.date)); + + // Min/Max für Y-Achse berechnen (mit etwas Puffer) + double maxY = + points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b); + double minY = + points.map((e) => e.estimated1RM).reduce((a, b) => a < b ? a : b); + + // Puffer hinzufügen (z.B. +/- 5kg), damit die Linie nicht am Rand klebt + maxY += 5; + minY = (minY - 5).clamp(0, double.infinity); + + return Container( + height: 250, + padding: const EdgeInsets.fromLTRB(16, 24, 16, 0), + decoration: BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppTheme.primaryColor.withOpacity(0.1)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Estimated 1RM Progress', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: AppTheme.textSecondary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Expanded( + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 5, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.white10, + strokeWidth: 1, + ), + ), + titlesData: FlTitlesData( + show: true, + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: (points.length / 3) + .ceil() + .toDouble(), // Zeige nicht jedes Datum + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index >= 0 && index < points.length) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + DateFormat.Md().format(points[index].date), + style: const TextStyle( + color: Colors.grey, + fontSize: 10, + ), + ), + ); + } + return const Text(''); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + interval: 5, // Alle 5kg eine Beschriftung + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 10, + ), + ); + }, + ), + ), + ), + borderData: FlBorderData(show: false), + minX: 0, + maxX: (points.length - 1).toDouble(), + minY: minY, + maxY: maxY, + lineBarsData: [ + LineChartBarData( + spots: points.asMap().entries.map((e) { + return FlSpot(e.key.toDouble(), e.value.estimated1RM); + }).toList(), + isCurved: true, // Kurve glätten + color: AppTheme.primaryColor, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( + radius: 4, + color: AppTheme.backgroundColor, + strokeWidth: 2, + strokeColor: AppTheme.primaryColor, + ), + ), + belowBarData: BarAreaData( + show: true, + color: AppTheme.primaryColor.withOpacity(0.1), + ), + ), + ], + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + // FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor) + tooltipBgColor: AppTheme.surfaceColor, + getTooltipItems: (touchedSpots) { + return touchedSpots.map((spot) { + final date = points[spot.x.toInt()].date; + return LineTooltipItem( + '${spot.y.toStringAsFixed(1)} kg\n${DateFormat.yMMMd().format(date)}', + const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), + ); + }).toList(); + }, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart new file mode 100644 index 0000000..d42c8ec --- /dev/null +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -0,0 +1,994 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:async'; +import 'dart:convert'; + +import '../../../../core/constants/asset_paths.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/domain/entities/exercise.dart'; +import '../../../../shared/domain/entities/workout_set.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; +import '../../../../shared/domain/logic/plate_calculator.dart'; +import '../../../../shared/domain/logic/xp_calculator.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../../shared/data/repositories/workout_repository.dart'; +import '../../../../shared/data/remote/sync_service.dart'; +import '../widgets/plate_visualizer.dart'; +import '../widgets/timer_widget.dart'; +import '../widgets/enemy_hp_bar.dart'; + +class BattleScreen extends ConsumerStatefulWidget { + final int week; + final int day; + final int? workoutId; + + const BattleScreen({ + super.key, + required this.week, + required this.day, + this.workoutId, + }); + + @override + ConsumerState createState() => _BattleScreenState(); +} + +class _BattleScreenState extends ConsumerState { + List _exercises = []; + int _currentExerciseIndex = 0; + int _currentSetIndex = 0; + int _repsCompleted = 0; + bool _isLoading = true; + Timer? _restTimer; + int _restSeconds = 0; + bool _isResting = false; + + @override + void initState() { + super.initState(); + _loadWorkout(); + } + + @override + void dispose() { + _restTimer?.cancel(); + super.dispose(); + } + + String _getEnemyAsset(String exerciseId) { + // Mapping basierend auf Übungs-ID + switch (exerciseId) { + case 'squat': + return AssetPaths.enemyIronGolem; + case 'pullup': + return AssetPaths.enemyGravityDemon; + case 'dip': + return AssetPaths.enemyPressurePhantom; + default: + return AssetPaths.enemyIronGolem; // Fallback + } + } + + List> _getExerciseConfig(int day) { + switch (day) { + case 1: + return [ + { + 'id': 'squat', + 'name': 'Back Squat', + 'type': ExerciseType.squat, + 'isMain': true + }, + { + 'id': 'pullup', + 'name': 'Weighted Pull-up', + 'type': ExerciseType.pullup, + 'isMain': false + }, + ]; + case 2: + return [ + { + 'id': 'dip', + 'name': 'Weighted Dip', + 'type': ExerciseType.dip, + 'isMain': true + }, + { + 'id': 'squat', + 'name': 'Back Squat', + 'type': ExerciseType.squat, + 'isMain': false + }, + ]; + case 3: + return [ + { + 'id': 'pullup', + 'name': 'Weighted Pull-up', + 'type': ExerciseType.pullup, + 'isMain': true + }, + { + 'id': 'dip', + 'name': 'Weighted Dip', + 'type': ExerciseType.dip, + 'isMain': false + }, + ]; + default: + return []; + } + } + + Future _loadWorkout() async { + final userRepo = ref.read(userRepositoryProvider); + final cycleRepo = ref.read(cycleRepositoryProvider); + + final user = await userRepo.getLocalUser(); + final cycle = await cycleRepo.getCurrentCycle(); + + if (user == null || cycle == null) { + if (mounted) context.go('/hub'); + return; + } + + final trainingMaxes = cycleRepo.getCurrentTrainingMaxes(); + final exercises = []; + + final exerciseConfigs = _getExerciseConfig(widget.day); + + for (final config in exerciseConfigs) { + final id = config['id'] as String; + final name = config['name'] as String; + final type = config['type'] as ExerciseType; + final isMain = config['isMain'] as bool; + + final tm = trainingMaxes[id] ?? 0.0; + List sets = []; + + if (isMain) { + sets = WendlerCalculator.generateSets( + week: widget.week, + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } else { + if (widget.week != 4) { + sets = WendlerCalculator.generateFSLSets( + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } + } + + if (sets.isNotEmpty) { + exercises.add(Exercise( + exerciseId: id, + exerciseName: isMain ? name : '$name (FSL)', + bodyweightAtSession: user.currentBodyweight, + sets: sets, + )); + } + } + + setState(() { + _exercises = exercises; + _isLoading = false; + + if (exercises.isNotEmpty && exercises.first.sets.isNotEmpty) { + _repsCompleted = exercises.first.sets.first.repsTarget; + } + }); + } + + void _completeSet() { + final currentExercise = _exercises[_currentExerciseIndex]; + final currentSet = currentExercise.sets[_currentSetIndex]; + + final updatedSet = currentSet.copyWith( + repsActual: _repsCompleted, + completed: true, + ); + + final updatedSets = List.from(currentExercise.sets); + updatedSets[_currentSetIndex] = updatedSet; + + final updatedExercise = currentExercise.copyWith(sets: updatedSets); + final updatedExercises = List.from(_exercises); + updatedExercises[_currentExerciseIndex] = updatedExercise; + + int nextRepsTarget = 0; + + if (_currentSetIndex < currentExercise.sets.length - 1) { + nextRepsTarget = currentExercise.sets[_currentSetIndex + 1].repsTarget; + + setState(() { + _exercises = updatedExercises; + _currentSetIndex++; + _repsCompleted = nextRepsTarget; + }); + _startRestTimer(90); + } else if (_currentExerciseIndex < _exercises.length - 1) { + final nextExercise = _exercises[_currentExerciseIndex + 1]; + if (nextExercise.sets.isNotEmpty) { + nextRepsTarget = nextExercise.sets.first.repsTarget; + } + + setState(() { + _exercises = updatedExercises; + _currentExerciseIndex++; + _currentSetIndex = 0; + _repsCompleted = nextRepsTarget; + }); + _startRestTimer(180); + } else { + setState(() { + _exercises = updatedExercises; + }); + _completeWorkout(); + } + } + + void _startRestTimer(int seconds) { + setState(() { + _isResting = true; + _restSeconds = seconds; + }); + + _restTimer?.cancel(); + _restTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_restSeconds > 0) { + setState(() => _restSeconds--); + } else { + timer.cancel(); + setState(() => _isResting = false); + } + }); + } + + void _skipRest() { + _restTimer?.cancel(); + setState(() { + _isResting = false; + _restSeconds = 0; + }); + } + + Future _completeWorkout() async { + final xpEarned = XPCalculator.calculateWorkoutXP(_exercises); + + final userRepo = ref.read(userRepositoryProvider); + await userRepo.updateXP(xpEarned); + + final user = await userRepo.getLocalUser(); + if (user != null) { + final newLevel = XPCalculator.calculateLevelFromXP(user.xp); + if (newLevel > user.level) { + await userRepo.updateLevel(newLevel); + if (mounted) { + _showLevelUpDialog(user.level, newLevel); + } + } + } + + if (widget.workoutId != null) { + final workoutRepo = ref.read(workoutRepositoryProvider); + final cycleRepo = ref.read(cycleRepositoryProvider); + final cycle = await cycleRepo.getCurrentCycle(); + + final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? ''; + + var workout = await workoutRepo.getWorkoutByWeekDay( + cycleId: cycleIdRef, week: widget.week, day: widget.day); + + if (workout != null) { + workout.exercisesJson = + jsonEncode(_exercises.map((e) => e.toJson()).toList()); + await workoutRepo.completeWorkout(workout, xpEarned: xpEarned); + + ref.read(syncServiceProvider).sync(); + } + } + + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('RAID COMPLETE!'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.emoji_events, + size: 64, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 16), + Text( + '+$xpEarned XP', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/hub'); + }, + child: const Text('BACK TO HUB'), + ), + ], + ), + ); + } + } + + void _showLevelUpDialog(int oldLevel, int newLevel) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.primaryColor, + title: const Text( + 'LEVEL UP!', + style: TextStyle(color: Colors.black), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.military_tech, size: 80, color: Colors.black), + const SizedBox(height: 16), + Text( + 'You have grown stronger!', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Colors.black), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'The monsters tremble at your new power.', // Story Flavor + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black54, fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ), + Text( + '$oldLevel → $newLevel', + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: + const Text('CONTINUE', style: TextStyle(color: Colors.black)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + if (_exercises.isEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Battle')), + body: const Center(child: Text('No exercises configured')), + ); + } + + final currentExercise = _exercises[_currentExerciseIndex]; + final currentSet = currentExercise.sets[_currentSetIndex]; + final userRepo = ref.watch(userRepositoryProvider); + + final totalHP = _exercises.fold( + 0, + (sum, ex) => sum + ex.sets.fold(0, (s, set) => s + set.repsTarget), + ); + + final completedHP = _exercises.take(_currentExerciseIndex).fold( + 0, + (sum, ex) => + sum + ex.sets.fold(0, (s, set) => s + set.repsActual), + ) + + currentExercise.sets + .take(_currentSetIndex) + .fold(0, (sum, set) => sum + set.repsActual); + + final isBodyweight = currentExercise.exerciseId != 'squat'; + final barWeight = isBodyweight + ? currentExercise.bodyweightAtSession + : userRepo.getBarWeight(); + final availablePlates = userRepo.getAvailablePlates(); + final inventory = userRepo.getInventorySettings(); + final bandsList = + (inventory['bands'] as List?)?.cast>() ?? []; + + final Map availableBands = {}; + for (var band in bandsList) { + final color = band['color'] as String; + final resistance = (band['resistance_kg'] as num).toDouble(); + if (band['count'] as int > 0) { + availableBands[color] = resistance; + } + } + + final plateResult = PlateCalculator.calculate( + targetWeight: currentSet.targetWeightTotal, + barWeight: barWeight, + availablePlates: availablePlates, + availableBands: availableBands, + isTwoSided: !isBodyweight, + ); + + return Scaffold( + appBar: AppBar( + title: Text('Week ${widget.week} - Day ${widget.day}'), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Abandon Raid?'), + content: const Text('Your progress will not be saved.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/hub'); + }, + style: TextButton.styleFrom( + foregroundColor: AppTheme.errorColor), + child: const Text('ABANDON'), + ), + ], + ), + ); + }, + ), + ), + body: Stack( + children: [ + // 1. HINTERGRUND (Underground Gym) + Positioned.fill( + child: Image.asset( + AssetPaths.bgUndergroundGym, + fit: BoxFit.cover, + ), + ), + + // 2. Overlay (Atmosphäre & Lesbarkeit) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.7), // Dunkler Schleier + ), + ), + + // 3. INHALT + SafeArea( + child: _isResting + ? _buildRestScreen() // Rest Screen überdeckt das Gym (oder man macht ihn auch transparent) + : _buildWorkoutScreen(currentExercise, currentSet, plateResult, + completedHP, totalHP), + ), + ], + ), + // body: _isResting + // ? _buildRestScreen() + // : _buildWorkoutScreen( + // currentExercise, currentSet, plateResult, completedHP, totalHP), + ); + } + + Widget _buildRestScreen() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.backgroundColor, + AppTheme.surfaceColor, + ], + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'REST', + style: Theme.of(context).textTheme.displayLarge, + ), + const SizedBox(height: 32), + SizedBox( + width: 200, + height: 200, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 200, + height: 200, + child: CircularProgressIndicator( + value: _restSeconds / 180, + strokeWidth: 12, + backgroundColor: AppTheme.xpBarBackground, + color: AppTheme.primaryColor, + ), + ), + Text( + _formatTime(_restSeconds), + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontSize: 48, + color: AppTheme.primaryColor, + ), + ), + ], + ), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: _skipRest, + child: const Text('SKIP REST'), + ), + ], + ), + ), + ); + } + + Widget _buildWorkoutScreen( + Exercise currentExercise, + WorkoutSet currentSet, + PlateLoadResult plateResult, + int completedHP, + int totalHP, + ) { + // Styles + final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white, + shadows: [ + const Shadow(color: Colors.black, blurRadius: 4, offset: Offset(0, 1)) + ], + ); + + final titleStyle = Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + shadows: [ + const Shadow(color: Colors.black, blurRadius: 8, offset: Offset(0, 2)) + ], + ); + + return Column( + children: [ + // --- 1. GEGNER BEREICH (Immersive) --- + // Wir nutzen Flexible, damit der Gegner Platz einnimmt, aber schrumpft, wenn nötig + Flexible( + flex: 4, // Verhältnis zum unteren Teil + child: Stack( + alignment: Alignment.center, + children: [ + // Gegner Bild (Groß & Frei) + Padding( + padding: const EdgeInsets.only(bottom: 40), // Platz für HP Bar + child: Image.asset( + _getEnemyAsset(currentExercise.exerciseId), + fit: BoxFit.contain, + // Ein Glow-Effekt hinter dem Gegner für bessere Abhebung vom Hintergrund + color: Colors.white.withOpacity(0.9), + colorBlendMode: BlendMode.modulate, + ), + ), + + // Wave Badge (Oben Rechts, dezent) + Positioned( + top: 16, + right: 16, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white24), + ), + child: Text( + 'WAVE ${_currentExerciseIndex + 1} / ${_exercises.length}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12), + ), + ), + ), + + // HP Bar (Direkt unter dem Gegner, Schwebend) + Positioned( + bottom: 10, + left: 32, + right: 32, + child: Column( + children: [ + // Kleines Herz Icon + const Icon(Icons.favorite, + color: AppTheme.errorColor, size: 24), + const SizedBox(height: 4), + EnemyHPBar( + current: totalHP - completedHP, + max: totalHP, + ), + ], + ), + ), + ], + ), + ), + + // --- 2. KONTROLL BEREICH (Scrollable) --- + // Dieser Teil enthält die Trainings-Infos und den Counter + Expanded( + flex: 6, + child: Container( + decoration: BoxDecoration( + color: AppTheme.surfaceColor + .withOpacity(0.95), // Fast undurchsichtig für Lesbarkeit + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, -5)) + ], + ), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + 24, 24, 24, 100), // Unten Platz für Button + child: Column( + children: [ + Text( + currentExercise.exerciseName, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Text( + 'Set ${_currentSetIndex + 1} of ${currentExercise.sets.length}', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Colors.white70), + ), + + const SizedBox(height: 24), + + // Target Info (Kompakt) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _InfoBox( + label: 'WEIGHT', + value: '${currentSet.targetWeightTotal} kg'), + _InfoBox( + label: 'REPS', + value: + '${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}'), + ], + ), + + const SizedBox(height: 24), + + // Load / Assistance Visualizer + if (plateResult.bandAssistance != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.primaryColor), + ), + child: Row( + children: [ + const Icon(Icons.help, + color: AppTheme.primaryColor, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text('ASSISTANCE', + style: TextStyle( + color: AppTheme.primaryColor, + fontSize: 12, + fontWeight: FontWeight.bold)), + Text(plateResult.bandAssistance!, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold)), + ], + ), + ), + ], + ), + ) + else + PlateVisualizer( + plateConfiguration: plateResult.plateConfiguration, + isTwoSided: currentExercise.exerciseId == 'squat', + exerciseName: currentExercise.exerciseName, + ), + + const SizedBox(height: 32), + + // // Counter (Groß) + // Text('REPS COMPLETED', + // style: TextStyle( + // color: Colors.grey, + // fontSize: 12, + // letterSpacing: 1.5, + // fontWeight: FontWeight.bold)), + // const SizedBox(height: 8), + // Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // _CounterButton( + // icon: Icons.remove, + // onTap: _repsCompleted > 0 + // ? () => setState(() => _repsCompleted--) + // : null), + // Container( + // width: 100, + // alignment: Alignment.center, + // child: Text( + // '$_repsCompleted', + // style: const TextStyle( + // fontSize: 64, + // fontWeight: FontWeight.bold, + // color: Colors.white), + // ), + // ), + // _CounterButton( + // icon: Icons.add, + // onTap: () => setState(() => _repsCompleted++)), + // ], + // ), + ], + ), + ), + ), + ], + ), + ), + ), + + // --- 3. FIXIERTER BUTTON --- + Container( + color: + AppTheme.surfaceColor, // Gleiche Farbe wie der Kontroll-Container + padding: const EdgeInsets.all(16), + child: SafeArea( + top: false, + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + // onPressed: _repsCompleted >= currentSet.repsTarget + // ? _completeSet + // : null, + onPressed: () => _handleCompletePress(currentSet), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: const Text('COMPLETE SET', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 1.2)), + ), + ), + ), + ), + ], + ); + } + + void _handleCompletePress(WorkoutSet currentSet) { + if (currentSet.isAmrap) { + _showAmrapDialog(currentSet); + } else { + // Standard-Satz: Wir gehen davon aus, dass das Ziel erreicht wurde + setState(() { + _repsCompleted = currentSet.repsTarget; + }); + _completeSet(); + } + } + + String _formatTime(int seconds) { + final minutes = seconds ~/ 60; + final secs = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + void _showAmrapDialog(WorkoutSet set) { + // Startwert ist das Ziel (oder was bisher eingestellt war) + int tempReps = set.repsTarget; + + showModalBottomSheet( + context: context, + backgroundColor: AppTheme.surfaceColor, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return StatefulBuilder(// Wichtig für State im Dialog + builder: (context, setModalState) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '🔥 AMRAP RESULT 🔥', + style: TextStyle( + color: AppTheme.secondaryColor, + fontWeight: FontWeight.bold, + fontSize: 20), + ), + const SizedBox(height: 8), + const Text( + 'Go all out! How many did you get?', + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 32), + + // Großer Counter + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _CounterButton( + icon: Icons.remove, + onTap: tempReps > 0 + ? () => setModalState(() => tempReps--) + : null), + Container( + width: 120, + alignment: Alignment.center, + child: Text( + '$tempReps', + style: const TextStyle( + fontSize: 72, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + ), + _CounterButton( + icon: Icons.add, + onTap: () => setModalState(() => tempReps++)), + ], + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); // Dialog schließen + // Wert übernehmen und Satz beenden + setState(() { + _repsCompleted = tempReps; + }); + _completeSet(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.secondaryColor, + foregroundColor: Colors.white, + ), + child: const Text('CONFIRM RESULT', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 16), // Puffer für iOS Home Bar + ], + ), + ); + }); + }, + ); + } +} + +class _InfoBox extends StatelessWidget { + final String label; + final String value; + const _InfoBox({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(label, + style: const TextStyle( + color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(value, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold)), + ], + ); + } +} + +// Der Counter Button Helper (kannst du so lassen oder anpassen) +class _CounterButton extends StatelessWidget { + final IconData icon; + final VoidCallback? onTap; + const _CounterButton({required this.icon, this.onTap}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(30), + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: onTap != null + ? AppTheme.primaryColor + : Colors.grey.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, + size: 32, color: onTap != null ? Colors.black : Colors.grey), + ), + ), + ); + } +} diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen_back b/lib/src/features/workout_runner/presentation/screens/battle_screen_back new file mode 100644 index 0000000..25abe80 --- /dev/null +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen_back @@ -0,0 +1,1011 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:async'; +import 'dart:convert'; + +import '../../../../core/constants/asset_paths.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/domain/entities/exercise.dart'; +import '../../../../shared/domain/entities/workout_set.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; +import '../../../../shared/domain/logic/plate_calculator.dart'; +import '../../../../shared/domain/logic/xp_calculator.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../../shared/data/repositories/cycle_repository.dart'; +import '../../../../shared/data/repositories/workout_repository.dart'; +import '../../../../shared/data/remote/sync_service.dart'; +import '../widgets/plate_visualizer.dart'; +import '../widgets/timer_widget.dart'; +import '../widgets/enemy_hp_bar.dart'; + +class BattleScreen extends ConsumerStatefulWidget { + final int week; + final int day; + final int? workoutId; + + const BattleScreen({ + super.key, + required this.week, + required this.day, + this.workoutId, + }); + + @override + ConsumerState createState() => _BattleScreenState(); +} + +class _BattleScreenState extends ConsumerState { + List _exercises = []; + int _currentExerciseIndex = 0; + int _currentSetIndex = 0; + int _repsCompleted = 0; + bool _isLoading = true; + Timer? _restTimer; + int _restSeconds = 0; + bool _isResting = false; + + @override + void initState() { + super.initState(); + _loadWorkout(); + } + + @override + void dispose() { + _restTimer?.cancel(); + super.dispose(); + } + + String _getEnemyAsset(String exerciseId) { + // Mapping basierend auf Übungs-ID + switch (exerciseId) { + case 'squat': + return AssetPaths.enemyIronGolem; + case 'pullup': + return AssetPaths.enemyGravityDemon; + case 'dip': + return AssetPaths.enemyPressurePhantom; + default: + return AssetPaths.enemyIronGolem; // Fallback + } + } + + List> _getExerciseConfig(int day) { + switch (day) { + case 1: + return [ + { + 'id': 'squat', + 'name': 'Back Squat', + 'type': ExerciseType.squat, + 'isMain': true + }, + { + 'id': 'pullup', + 'name': 'Weighted Pull-up', + 'type': ExerciseType.pullup, + 'isMain': false + }, + ]; + case 2: + return [ + { + 'id': 'dip', + 'name': 'Weighted Dip', + 'type': ExerciseType.dip, + 'isMain': true + }, + { + 'id': 'squat', + 'name': 'Back Squat', + 'type': ExerciseType.squat, + 'isMain': false + }, + ]; + case 3: + return [ + { + 'id': 'pullup', + 'name': 'Weighted Pull-up', + 'type': ExerciseType.pullup, + 'isMain': true + }, + { + 'id': 'dip', + 'name': 'Weighted Dip', + 'type': ExerciseType.dip, + 'isMain': false + }, + ]; + default: + return []; + } + } + + Future _loadWorkout() async { + final userRepo = ref.read(userRepositoryProvider); + final cycleRepo = ref.read(cycleRepositoryProvider); + + final user = await userRepo.getLocalUser(); + final cycle = await cycleRepo.getCurrentCycle(); + + if (user == null || cycle == null) { + if (mounted) context.go('/hub'); + return; + } + + final trainingMaxes = cycleRepo.getCurrentTrainingMaxes(); + final exercises = []; + + final exerciseConfigs = _getExerciseConfig(widget.day); + + for (final config in exerciseConfigs) { + final id = config['id'] as String; + final name = config['name'] as String; + final type = config['type'] as ExerciseType; + final isMain = config['isMain'] as bool; + + final tm = trainingMaxes[id] ?? 0.0; + List sets = []; + + if (isMain) { + sets = WendlerCalculator.generateSets( + week: widget.week, + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } else { + if (widget.week != 4) { + sets = WendlerCalculator.generateFSLSets( + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } + } + + if (sets.isNotEmpty) { + exercises.add(Exercise( + exerciseId: id, + exerciseName: isMain ? name : '$name (FSL)', + bodyweightAtSession: user.currentBodyweight, + sets: sets, + )); + } + } + + setState(() { + _exercises = exercises; + _isLoading = false; + + if (exercises.isNotEmpty && exercises.first.sets.isNotEmpty) { + _repsCompleted = exercises.first.sets.first.repsTarget; + } + }); + } + + void _completeSet() { + final currentExercise = _exercises[_currentExerciseIndex]; + final currentSet = currentExercise.sets[_currentSetIndex]; + + final updatedSet = currentSet.copyWith( + repsActual: _repsCompleted, + completed: true, + ); + + final updatedSets = List.from(currentExercise.sets); + updatedSets[_currentSetIndex] = updatedSet; + + final updatedExercise = currentExercise.copyWith(sets: updatedSets); + final updatedExercises = List.from(_exercises); + updatedExercises[_currentExerciseIndex] = updatedExercise; + + int nextRepsTarget = 0; + + if (_currentSetIndex < currentExercise.sets.length - 1) { + nextRepsTarget = currentExercise.sets[_currentSetIndex + 1].repsTarget; + + setState(() { + _exercises = updatedExercises; + _currentSetIndex++; + _repsCompleted = nextRepsTarget; + }); + _startRestTimer(90); + } else if (_currentExerciseIndex < _exercises.length - 1) { + final nextExercise = _exercises[_currentExerciseIndex + 1]; + if (nextExercise.sets.isNotEmpty) { + nextRepsTarget = nextExercise.sets.first.repsTarget; + } + + setState(() { + _exercises = updatedExercises; + _currentExerciseIndex++; + _currentSetIndex = 0; + _repsCompleted = nextRepsTarget; + }); + _startRestTimer(180); + } else { + setState(() { + _exercises = updatedExercises; + }); + _completeWorkout(); + } + } + + void _startRestTimer(int seconds) { + setState(() { + _isResting = true; + _restSeconds = seconds; + }); + + _restTimer?.cancel(); + _restTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_restSeconds > 0) { + setState(() => _restSeconds--); + } else { + timer.cancel(); + setState(() => _isResting = false); + } + }); + } + + void _skipRest() { + _restTimer?.cancel(); + setState(() { + _isResting = false; + _restSeconds = 0; + }); + } + + Future _completeWorkout() async { + final xpEarned = XPCalculator.calculateWorkoutXP(_exercises); + + final userRepo = ref.read(userRepositoryProvider); + await userRepo.updateXP(xpEarned); + + final user = await userRepo.getLocalUser(); + if (user != null) { + final newLevel = XPCalculator.calculateLevelFromXP(user.xp); + if (newLevel > user.level) { + await userRepo.updateLevel(newLevel); + if (mounted) { + _showLevelUpDialog(user.level, newLevel); + } + } + } + + if (widget.workoutId != null) { + final workoutRepo = ref.read(workoutRepositoryProvider); + final cycleRepo = ref.read(cycleRepositoryProvider); + final cycle = await cycleRepo.getCurrentCycle(); + + final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? ''; + + var workout = await workoutRepo.getWorkoutByWeekDay( + cycleId: cycleIdRef, week: widget.week, day: widget.day); + + if (workout != null) { + workout.exercisesJson = + jsonEncode(_exercises.map((e) => e.toJson()).toList()); + await workoutRepo.completeWorkout(workout, xpEarned: xpEarned); + + ref.read(syncServiceProvider).sync(); + } + } + + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('RAID COMPLETE!'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.emoji_events, + size: 64, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 16), + Text( + '+$xpEarned XP', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/hub'); + }, + child: const Text('BACK TO HUB'), + ), + ], + ), + ); + } + } + + void _showLevelUpDialog(int oldLevel, int newLevel) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.primaryColor, + title: const Text( + 'LEVEL UP!', + style: TextStyle(color: Colors.black), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.military_tech, size: 80, color: Colors.black), + const SizedBox(height: 16), + Text( + '$oldLevel → $newLevel', + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: + const Text('CONTINUE', style: TextStyle(color: Colors.black)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + if (_exercises.isEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Battle')), + body: const Center(child: Text('No exercises configured')), + ); + } + + final currentExercise = _exercises[_currentExerciseIndex]; + final currentSet = currentExercise.sets[_currentSetIndex]; + final userRepo = ref.watch(userRepositoryProvider); + + final totalHP = _exercises.fold( + 0, + (sum, ex) => sum + ex.sets.fold(0, (s, set) => s + set.repsTarget), + ); + + final completedHP = _exercises.take(_currentExerciseIndex).fold( + 0, + (sum, ex) => + sum + ex.sets.fold(0, (s, set) => s + set.repsActual), + ) + + currentExercise.sets + .take(_currentSetIndex) + .fold(0, (sum, set) => sum + set.repsActual); + + final isBodyweight = currentExercise.exerciseId != 'squat'; + final barWeight = isBodyweight + ? currentExercise.bodyweightAtSession + : userRepo.getBarWeight(); + final availablePlates = userRepo.getAvailablePlates(); + final inventory = userRepo.getInventorySettings(); + final bandsList = + (inventory['bands'] as List?)?.cast>() ?? []; + + final Map availableBands = {}; + for (var band in bandsList) { + final color = band['color'] as String; + final resistance = (band['resistance_kg'] as num).toDouble(); + if (band['count'] as int > 0) { + availableBands[color] = resistance; + } + } + + final plateResult = PlateCalculator.calculate( + targetWeight: currentSet.targetWeightTotal, + barWeight: barWeight, + availablePlates: availablePlates, + availableBands: availableBands, + isTwoSided: !isBodyweight, + ); + + return Scaffold( + appBar: AppBar( + title: Text('Week ${widget.week} - Day ${widget.day}'), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Abandon Raid?'), + content: const Text('Your progress will not be saved.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.go('/hub'); + }, + style: TextButton.styleFrom( + foregroundColor: AppTheme.errorColor), + child: const Text('ABANDON'), + ), + ], + ), + ); + }, + ), + ), + body: Stack( + children: [ + // 1. HINTERGRUND (Underground Gym) + Positioned.fill( + child: Image.asset( + AssetPaths.bgUndergroundGym, + fit: BoxFit.cover, + ), + ), + + // 2. Overlay (Atmosphäre & Lesbarkeit) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.7), // Dunkler Schleier + ), + ), + + // 3. INHALT + SafeArea( + child: _isResting + ? _buildRestScreen() // Rest Screen überdeckt das Gym (oder man macht ihn auch transparent) + : _buildWorkoutScreen(currentExercise, currentSet, plateResult, + completedHP, totalHP), + ), + ], + ), + // body: _isResting + // ? _buildRestScreen() + // : _buildWorkoutScreen( + // currentExercise, currentSet, plateResult, completedHP, totalHP), + ); + } + + Widget _buildRestScreen() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.backgroundColor, + AppTheme.surfaceColor, + ], + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'REST', + style: Theme.of(context).textTheme.displayLarge, + ), + const SizedBox(height: 32), + SizedBox( + width: 200, + height: 200, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 200, + height: 200, + child: CircularProgressIndicator( + value: _restSeconds / 180, + strokeWidth: 12, + backgroundColor: AppTheme.xpBarBackground, + color: AppTheme.primaryColor, + ), + ), + Text( + _formatTime(_restSeconds), + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontSize: 48, + color: AppTheme.primaryColor, + ), + ), + ], + ), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: _skipRest, + child: const Text('SKIP REST'), + ), + ], + ), + ), + ); + } + + Widget _buildWorkoutScreen( + Exercise currentExercise, + WorkoutSet currentSet, + PlateLoadResult plateResult, + int completedHP, + int totalHP, + ) { + // Gemeinsamer Text-Style für bessere Lesbarkeit auf Hintergründen + final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white, + shadows: [ + const Shadow(color: Colors.black, blurRadius: 4, offset: Offset(0, 1)) + ], + ); + + final titleStyle = Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + shadows: [ + const Shadow(color: Colors.black, blurRadius: 8, offset: Offset(0, 2)) + ], + ); + + return Column( + children: [ + // Info Header (HP & Wave) + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.surfaceColor.withOpacity(0.9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white10), + ), + child: Column( + children: [ + // --- NEU: Enemy Image --- + SizedBox( + height: 120, // Gute Größe für den Header + child: Image.asset( + _getEnemyAsset( + currentExercise.exerciseId), // Wählt das richtige Bild + fit: BoxFit.contain, + // Ein leichter Schatten, damit der Gegner nicht "schwebt" + color: Colors.black.withOpacity(0.2), + colorBlendMode: BlendMode.dstOver, + // Fallback Icon, falls Asset fehlt + errorBuilder: (c, o, s) => const Icon( + Icons.warning_amber_rounded, + size: 48, + color: Colors.white24), + ), + ), + const SizedBox(height: 16), // Abstand zum Text + // ------------------------ + Text( + 'Wave ${_currentExerciseIndex + 1}/${_exercises.length}', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + EnemyHPBar( + current: totalHP - completedHP, + max: totalHP, + ), + ], + ), + ), + + // Scrollbarer Inhalt (nimmt den verfügbaren Platz ein) + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + currentExercise.exerciseName, + style: Theme.of(context).textTheme.displayMedium?.copyWith( + shadows: [ + const Shadow(color: Colors.black, blurRadius: 10) + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Set ${_currentSetIndex + 1}/${currentExercise.sets.length}', + style: titleStyle, // Neuer Style + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Target Card (etwas transparenter für Look) + Card( + color: AppTheme.surfaceColor.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text('TARGET', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: AppTheme.textSecondary)), + const SizedBox(height: 8), + Text( + '${currentSet.targetWeightTotal.toStringAsFixed(1)} kg', + style: Theme.of(context) + .textTheme + .displayMedium + ?.copyWith(color: AppTheme.primaryColor), + ), + Text( + '${currentSet.targetPercentage}% TM × ${currentSet.repsTarget} reps${currentSet.isAmrap ? '+' : ''}', + style: readableStyle, // Neuer Style + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Visualizer oder Band Info + // Wir geben dem Container eine feste Mindesthöhe, damit das Layout weniger springt, + // aber da der Button jetzt fixiert ist, ist das Springen weniger kritisch. + if (plateResult.bandAssistance != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.primaryColor + .withOpacity(0.2), // Heller für Kontrast + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppTheme.primaryColor), + ), + child: Column( + children: [ + const Icon(Icons.help_outline, + size: 48, color: AppTheme.primaryColor), + const SizedBox(height: 8), + Text( + 'ASSISTANCE NEEDED', + style: titleStyle?.copyWith( + color: AppTheme.primaryColor), + ), + const SizedBox(height: 8), + Text( + 'Use ${plateResult.bandAssistance} Band', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Text( + '(approx. -${(plateResult.totalAchieved - currentSet.targetWeightTotal).abs().toStringAsFixed(1)} kg)', + style: readableStyle, // Neuer Style + ), + ], + ), + ) + else + PlateVisualizer( + plateConfiguration: plateResult.plateConfiguration, + isTwoSided: currentExercise.exerciseId == 'squat', + exerciseName: currentExercise.exerciseName, + ), + + const SizedBox(height: 32), + + Text( + 'REPS COMPLETED', + style: titleStyle, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Counter (Hintergrund hinzufügen für Lesbarkeit) + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(30), + ), + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove_circle), + iconSize: 48, + color: AppTheme.primaryColor, + onPressed: _repsCompleted > 0 + ? () => setState(() => _repsCompleted--) + : null, + ), + const SizedBox(width: 24), + SizedBox( + width: 80, + child: Text( + _repsCompleted.toString(), + style: Theme.of(context) + .textTheme + .displayLarge + ?.copyWith(fontSize: 56, color: Colors.white), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 24), + IconButton( + icon: const Icon(Icons.add_circle), + iconSize: 48, + color: AppTheme.primaryColor, + onPressed: () => setState(() => _repsCompleted++), + ), + ], + ), + ), + + if (currentSet.isAmrap) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Text( + '🔥 AMRAP - Go for max reps! 🔥', + style: readableStyle?.copyWith( + color: AppTheme.secondaryColor, + fontWeight: FontWeight.bold, + fontSize: 18), + textAlign: TextAlign.center, + ), + ), + + // Platzhalter am Ende, damit man nicht "hinter" den fixierten Button scrollen muss + const SizedBox(height: 100), + ], + ), + ), + ), + + // --- FIXIERTER BUTTON BEREICH --- + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.surfaceColor, // Solider Hintergrund + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SafeArea( + top: false, + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _repsCompleted >= currentSet.repsTarget + ? _completeSet + : null, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.black, + ), + child: const Text( + 'COMPLETE SET', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 1.5), + ), + ), + ), + ), + ), + ], + ); + } + // Widget _buildWorkoutScreen( + // Exercise currentExercise, + // WorkoutSet currentSet, + // PlateLoadResult plateResult, + // int completedHP, + // int totalHP, + // ) { + // return Column( + // children: [ + // Container( + // padding: const EdgeInsets.all(16), + // color: AppTheme.surfaceColor, + // child: Column( + // children: [ + // Text( + // 'Wave ${_currentExerciseIndex + 1}/${_exercises.length}', + // style: Theme.of(context).textTheme.titleMedium, + // ), + // const SizedBox(height: 8), + // EnemyHPBar( + // current: totalHP - completedHP, + // max: totalHP, + // ), + // ], + // ), + // ), + // Expanded( + // child: SingleChildScrollView( + // padding: const EdgeInsets.all(24), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // Text( + // currentExercise.exerciseName, + // style: Theme.of(context).textTheme.displayMedium, + // textAlign: TextAlign.center, + // ), + // const SizedBox(height: 8), + // Text( + // 'Set ${_currentSetIndex + 1}/${currentExercise.sets.length}', + // style: Theme.of(context).textTheme.titleLarge, + // textAlign: TextAlign.center, + // ), + // const SizedBox(height: 24), + // Card( + // child: Padding( + // padding: const EdgeInsets.all(16), + // child: Column( + // children: [ + // Text('Target', + // style: Theme.of(context).textTheme.bodyMedium), + // const SizedBox(height: 8), + // Text( + // '${currentSet.targetWeightTotal.toStringAsFixed(1)} kg', + // style: Theme.of(context) + // .textTheme + // .displayMedium + // ?.copyWith(color: AppTheme.primaryColor), + // ), + // Text( + // '${currentSet.targetPercentage}% TM × ${currentSet.repsTarget} reps${currentSet.isAmrap ? '+' : ''}', + // style: Theme.of(context).textTheme.bodyMedium, + // ), + // ], + // ), + // ), + // ), + // const SizedBox(height: 24), + // if (plateResult.bandAssistance != null) + // Container( + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: AppTheme.primaryColor.withOpacity(0.1), + // borderRadius: BorderRadius.circular(16), + // border: Border.all(color: AppTheme.primaryColor), + // ), + // child: Column( + // children: [ + // const Icon(Icons.help_outline, + // size: 48, color: AppTheme.primaryColor), + // const SizedBox(height: 8), + // Text( + // 'ASSISTANCE NEEDED', + // style: + // Theme.of(context).textTheme.titleMedium?.copyWith( + // color: AppTheme.primaryColor, + // fontWeight: FontWeight.bold, + // ), + // ), + // const SizedBox(height: 8), + // Text( + // 'Use ${plateResult.bandAssistance} Band', + // style: Theme.of(context).textTheme.headlineSmall, + // ), + // Text( + // '(approx. -${(plateResult.totalAchieved - currentSet.targetWeightTotal).abs().toStringAsFixed(1)} kg)', + // style: Theme.of(context).textTheme.bodySmall, + // ), + // ], + // ), + // ) + // else + // PlateVisualizer( + // plateConfiguration: plateResult.plateConfiguration, + // isTwoSided: currentExercise.exerciseId == 'squat', + // exerciseName: currentExercise.exerciseName, + // ), + // const SizedBox(height: 32), + // Text( + // 'Reps Completed', + // style: Theme.of(context).textTheme.titleLarge, + // textAlign: TextAlign.center, + // ), + // const SizedBox(height: 16), + // Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // IconButton( + // icon: const Icon(Icons.remove_circle), + // iconSize: 48, + // color: AppTheme.primaryColor, + // onPressed: _repsCompleted > 0 + // ? () => setState(() => _repsCompleted--) + // : null, + // ), + // const SizedBox(width: 24), + // SizedBox( + // width: 100, + // child: Text( + // _repsCompleted.toString(), + // style: Theme.of(context) + // .textTheme + // .displayLarge + // ?.copyWith( + // fontSize: 64, color: AppTheme.primaryColor), + // textAlign: TextAlign.center, + // ), + // ), + // const SizedBox(width: 24), + // IconButton( + // icon: const Icon(Icons.add_circle), + // iconSize: 48, + // color: AppTheme.primaryColor, + // onPressed: () => setState(() => _repsCompleted++), + // ), + // ], + // ), + // if (currentSet.isAmrap) + // Padding( + // padding: const EdgeInsets.only(top: 8), + // child: Text( + // 'AMRAP - Go for max reps!', + // style: Theme.of(context).textTheme.bodyMedium?.copyWith( + // color: AppTheme.secondaryColor, + // fontWeight: FontWeight.bold), + // textAlign: TextAlign.center, + // ), + // ), + // const SizedBox(height: 32), + // ElevatedButton( + // onPressed: _repsCompleted >= currentSet.repsTarget + // ? _completeSet + // : null, + // style: ElevatedButton.styleFrom( + // padding: const EdgeInsets.symmetric(vertical: 20)), + // child: const Text('COMPLETE SET'), + // ), + // ], + // ), + // ), + // ), + // ], + // ); + // } + + String _formatTime(int seconds) { + final minutes = seconds ~/ 60; + final secs = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart b/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart new file mode 100644 index 0000000..c8f3b22 --- /dev/null +++ b/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; + +class EnemyHPBar extends StatelessWidget { + final int current; + final int max; + + const EnemyHPBar({ + super.key, + required this.current, + required this.max, + }); + + @override + Widget build(BuildContext context) { + final percentage = max > 0 ? current / max : 0.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon( + Icons.favorite, + color: AppTheme.errorColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'HP', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + Text( + '$current / $max', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: AppTheme.errorColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + + Stack( + children: [ + // Background + Container( + height: 24, + decoration: BoxDecoration( + color: Colors.red[900], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.errorColor.withOpacity(0.5), + width: 2, + ), + ), + ), + + // HP Fill + FractionallySizedBox( + widthFactor: percentage.clamp(0.0, 1.0), + child: Container( + height: 24, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.errorColor, + Colors.red[300]!, + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.errorColor.withOpacity(0.5), + blurRadius: 8, + ), + ], + ), + ), + ), + ], + ), + ], + ); + } +} + diff --git a/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart new file mode 100644 index 0000000..58a987a --- /dev/null +++ b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/constants/asset_paths.dart'; + +class PlateVisualizer extends StatelessWidget { + final List plateConfiguration; + final bool isTwoSided; + final String exerciseName; + + const PlateVisualizer({ + super.key, + required this.plateConfiguration, + required this.isTwoSided, + required this.exerciseName, + }); + + Color _getPlateColor(double weight) { + final colorValue = PlateColors.colors[weight]; + return colorValue != null ? Color(colorValue) : Colors.grey; + } + + @override + Widget build(BuildContext context) { + if (plateConfiguration.isEmpty) { + return Card( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon( + isTwoSided ? Icons.fitness_center : Icons.accessibility, + size: 64, + color: AppTheme.primaryColor.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + isTwoSided ? 'Bar Only' : 'Bodyweight Only', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: AppTheme.textSecondary), + ), + ], + ), + ), + ); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + isTwoSided ? 'Load Per Side' : 'Load on Belt', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + if (isTwoSided) _buildBarbellView() else _buildBeltView(), + const SizedBox(height: 16), + Text( + 'Total: ${plateConfiguration.fold(0, (sum, p) => sum + p).toStringAsFixed(1)} kg ${isTwoSided ? 'per side' : ''}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppTheme.primaryColor, + ), + ), + ], + ), + ), + ); + } + + Widget _buildBarbellView() { + return SizedBox( + height: 120, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left collar + Container( + width: 8, + height: 80, + color: Colors.grey[800], + ), + + // Plates (from largest to smallest) + ...plateConfiguration.map((weight) { + final size = _getPlateSize(weight); + return Container( + width: 20, + height: size, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: _getPlateColor(weight), + border: Border.all(color: Colors.white24, width: 2), + borderRadius: BorderRadius.circular(4), + ), + ); + }).toList(), + + // Sleeve (bar end) + Container( + width: 40, + height: 20, + decoration: BoxDecoration( + color: Colors.grey[700], + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Container( + width: 30, + height: 10, + decoration: BoxDecoration( + color: Colors.grey[600], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildBeltView() { + return Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: plateConfiguration.map((weight) { + return Container( + width: _getPlateSize(weight) * 0.8, + height: _getPlateSize(weight) * 0.8, + decoration: BoxDecoration( + color: _getPlateColor(weight), + shape: BoxShape.circle, + border: Border.all(color: Colors.white24, width: 3), + ), + child: Center( + child: Text( + weight == weight.toInt() + ? '${weight.toInt()}' + : weight.toStringAsFixed(2), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ); + }).toList(), + ); + } + + double _getPlateSize(double weight) { + // Scale plate size based on weight + if (weight >= 20) return 120.0; + if (weight >= 10) return 100.0; + if (weight >= 5) return 80.0; + return 60.0; + } +} diff --git a/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart b/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart new file mode 100644 index 0000000..bd575d9 --- /dev/null +++ b/lib/src/features/workout_runner/presentation/widgets/timer_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import '../../../../core/theme/app_theme.dart'; + +class TimerWidget extends StatefulWidget { + const TimerWidget({super.key}); + + @override + State createState() => _TimerWidgetState(); +} + +class _TimerWidgetState extends State { + int _seconds = 0; + Timer? _timer; + bool _isRunning = false; + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _start() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() => _seconds++); + }); + setState(() => _isRunning = true); + } + + void _pause() { + _timer?.cancel(); + setState(() => _isRunning = false); + } + + void _reset() { + _timer?.cancel(); + setState(() { + _seconds = 0; + _isRunning = false; + }); + } + + String _formatTime() { + final minutes = _seconds ~/ 60; + final secs = _seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _formatTime(), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 16), + IconButton( + icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow), + onPressed: _isRunning ? _pause : _start, + color: AppTheme.primaryColor, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _reset, + color: AppTheme.primaryColor, + ), + ], + ); + } +} + diff --git a/lib/src/shared/data/local/collections/cycle_collection.dart b/lib/src/shared/data/local/collections/cycle_collection.dart new file mode 100644 index 0000000..72bd209 --- /dev/null +++ b/lib/src/shared/data/local/collections/cycle_collection.dart @@ -0,0 +1,27 @@ +import 'package:isar/isar.dart'; + +part 'cycle_collection.g.dart'; + +@collection +class CycleCollection { + Id id = Isar.autoIncrement; + + @Index(unique: true) + String? serverId; + + String userId = ''; // Local reference + int cycleNumber = 1; + + DateTime startDate = DateTime.now(); + DateTime? endDate; + + bool isActive = true; + + // Training Maxes (stored as JSON string) + String trainingMaxesJson = '{}'; + + bool isDirty = false; + DateTime createdAt = DateTime.now(); + DateTime updatedAt = DateTime.now(); +} + diff --git a/lib/src/shared/data/local/collections/cycle_collection.g.dart b/lib/src/shared/data/local/collections/cycle_collection.g.dart new file mode 100644 index 0000000..5642b32 --- /dev/null +++ b/lib/src/shared/data/local/collections/cycle_collection.g.dart @@ -0,0 +1,1649 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cycle_collection.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetCycleCollectionCollection on Isar { + IsarCollection get cycleCollections => this.collection(); +} + +const CycleCollectionSchema = CollectionSchema( + name: r'CycleCollection', + id: 3532561085934404, + properties: { + r'createdAt': PropertySchema( + id: 0, + name: r'createdAt', + type: IsarType.dateTime, + ), + r'cycleNumber': PropertySchema( + id: 1, + name: r'cycleNumber', + type: IsarType.long, + ), + r'endDate': PropertySchema( + id: 2, + name: r'endDate', + type: IsarType.dateTime, + ), + r'isActive': PropertySchema( + id: 3, + name: r'isActive', + type: IsarType.bool, + ), + r'isDirty': PropertySchema( + id: 4, + name: r'isDirty', + type: IsarType.bool, + ), + r'serverId': PropertySchema( + id: 5, + name: r'serverId', + type: IsarType.string, + ), + r'startDate': PropertySchema( + id: 6, + name: r'startDate', + type: IsarType.dateTime, + ), + r'trainingMaxesJson': PropertySchema( + id: 7, + name: r'trainingMaxesJson', + type: IsarType.string, + ), + r'updatedAt': PropertySchema( + id: 8, + name: r'updatedAt', + type: IsarType.dateTime, + ), + r'userId': PropertySchema( + id: 9, + name: r'userId', + type: IsarType.string, + ) + }, + estimateSize: _cycleCollectionEstimateSize, + serialize: _cycleCollectionSerialize, + deserialize: _cycleCollectionDeserialize, + deserializeProp: _cycleCollectionDeserializeProp, + idName: r'id', + indexes: { + r'serverId': IndexSchema( + id: -7950187970872907662, + name: r'serverId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'serverId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _cycleCollectionGetId, + getLinks: _cycleCollectionGetLinks, + attach: _cycleCollectionAttach, + version: '3.1.0+1', +); + +int _cycleCollectionEstimateSize( + CycleCollection object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.serverId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.trainingMaxesJson.length * 3; + bytesCount += 3 + object.userId.length * 3; + return bytesCount; +} + +void _cycleCollectionSerialize( + CycleCollection object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeDateTime(offsets[0], object.createdAt); + writer.writeLong(offsets[1], object.cycleNumber); + writer.writeDateTime(offsets[2], object.endDate); + writer.writeBool(offsets[3], object.isActive); + writer.writeBool(offsets[4], object.isDirty); + writer.writeString(offsets[5], object.serverId); + writer.writeDateTime(offsets[6], object.startDate); + writer.writeString(offsets[7], object.trainingMaxesJson); + writer.writeDateTime(offsets[8], object.updatedAt); + writer.writeString(offsets[9], object.userId); +} + +CycleCollection _cycleCollectionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = CycleCollection(); + object.createdAt = reader.readDateTime(offsets[0]); + object.cycleNumber = reader.readLong(offsets[1]); + object.endDate = reader.readDateTimeOrNull(offsets[2]); + object.id = id; + object.isActive = reader.readBool(offsets[3]); + object.isDirty = reader.readBool(offsets[4]); + object.serverId = reader.readStringOrNull(offsets[5]); + object.startDate = reader.readDateTime(offsets[6]); + object.trainingMaxesJson = reader.readString(offsets[7]); + object.updatedAt = reader.readDateTime(offsets[8]); + object.userId = reader.readString(offsets[9]); + return object; +} + +P _cycleCollectionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readDateTime(offset)) as P; + case 1: + return (reader.readLong(offset)) as P; + case 2: + return (reader.readDateTimeOrNull(offset)) as P; + case 3: + return (reader.readBool(offset)) as P; + case 4: + return (reader.readBool(offset)) as P; + case 5: + return (reader.readStringOrNull(offset)) as P; + case 6: + return (reader.readDateTime(offset)) as P; + case 7: + return (reader.readString(offset)) as P; + case 8: + return (reader.readDateTime(offset)) as P; + case 9: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _cycleCollectionGetId(CycleCollection object) { + return object.id; +} + +List> _cycleCollectionGetLinks(CycleCollection object) { + return []; +} + +void _cycleCollectionAttach( + IsarCollection col, Id id, CycleCollection object) { + object.id = id; +} + +extension CycleCollectionByIndex on IsarCollection { + Future getByServerId(String? serverId) { + return getByIndex(r'serverId', [serverId]); + } + + CycleCollection? getByServerIdSync(String? serverId) { + return getByIndexSync(r'serverId', [serverId]); + } + + Future deleteByServerId(String? serverId) { + return deleteByIndex(r'serverId', [serverId]); + } + + bool deleteByServerIdSync(String? serverId) { + return deleteByIndexSync(r'serverId', [serverId]); + } + + Future> getAllByServerId( + List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return getAllByIndex(r'serverId', values); + } + + List getAllByServerIdSync(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'serverId', values); + } + + Future deleteAllByServerId(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'serverId', values); + } + + int deleteAllByServerIdSync(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'serverId', values); + } + + Future putByServerId(CycleCollection object) { + return putByIndex(r'serverId', object); + } + + Id putByServerIdSync(CycleCollection object, {bool saveLinks = true}) { + return putByIndexSync(r'serverId', object, saveLinks: saveLinks); + } + + Future> putAllByServerId(List objects) { + return putAllByIndex(r'serverId', objects); + } + + List putAllByServerIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'serverId', objects, saveLinks: saveLinks); + } +} + +extension CycleCollectionQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension CycleCollectionQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [null], + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [null], + includeLower: false, + upper: [], + )); + }); + } + + QueryBuilder + serverIdEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [serverId], + )); + }); + } + + QueryBuilder + serverIdNotEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )); + } + }); + } +} + +extension CycleCollectionQueryFilter + on QueryBuilder { + QueryBuilder + createdAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'createdAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + cycleNumberEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'cycleNumber', + value: value, + )); + }); + } + + QueryBuilder + cycleNumberGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'cycleNumber', + value: value, + )); + }); + } + + QueryBuilder + cycleNumberLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'cycleNumber', + value: value, + )); + }); + } + + QueryBuilder + cycleNumberBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'cycleNumber', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + endDateIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'endDate', + )); + }); + } + + QueryBuilder + endDateIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'endDate', + )); + }); + } + + QueryBuilder + endDateEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'endDate', + value: value, + )); + }); + } + + QueryBuilder + endDateGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'endDate', + value: value, + )); + }); + } + + QueryBuilder + endDateLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'endDate', + value: value, + )); + }); + } + + QueryBuilder + endDateBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'endDate', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + isActiveEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isActive', + value: value, + )); + }); + } + + QueryBuilder + isDirtyEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isDirty', + value: value, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serverId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'serverId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + serverIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + startDateEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'startDate', + value: value, + )); + }); + } + + QueryBuilder + startDateGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'startDate', + value: value, + )); + }); + } + + QueryBuilder + startDateLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'startDate', + value: value, + )); + }); + } + + QueryBuilder + startDateBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'startDate', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + trainingMaxesJsonEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'trainingMaxesJson', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'trainingMaxesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'trainingMaxesJson', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + trainingMaxesJsonIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'trainingMaxesJson', + value: '', + )); + }); + } + + QueryBuilder + trainingMaxesJsonIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'trainingMaxesJson', + value: '', + )); + }); + } + + QueryBuilder + updatedAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'updatedAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + userIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'userId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'userId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'userId', + value: '', + )); + }); + } + + QueryBuilder + userIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'userId', + value: '', + )); + }); + } +} + +extension CycleCollectionQueryObject + on QueryBuilder {} + +extension CycleCollectionQueryLinks + on QueryBuilder {} + +extension CycleCollectionQuerySortBy + on QueryBuilder { + QueryBuilder + sortByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + sortByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + sortByCycleNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleNumber', Sort.asc); + }); + } + + QueryBuilder + sortByCycleNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleNumber', Sort.desc); + }); + } + + QueryBuilder sortByEndDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'endDate', Sort.asc); + }); + } + + QueryBuilder + sortByEndDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'endDate', Sort.desc); + }); + } + + QueryBuilder + sortByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.asc); + }); + } + + QueryBuilder + sortByIsActiveDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.desc); + }); + } + + QueryBuilder sortByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + sortByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + sortByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + sortByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder + sortByStartDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'startDate', Sort.asc); + }); + } + + QueryBuilder + sortByStartDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'startDate', Sort.desc); + }); + } + + QueryBuilder + sortByTrainingMaxesJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'trainingMaxesJson', Sort.asc); + }); + } + + QueryBuilder + sortByTrainingMaxesJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'trainingMaxesJson', Sort.desc); + }); + } + + QueryBuilder + sortByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + sortByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder sortByUserId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.asc); + }); + } + + QueryBuilder + sortByUserIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.desc); + }); + } +} + +extension CycleCollectionQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + thenByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + thenByCycleNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleNumber', Sort.asc); + }); + } + + QueryBuilder + thenByCycleNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleNumber', Sort.desc); + }); + } + + QueryBuilder thenByEndDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'endDate', Sort.asc); + }); + } + + QueryBuilder + thenByEndDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'endDate', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.asc); + }); + } + + QueryBuilder + thenByIsActiveDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.desc); + }); + } + + QueryBuilder thenByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + thenByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + thenByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + thenByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder + thenByStartDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'startDate', Sort.asc); + }); + } + + QueryBuilder + thenByStartDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'startDate', Sort.desc); + }); + } + + QueryBuilder + thenByTrainingMaxesJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'trainingMaxesJson', Sort.asc); + }); + } + + QueryBuilder + thenByTrainingMaxesJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'trainingMaxesJson', Sort.desc); + }); + } + + QueryBuilder + thenByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + thenByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder thenByUserId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.asc); + }); + } + + QueryBuilder + thenByUserIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.desc); + }); + } +} + +extension CycleCollectionQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'createdAt'); + }); + } + + QueryBuilder + distinctByCycleNumber() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'cycleNumber'); + }); + } + + QueryBuilder + distinctByEndDate() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'endDate'); + }); + } + + QueryBuilder + distinctByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isActive'); + }); + } + + QueryBuilder + distinctByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isDirty'); + }); + } + + QueryBuilder distinctByServerId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serverId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByStartDate() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'startDate'); + }); + } + + QueryBuilder + distinctByTrainingMaxesJson({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'trainingMaxesJson', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'updatedAt'); + }); + } + + QueryBuilder distinctByUserId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'userId', caseSensitive: caseSensitive); + }); + } +} + +extension CycleCollectionQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + createdAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'createdAt'); + }); + } + + QueryBuilder cycleNumberProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'cycleNumber'); + }); + } + + QueryBuilder endDateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'endDate'); + }); + } + + QueryBuilder isActiveProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isActive'); + }); + } + + QueryBuilder isDirtyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isDirty'); + }); + } + + QueryBuilder serverIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serverId'); + }); + } + + QueryBuilder + startDateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'startDate'); + }); + } + + QueryBuilder + trainingMaxesJsonProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'trainingMaxesJson'); + }); + } + + QueryBuilder + updatedAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'updatedAt'); + }); + } + + QueryBuilder userIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'userId'); + }); + } +} diff --git a/lib/src/shared/data/local/collections/user_collection.dart b/lib/src/shared/data/local/collections/user_collection.dart new file mode 100644 index 0000000..e581e83 --- /dev/null +++ b/lib/src/shared/data/local/collections/user_collection.dart @@ -0,0 +1,26 @@ +import 'package:isar/isar.dart'; + +part 'user_collection.g.dart'; + +@collection +class UserCollection { + Id id = Isar.autoIncrement; + + @Index(unique: true) + String? serverId; // PocketBase ID + + String email = ''; + int xp = 0; + int level = 1; + double currentBodyweight = 70.0; + + String? inventorySettingsJson; // JSON string + String? avatarConfigJson; // JSON string + + DateTime? lastSyncAt; + bool isDirty = false; // Needs sync + + DateTime createdAt = DateTime.now(); + DateTime updatedAt = DateTime.now(); +} + diff --git a/lib/src/shared/data/local/collections/user_collection.g.dart b/lib/src/shared/data/local/collections/user_collection.g.dart new file mode 100644 index 0000000..4ecd4ee --- /dev/null +++ b/lib/src/shared/data/local/collections/user_collection.g.dart @@ -0,0 +1,1921 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_collection.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetUserCollectionCollection on Isar { + IsarCollection get userCollections => this.collection(); +} + +const UserCollectionSchema = CollectionSchema( + name: r'UserCollection', + id: 1551134645489327298, + properties: { + r'avatarConfigJson': PropertySchema( + id: 0, + name: r'avatarConfigJson', + type: IsarType.string, + ), + r'createdAt': PropertySchema( + id: 1, + name: r'createdAt', + type: IsarType.dateTime, + ), + r'currentBodyweight': PropertySchema( + id: 2, + name: r'currentBodyweight', + type: IsarType.double, + ), + r'email': PropertySchema( + id: 3, + name: r'email', + type: IsarType.string, + ), + r'inventorySettingsJson': PropertySchema( + id: 4, + name: r'inventorySettingsJson', + type: IsarType.string, + ), + r'isDirty': PropertySchema( + id: 5, + name: r'isDirty', + type: IsarType.bool, + ), + r'lastSyncAt': PropertySchema( + id: 6, + name: r'lastSyncAt', + type: IsarType.dateTime, + ), + r'level': PropertySchema( + id: 7, + name: r'level', + type: IsarType.long, + ), + r'serverId': PropertySchema( + id: 8, + name: r'serverId', + type: IsarType.string, + ), + r'updatedAt': PropertySchema( + id: 9, + name: r'updatedAt', + type: IsarType.dateTime, + ), + r'xp': PropertySchema( + id: 10, + name: r'xp', + type: IsarType.long, + ) + }, + estimateSize: _userCollectionEstimateSize, + serialize: _userCollectionSerialize, + deserialize: _userCollectionDeserialize, + deserializeProp: _userCollectionDeserializeProp, + idName: r'id', + indexes: { + r'serverId': IndexSchema( + id: -7950187970872907662, + name: r'serverId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'serverId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _userCollectionGetId, + getLinks: _userCollectionGetLinks, + attach: _userCollectionAttach, + version: '3.1.0+1', +); + +int _userCollectionEstimateSize( + UserCollection object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.avatarConfigJson; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.email.length * 3; + { + final value = object.inventorySettingsJson; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.serverId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _userCollectionSerialize( + UserCollection object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.avatarConfigJson); + writer.writeDateTime(offsets[1], object.createdAt); + writer.writeDouble(offsets[2], object.currentBodyweight); + writer.writeString(offsets[3], object.email); + writer.writeString(offsets[4], object.inventorySettingsJson); + writer.writeBool(offsets[5], object.isDirty); + writer.writeDateTime(offsets[6], object.lastSyncAt); + writer.writeLong(offsets[7], object.level); + writer.writeString(offsets[8], object.serverId); + writer.writeDateTime(offsets[9], object.updatedAt); + writer.writeLong(offsets[10], object.xp); +} + +UserCollection _userCollectionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = UserCollection(); + object.avatarConfigJson = reader.readStringOrNull(offsets[0]); + object.createdAt = reader.readDateTime(offsets[1]); + object.currentBodyweight = reader.readDouble(offsets[2]); + object.email = reader.readString(offsets[3]); + object.id = id; + object.inventorySettingsJson = reader.readStringOrNull(offsets[4]); + object.isDirty = reader.readBool(offsets[5]); + object.lastSyncAt = reader.readDateTimeOrNull(offsets[6]); + object.level = reader.readLong(offsets[7]); + object.serverId = reader.readStringOrNull(offsets[8]); + object.updatedAt = reader.readDateTime(offsets[9]); + object.xp = reader.readLong(offsets[10]); + return object; +} + +P _userCollectionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readDateTime(offset)) as P; + case 2: + return (reader.readDouble(offset)) as P; + case 3: + return (reader.readString(offset)) as P; + case 4: + return (reader.readStringOrNull(offset)) as P; + case 5: + return (reader.readBool(offset)) as P; + case 6: + return (reader.readDateTimeOrNull(offset)) as P; + case 7: + return (reader.readLong(offset)) as P; + case 8: + return (reader.readStringOrNull(offset)) as P; + case 9: + return (reader.readDateTime(offset)) as P; + case 10: + return (reader.readLong(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _userCollectionGetId(UserCollection object) { + return object.id; +} + +List> _userCollectionGetLinks(UserCollection object) { + return []; +} + +void _userCollectionAttach( + IsarCollection col, Id id, UserCollection object) { + object.id = id; +} + +extension UserCollectionByIndex on IsarCollection { + Future getByServerId(String? serverId) { + return getByIndex(r'serverId', [serverId]); + } + + UserCollection? getByServerIdSync(String? serverId) { + return getByIndexSync(r'serverId', [serverId]); + } + + Future deleteByServerId(String? serverId) { + return deleteByIndex(r'serverId', [serverId]); + } + + bool deleteByServerIdSync(String? serverId) { + return deleteByIndexSync(r'serverId', [serverId]); + } + + Future> getAllByServerId(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return getAllByIndex(r'serverId', values); + } + + List getAllByServerIdSync(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'serverId', values); + } + + Future deleteAllByServerId(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'serverId', values); + } + + int deleteAllByServerIdSync(List serverIdValues) { + final values = serverIdValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'serverId', values); + } + + Future putByServerId(UserCollection object) { + return putByIndex(r'serverId', object); + } + + Id putByServerIdSync(UserCollection object, {bool saveLinks = true}) { + return putByIndexSync(r'serverId', object, saveLinks: saveLinks); + } + + Future> putAllByServerId(List objects) { + return putAllByIndex(r'serverId', objects); + } + + List putAllByServerIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'serverId', objects, saveLinks: saveLinks); + } +} + +extension UserCollectionQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension UserCollectionQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [null], + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [null], + includeLower: false, + upper: [], + )); + }); + } + + QueryBuilder + serverIdEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [serverId], + )); + }); + } + + QueryBuilder + serverIdNotEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )); + } + }); + } +} + +extension UserCollectionQueryFilter + on QueryBuilder { + QueryBuilder + avatarConfigJsonIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'avatarConfigJson', + )); + }); + } + + QueryBuilder + avatarConfigJsonIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'avatarConfigJson', + )); + }); + } + + QueryBuilder + avatarConfigJsonEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'avatarConfigJson', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'avatarConfigJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'avatarConfigJson', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + avatarConfigJsonIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'avatarConfigJson', + value: '', + )); + }); + } + + QueryBuilder + avatarConfigJsonIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'avatarConfigJson', + value: '', + )); + }); + } + + QueryBuilder + createdAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'createdAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + currentBodyweightEqualTo( + double value, { + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'currentBodyweight', + value: value, + epsilon: epsilon, + )); + }); + } + + QueryBuilder + currentBodyweightGreaterThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'currentBodyweight', + value: value, + epsilon: epsilon, + )); + }); + } + + QueryBuilder + currentBodyweightLessThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'currentBodyweight', + value: value, + epsilon: epsilon, + )); + }); + } + + QueryBuilder + currentBodyweightBetween( + double lower, + double upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'currentBodyweight', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + )); + }); + } + + QueryBuilder + emailEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'email', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'email', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'email', + value: '', + )); + }); + } + + QueryBuilder + emailIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'email', + value: '', + )); + }); + } + + QueryBuilder idEqualTo( + Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + inventorySettingsJsonIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'inventorySettingsJson', + )); + }); + } + + QueryBuilder + inventorySettingsJsonIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'inventorySettingsJson', + )); + }); + } + + QueryBuilder + inventorySettingsJsonEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'inventorySettingsJson', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'inventorySettingsJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonMatches(String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'inventorySettingsJson', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inventorySettingsJsonIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inventorySettingsJson', + value: '', + )); + }); + } + + QueryBuilder + inventorySettingsJsonIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'inventorySettingsJson', + value: '', + )); + }); + } + + QueryBuilder + isDirtyEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isDirty', + value: value, + )); + }); + } + + QueryBuilder + lastSyncAtIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastSyncAt', + )); + }); + } + + QueryBuilder + lastSyncAtIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastSyncAt', + )); + }); + } + + QueryBuilder + lastSyncAtEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastSyncAt', + value: value, + )); + }); + } + + QueryBuilder + lastSyncAtGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastSyncAt', + value: value, + )); + }); + } + + QueryBuilder + lastSyncAtLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastSyncAt', + value: value, + )); + }); + } + + QueryBuilder + lastSyncAtBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastSyncAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + levelEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'level', + value: value, + )); + }); + } + + QueryBuilder + levelGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'level', + value: value, + )); + }); + } + + QueryBuilder + levelLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'level', + value: value, + )); + }); + } + + QueryBuilder + levelBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'level', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serverId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'serverId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + serverIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + updatedAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'updatedAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder xpEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'xp', + value: value, + )); + }); + } + + QueryBuilder + xpGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'xp', + value: value, + )); + }); + } + + QueryBuilder + xpLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'xp', + value: value, + )); + }); + } + + QueryBuilder xpBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'xp', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension UserCollectionQueryObject + on QueryBuilder {} + +extension UserCollectionQueryLinks + on QueryBuilder {} + +extension UserCollectionQuerySortBy + on QueryBuilder { + QueryBuilder + sortByAvatarConfigJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarConfigJson', Sort.asc); + }); + } + + QueryBuilder + sortByAvatarConfigJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarConfigJson', Sort.desc); + }); + } + + QueryBuilder sortByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + sortByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + sortByCurrentBodyweight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentBodyweight', Sort.asc); + }); + } + + QueryBuilder + sortByCurrentBodyweightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentBodyweight', Sort.desc); + }); + } + + QueryBuilder sortByEmail() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.asc); + }); + } + + QueryBuilder sortByEmailDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.desc); + }); + } + + QueryBuilder + sortByInventorySettingsJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inventorySettingsJson', Sort.asc); + }); + } + + QueryBuilder + sortByInventorySettingsJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inventorySettingsJson', Sort.desc); + }); + } + + QueryBuilder sortByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + sortByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + sortByLastSyncAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSyncAt', Sort.asc); + }); + } + + QueryBuilder + sortByLastSyncAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSyncAt', Sort.desc); + }); + } + + QueryBuilder sortByLevel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'level', Sort.asc); + }); + } + + QueryBuilder sortByLevelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'level', Sort.desc); + }); + } + + QueryBuilder sortByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + sortByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder sortByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + sortByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder sortByXp() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xp', Sort.asc); + }); + } + + QueryBuilder sortByXpDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xp', Sort.desc); + }); + } +} + +extension UserCollectionQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByAvatarConfigJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarConfigJson', Sort.asc); + }); + } + + QueryBuilder + thenByAvatarConfigJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarConfigJson', Sort.desc); + }); + } + + QueryBuilder thenByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + thenByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + thenByCurrentBodyweight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentBodyweight', Sort.asc); + }); + } + + QueryBuilder + thenByCurrentBodyweightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentBodyweight', Sort.desc); + }); + } + + QueryBuilder thenByEmail() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.asc); + }); + } + + QueryBuilder thenByEmailDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByInventorySettingsJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inventorySettingsJson', Sort.asc); + }); + } + + QueryBuilder + thenByInventorySettingsJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inventorySettingsJson', Sort.desc); + }); + } + + QueryBuilder thenByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + thenByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + thenByLastSyncAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSyncAt', Sort.asc); + }); + } + + QueryBuilder + thenByLastSyncAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSyncAt', Sort.desc); + }); + } + + QueryBuilder thenByLevel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'level', Sort.asc); + }); + } + + QueryBuilder thenByLevelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'level', Sort.desc); + }); + } + + QueryBuilder thenByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + thenByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder thenByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + thenByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder thenByXp() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xp', Sort.asc); + }); + } + + QueryBuilder thenByXpDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xp', Sort.desc); + }); + } +} + +extension UserCollectionQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByAvatarConfigJson({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'avatarConfigJson', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'createdAt'); + }); + } + + QueryBuilder + distinctByCurrentBodyweight() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'currentBodyweight'); + }); + } + + QueryBuilder distinctByEmail( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'email', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByInventorySettingsJson({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'inventorySettingsJson', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isDirty'); + }); + } + + QueryBuilder + distinctByLastSyncAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastSyncAt'); + }); + } + + QueryBuilder distinctByLevel() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'level'); + }); + } + + QueryBuilder distinctByServerId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serverId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'updatedAt'); + }); + } + + QueryBuilder distinctByXp() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'xp'); + }); + } +} + +extension UserCollectionQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + avatarConfigJsonProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'avatarConfigJson'); + }); + } + + QueryBuilder createdAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'createdAt'); + }); + } + + QueryBuilder + currentBodyweightProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'currentBodyweight'); + }); + } + + QueryBuilder emailProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'email'); + }); + } + + QueryBuilder + inventorySettingsJsonProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'inventorySettingsJson'); + }); + } + + QueryBuilder isDirtyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isDirty'); + }); + } + + QueryBuilder + lastSyncAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastSyncAt'); + }); + } + + QueryBuilder levelProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'level'); + }); + } + + QueryBuilder serverIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serverId'); + }); + } + + QueryBuilder updatedAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'updatedAt'); + }); + } + + QueryBuilder xpProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'xp'); + }); + } +} diff --git a/lib/src/shared/data/local/collections/workout_collection.dart b/lib/src/shared/data/local/collections/workout_collection.dart new file mode 100644 index 0000000..dc3c6d8 --- /dev/null +++ b/lib/src/shared/data/local/collections/workout_collection.dart @@ -0,0 +1,32 @@ +import 'package:isar/isar.dart'; + +part 'workout_collection.g.dart'; + +@collection +class WorkoutCollection { + Id id = Isar.autoIncrement; + + // @Index(unique: true) + @Index() + String? serverId; + + String userId = ''; + String cycleId = ''; + + int week = 1; // 1-4 + int day = 1; // 1-3 + + DateTime? scheduledDate; + DateTime? completedAt; + + int xpEarned = 0; + + // Exercises data (JSON string) + String exercisesJson = '[]'; + + String notes = ''; + + bool isDirty = false; + DateTime createdAt = DateTime.now(); + DateTime updatedAt = DateTime.now(); +} diff --git a/lib/src/shared/data/local/collections/workout_collection.g.dart b/lib/src/shared/data/local/collections/workout_collection.g.dart new file mode 100644 index 0000000..fe6fde4 --- /dev/null +++ b/lib/src/shared/data/local/collections/workout_collection.g.dart @@ -0,0 +1,2145 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'workout_collection.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetWorkoutCollectionCollection on Isar { + IsarCollection get workoutCollections => this.collection(); +} + +const WorkoutCollectionSchema = CollectionSchema( + name: r'WorkoutCollection', + id: -3773153767250735449, + properties: { + r'completedAt': PropertySchema( + id: 0, + name: r'completedAt', + type: IsarType.dateTime, + ), + r'createdAt': PropertySchema( + id: 1, + name: r'createdAt', + type: IsarType.dateTime, + ), + r'cycleId': PropertySchema( + id: 2, + name: r'cycleId', + type: IsarType.string, + ), + r'day': PropertySchema( + id: 3, + name: r'day', + type: IsarType.long, + ), + r'exercisesJson': PropertySchema( + id: 4, + name: r'exercisesJson', + type: IsarType.string, + ), + r'isDirty': PropertySchema( + id: 5, + name: r'isDirty', + type: IsarType.bool, + ), + r'notes': PropertySchema( + id: 6, + name: r'notes', + type: IsarType.string, + ), + r'scheduledDate': PropertySchema( + id: 7, + name: r'scheduledDate', + type: IsarType.dateTime, + ), + r'serverId': PropertySchema( + id: 8, + name: r'serverId', + type: IsarType.string, + ), + r'updatedAt': PropertySchema( + id: 9, + name: r'updatedAt', + type: IsarType.dateTime, + ), + r'userId': PropertySchema( + id: 10, + name: r'userId', + type: IsarType.string, + ), + r'week': PropertySchema( + id: 11, + name: r'week', + type: IsarType.long, + ), + r'xpEarned': PropertySchema( + id: 12, + name: r'xpEarned', + type: IsarType.long, + ) + }, + estimateSize: _workoutCollectionEstimateSize, + serialize: _workoutCollectionSerialize, + deserialize: _workoutCollectionDeserialize, + deserializeProp: _workoutCollectionDeserializeProp, + idName: r'id', + indexes: { + r'serverId': IndexSchema( + id: -7950187970872907662, + name: r'serverId', + unique: false, + replace: false, + properties: [ + IndexPropertySchema( + name: r'serverId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _workoutCollectionGetId, + getLinks: _workoutCollectionGetLinks, + attach: _workoutCollectionAttach, + version: '3.1.0+1', +); + +int _workoutCollectionEstimateSize( + WorkoutCollection object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.cycleId.length * 3; + bytesCount += 3 + object.exercisesJson.length * 3; + bytesCount += 3 + object.notes.length * 3; + { + final value = object.serverId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.userId.length * 3; + return bytesCount; +} + +void _workoutCollectionSerialize( + WorkoutCollection object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeDateTime(offsets[0], object.completedAt); + writer.writeDateTime(offsets[1], object.createdAt); + writer.writeString(offsets[2], object.cycleId); + writer.writeLong(offsets[3], object.day); + writer.writeString(offsets[4], object.exercisesJson); + writer.writeBool(offsets[5], object.isDirty); + writer.writeString(offsets[6], object.notes); + writer.writeDateTime(offsets[7], object.scheduledDate); + writer.writeString(offsets[8], object.serverId); + writer.writeDateTime(offsets[9], object.updatedAt); + writer.writeString(offsets[10], object.userId); + writer.writeLong(offsets[11], object.week); + writer.writeLong(offsets[12], object.xpEarned); +} + +WorkoutCollection _workoutCollectionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = WorkoutCollection(); + object.completedAt = reader.readDateTimeOrNull(offsets[0]); + object.createdAt = reader.readDateTime(offsets[1]); + object.cycleId = reader.readString(offsets[2]); + object.day = reader.readLong(offsets[3]); + object.exercisesJson = reader.readString(offsets[4]); + object.id = id; + object.isDirty = reader.readBool(offsets[5]); + object.notes = reader.readString(offsets[6]); + object.scheduledDate = reader.readDateTimeOrNull(offsets[7]); + object.serverId = reader.readStringOrNull(offsets[8]); + object.updatedAt = reader.readDateTime(offsets[9]); + object.userId = reader.readString(offsets[10]); + object.week = reader.readLong(offsets[11]); + object.xpEarned = reader.readLong(offsets[12]); + return object; +} + +P _workoutCollectionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readDateTimeOrNull(offset)) as P; + case 1: + return (reader.readDateTime(offset)) as P; + case 2: + return (reader.readString(offset)) as P; + case 3: + return (reader.readLong(offset)) as P; + case 4: + return (reader.readString(offset)) as P; + case 5: + return (reader.readBool(offset)) as P; + case 6: + return (reader.readString(offset)) as P; + case 7: + return (reader.readDateTimeOrNull(offset)) as P; + case 8: + return (reader.readStringOrNull(offset)) as P; + case 9: + return (reader.readDateTime(offset)) as P; + case 10: + return (reader.readString(offset)) as P; + case 11: + return (reader.readLong(offset)) as P; + case 12: + return (reader.readLong(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _workoutCollectionGetId(WorkoutCollection object) { + return object.id; +} + +List> _workoutCollectionGetLinks( + WorkoutCollection object) { + return []; +} + +void _workoutCollectionAttach( + IsarCollection col, Id id, WorkoutCollection object) { + object.id = id; +} + +extension WorkoutCollectionQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension WorkoutCollectionQueryWhere + on QueryBuilder { + QueryBuilder + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [null], + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [null], + includeLower: false, + upper: [], + )); + }); + } + + QueryBuilder + serverIdEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'serverId', + value: [serverId], + )); + }); + } + + QueryBuilder + serverIdNotEqualTo(String? serverId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [serverId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'serverId', + lower: [], + upper: [serverId], + includeUpper: false, + )); + } + }); + } +} + +extension WorkoutCollectionQueryFilter + on QueryBuilder { + QueryBuilder + completedAtIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'completedAt', + )); + }); + } + + QueryBuilder + completedAtIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'completedAt', + )); + }); + } + + QueryBuilder + completedAtEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'completedAt', + value: value, + )); + }); + } + + QueryBuilder + completedAtGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'completedAt', + value: value, + )); + }); + } + + QueryBuilder + completedAtLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'completedAt', + value: value, + )); + }); + } + + QueryBuilder + completedAtBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'completedAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + createdAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'createdAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + cycleIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'cycleId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'cycleId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'cycleId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + cycleIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'cycleId', + value: '', + )); + }); + } + + QueryBuilder + cycleIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'cycleId', + value: '', + )); + }); + } + + QueryBuilder + dayEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'day', + value: value, + )); + }); + } + + QueryBuilder + dayGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'day', + value: value, + )); + }); + } + + QueryBuilder + dayLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'day', + value: value, + )); + }); + } + + QueryBuilder + dayBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'day', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + exercisesJsonEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'exercisesJson', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'exercisesJson', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'exercisesJson', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + exercisesJsonIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'exercisesJson', + value: '', + )); + }); + } + + QueryBuilder + exercisesJsonIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'exercisesJson', + value: '', + )); + }); + } + + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + isDirtyEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isDirty', + value: value, + )); + }); + } + + QueryBuilder + notesEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'notes', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'notes', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'notes', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + notesIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'notes', + value: '', + )); + }); + } + + QueryBuilder + notesIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'notes', + value: '', + )); + }); + } + + QueryBuilder + scheduledDateIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'scheduledDate', + )); + }); + } + + QueryBuilder + scheduledDateIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'scheduledDate', + )); + }); + } + + QueryBuilder + scheduledDateEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'scheduledDate', + value: value, + )); + }); + } + + QueryBuilder + scheduledDateGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'scheduledDate', + value: value, + )); + }); + } + + QueryBuilder + scheduledDateLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'scheduledDate', + value: value, + )); + }); + } + + QueryBuilder + scheduledDateBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'scheduledDate', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serverId', + )); + }); + } + + QueryBuilder + serverIdEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serverId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'serverId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'serverId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + serverIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'serverId', + value: '', + )); + }); + } + + QueryBuilder + updatedAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'updatedAt', + value: value, + )); + }); + } + + QueryBuilder + updatedAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'updatedAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + userIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'userId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'userId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'userId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + userIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'userId', + value: '', + )); + }); + } + + QueryBuilder + userIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'userId', + value: '', + )); + }); + } + + QueryBuilder + weekEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'week', + value: value, + )); + }); + } + + QueryBuilder + weekGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'week', + value: value, + )); + }); + } + + QueryBuilder + weekLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'week', + value: value, + )); + }); + } + + QueryBuilder + weekBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'week', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + xpEarnedEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'xpEarned', + value: value, + )); + }); + } + + QueryBuilder + xpEarnedGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'xpEarned', + value: value, + )); + }); + } + + QueryBuilder + xpEarnedLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'xpEarned', + value: value, + )); + }); + } + + QueryBuilder + xpEarnedBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'xpEarned', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension WorkoutCollectionQueryObject + on QueryBuilder {} + +extension WorkoutCollectionQueryLinks + on QueryBuilder {} + +extension WorkoutCollectionQuerySortBy + on QueryBuilder { + QueryBuilder + sortByCompletedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'completedAt', Sort.asc); + }); + } + + QueryBuilder + sortByCompletedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'completedAt', Sort.desc); + }); + } + + QueryBuilder + sortByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + sortByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + sortByCycleId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleId', Sort.asc); + }); + } + + QueryBuilder + sortByCycleIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleId', Sort.desc); + }); + } + + QueryBuilder sortByDay() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'day', Sort.asc); + }); + } + + QueryBuilder + sortByDayDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'day', Sort.desc); + }); + } + + QueryBuilder + sortByExercisesJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'exercisesJson', Sort.asc); + }); + } + + QueryBuilder + sortByExercisesJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'exercisesJson', Sort.desc); + }); + } + + QueryBuilder + sortByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + sortByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + sortByNotes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'notes', Sort.asc); + }); + } + + QueryBuilder + sortByNotesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'notes', Sort.desc); + }); + } + + QueryBuilder + sortByScheduledDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'scheduledDate', Sort.asc); + }); + } + + QueryBuilder + sortByScheduledDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'scheduledDate', Sort.desc); + }); + } + + QueryBuilder + sortByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + sortByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder + sortByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + sortByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder + sortByUserId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.asc); + }); + } + + QueryBuilder + sortByUserIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.desc); + }); + } + + QueryBuilder + sortByWeek() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'week', Sort.asc); + }); + } + + QueryBuilder + sortByWeekDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'week', Sort.desc); + }); + } + + QueryBuilder + sortByXpEarned() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xpEarned', Sort.asc); + }); + } + + QueryBuilder + sortByXpEarnedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xpEarned', Sort.desc); + }); + } +} + +extension WorkoutCollectionQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByCompletedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'completedAt', Sort.asc); + }); + } + + QueryBuilder + thenByCompletedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'completedAt', Sort.desc); + }); + } + + QueryBuilder + thenByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + thenByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + thenByCycleId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleId', Sort.asc); + }); + } + + QueryBuilder + thenByCycleIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'cycleId', Sort.desc); + }); + } + + QueryBuilder thenByDay() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'day', Sort.asc); + }); + } + + QueryBuilder + thenByDayDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'day', Sort.desc); + }); + } + + QueryBuilder + thenByExercisesJson() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'exercisesJson', Sort.asc); + }); + } + + QueryBuilder + thenByExercisesJsonDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'exercisesJson', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.asc); + }); + } + + QueryBuilder + thenByIsDirtyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isDirty', Sort.desc); + }); + } + + QueryBuilder + thenByNotes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'notes', Sort.asc); + }); + } + + QueryBuilder + thenByNotesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'notes', Sort.desc); + }); + } + + QueryBuilder + thenByScheduledDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'scheduledDate', Sort.asc); + }); + } + + QueryBuilder + thenByScheduledDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'scheduledDate', Sort.desc); + }); + } + + QueryBuilder + thenByServerId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.asc); + }); + } + + QueryBuilder + thenByServerIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serverId', Sort.desc); + }); + } + + QueryBuilder + thenByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.asc); + }); + } + + QueryBuilder + thenByUpdatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'updatedAt', Sort.desc); + }); + } + + QueryBuilder + thenByUserId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.asc); + }); + } + + QueryBuilder + thenByUserIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'userId', Sort.desc); + }); + } + + QueryBuilder + thenByWeek() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'week', Sort.asc); + }); + } + + QueryBuilder + thenByWeekDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'week', Sort.desc); + }); + } + + QueryBuilder + thenByXpEarned() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xpEarned', Sort.asc); + }); + } + + QueryBuilder + thenByXpEarnedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'xpEarned', Sort.desc); + }); + } +} + +extension WorkoutCollectionQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByCompletedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'completedAt'); + }); + } + + QueryBuilder + distinctByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'createdAt'); + }); + } + + QueryBuilder + distinctByCycleId({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'cycleId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByDay() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'day'); + }); + } + + QueryBuilder + distinctByExercisesJson({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'exercisesJson', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByIsDirty() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isDirty'); + }); + } + + QueryBuilder distinctByNotes( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'notes', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByScheduledDate() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'scheduledDate'); + }); + } + + QueryBuilder + distinctByServerId({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serverId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByUpdatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'updatedAt'); + }); + } + + QueryBuilder + distinctByUserId({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'userId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByWeek() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'week'); + }); + } + + QueryBuilder + distinctByXpEarned() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'xpEarned'); + }); + } +} + +extension WorkoutCollectionQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + completedAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'completedAt'); + }); + } + + QueryBuilder + createdAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'createdAt'); + }); + } + + QueryBuilder cycleIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'cycleId'); + }); + } + + QueryBuilder dayProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'day'); + }); + } + + QueryBuilder + exercisesJsonProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'exercisesJson'); + }); + } + + QueryBuilder isDirtyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isDirty'); + }); + } + + QueryBuilder notesProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'notes'); + }); + } + + QueryBuilder + scheduledDateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'scheduledDate'); + }); + } + + QueryBuilder + serverIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serverId'); + }); + } + + QueryBuilder + updatedAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'updatedAt'); + }); + } + + QueryBuilder userIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'userId'); + }); + } + + QueryBuilder weekProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'week'); + }); + } + + QueryBuilder xpEarnedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'xpEarned'); + }); + } +} diff --git a/lib/src/shared/data/remote/api_client.dart b/lib/src/shared/data/remote/api_client.dart new file mode 100644 index 0000000..e36c64a --- /dev/null +++ b/lib/src/shared/data/remote/api_client.dart @@ -0,0 +1,275 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:logger/logger.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +import '../../../core/constants/app_constants.dart'; + +class ApiClient { + late final Dio _dio; + final FlutterSecureStorage _storage; + final Logger _logger; + + ApiClient({ + FlutterSecureStorage? storage, + Logger? logger, + }) : _storage = storage ?? const FlutterSecureStorage(), + _logger = logger ?? Logger() { + _dio = Dio( + BaseOptions( + baseUrl: AppConstants.apiBaseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ), + ); + + // Add interceptors + _dio.interceptors.add( + PrettyDioLogger( + requestHeader: true, + requestBody: true, + responseBody: true, + responseHeader: false, + error: true, + compact: true, + ), + ); + + // Auth token interceptor + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await _storage.read(key: AppConstants.keyAuthToken); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + onError: (error, handler) async { + if (error.response?.statusCode == 401) { + _logger.w('Unauthorized - clearing token'); + await _storage.delete(key: AppConstants.keyAuthToken); + } + return handler.next(error); + }, + ), + ); + } + + // Authentication + Future> login(String email, String password) async { + try { + final response = await _dio.post( + ApiEndpoints.login, + data: { + 'identity': email, + 'password': password, + }, + ); + + final token = response.data['token']; + if (token != null) { + await _storage.write(key: AppConstants.keyAuthToken, value: token); + } + + return response.data; + } catch (e) { + _logger.e('Login failed', error: e); + rethrow; + } + } + + Future> register({ + required String email, + required String password, + required double bodyweight, + required Map inventorySettings, + }) async { + try { + final response = await _dio.post( + ApiEndpoints.register, + data: { + 'email': email, + 'password': password, + 'passwordConfirm': password, + 'xp': 0, + 'level': 1, + 'current_bodyweight': bodyweight, + 'inventory_settings': inventorySettings, + 'avatar_config': { + 'skin_tone': 'medium', + 'hair_style': 'short_01', + 'clothing': 'basic_tee', + 'unlocked_items': ['basic_tee'], + }, + }, + ); + return response.data; + } catch (e) { + _logger.e('Registration failed', error: e); + rethrow; + } + } + + Future logout() async { + await _storage.delete(key: AppConstants.keyAuthToken); + await _storage.delete(key: AppConstants.keyUserId); + } + + // Sync + Future> sync({ + required String lastSyncTimestamp, + required Map pushData, + }) async { + try { + final response = await _dio.post( + ApiEndpoints.sync, + data: { + 'last_sync_timestamp': lastSyncTimestamp, + 'push_data': pushData, + }, + ); + return response.data; + } catch (e) { + _logger.e('Sync failed', error: e); + rethrow; + } + } + + // Cycle Management + Future> createCycle( + Map trainingMaxes) async { + try { + final response = await _dio.post( + ApiEndpoints.cycleCreate, + data: {'training_maxes': trainingMaxes}, + ); + return response.data; + } catch (e) { + _logger.e('Create cycle failed', error: e); + rethrow; + } + } + + Future> finishCycle(String cycleId) async { + try { + final response = await _dio.post( + ApiEndpoints.cycleFinish, + data: {'cycle_id': cycleId}, + ); + return response.data; + } catch (e) { + _logger.e('Finish cycle failed', error: e); + rethrow; + } + } + + Future> getCurrentCycle() async { + try { + final response = await _dio.get(ApiEndpoints.cycleCurrent); + return response.data; + } catch (e) { + _logger.e('Get current cycle failed', error: e); + rethrow; + } + } + + // Stats + Future> getStatsHistory({ + required String exercise, + required String range, + }) async { + try { + final response = await _dio.get( + ApiEndpoints.statsHistory, + queryParameters: { + 'exercise': exercise, + 'range': range, + }, + ); + return response.data; + } catch (e) { + _logger.e('Get stats history failed', error: e); + rethrow; + } + } + + Future> getStatsSummary() async { + try { + final response = await _dio.get(ApiEndpoints.statsSummary); + return response.data; + } catch (e) { + _logger.e('Get stats summary failed', error: e); + rethrow; + } + } + + // Profile + Future updateBodyweight(double bodyweight) async { + try { + await _dio.patch( + ApiEndpoints.profileBodyweight, + data: {'bodyweight': bodyweight}, + ); + } catch (e) { + _logger.e('Update bodyweight failed', error: e); + rethrow; + } + } + + Future updateInventory(Map inventory) async { + try { + await _dio.patch( + ApiEndpoints.profileInventory, + data: inventory, + ); + } catch (e) { + _logger.e('Update inventory failed', error: e); + rethrow; + } + } + + Future updatePassword({ + required String userId, + required String oldPassword, + required String newPassword, + required String newPasswordConfirm, + }) async { + try { + // PocketBase erwartet oldPassword, password, passwordConfirm + await _dio.patch( + '${ApiEndpoints.userUpdate}/$userId', + data: { + 'oldPassword': oldPassword, + 'password': newPassword, + 'passwordConfirm': newPasswordConfirm, + }, + ); + } catch (e) { + _logger.e('Update password failed', error: e); + rethrow; + } + } + + Future deleteAccount(String userId) async { + try { + await _dio.delete('${ApiEndpoints.userDelete}/$userId'); + } catch (e) { + _logger.e('Delete account failed', error: e); + rethrow; + } + } + + Future resetProgress() async { + try { + await _dio.post(ApiEndpoints.profileReset); + } catch (e) { + _logger.e('Reset progress failed', error: e); + rethrow; + } + } +} diff --git a/lib/src/shared/data/remote/sync_service.dart b/lib/src/shared/data/remote/sync_service.dart new file mode 100644 index 0000000..0786282 --- /dev/null +++ b/lib/src/shared/data/remote/sync_service.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../../../../main.dart'; +import '../../../core/constants/app_constants.dart'; +import '../local/collections/user_collection.dart'; +import '../local/collections/cycle_collection.dart'; +import '../local/collections/workout_collection.dart'; +import 'api_client.dart'; +import '../repositories/user_repository.dart'; + +final syncServiceProvider = Provider((ref) { + final isar = ref.watch(isarProvider); + final apiClient = ref.watch(apiClientProvider); + return SyncService(isar: isar, apiClient: apiClient); +}); + +class SyncService { + final Isar isar; + final ApiClient apiClient; + final _storage = const FlutterSecureStorage(); + bool _isSyncing = false; + + SyncService({required this.isar, required this.apiClient}); + + Future sync() async { + if (_isSyncing) return; + _isSyncing = true; + + try { + debugPrint('🔄 Starting Sync...'); + + // --------------------------------------------------------- + // STEP 1: Sync Cycles First (Parents of Workouts) + // --------------------------------------------------------- + final dirtyCycles = + await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll(); + + for (var cycle in dirtyCycles) { + try { + if (cycle.serverId == null) { + // Create new cycle on server + debugPrint( + '📤 Pushing new cycle ${cycle.cycleNumber} to server...'); + + // Parse TMs safely + Map tmsMap = {}; + try { + final tms = jsonDecode(cycle.trainingMaxesJson); + tmsMap = Map.from( + tms.map((k, v) => MapEntry(k, (v as num).toDouble()))); + } catch (e) { + debugPrint('⚠️ Error parsing TMs for cycle ${cycle.id}: $e'); + // Default fallback if parsing fails + tmsMap = {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0}; + } + + final response = await apiClient.createCycle(tmsMap); + final newServerId = response['id']; + + await isar.writeTxn(() async { + // Update cycle with server ID + cycle.serverId = newServerId; + cycle.isDirty = false; + await isar.cycleCollections.put(cycle); + + // CRITICAL: Update all workouts that linked to the local ID of this cycle + // Since we stored 'local ID string' in cycleId for offline workouts + final oldLocalIdRef = cycle.id.toString(); + + final orphanWorkouts = await isar.workoutCollections + .filter() + .cycleIdEqualTo(oldLocalIdRef) + .findAll(); + + for (var w in orphanWorkouts) { + w.cycleId = newServerId; // Update link to valid server ID + w.isDirty = true; // Ensure it gets picked up in next step + await isar.workoutCollections.put(w); + debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId'); + } + }); + } else { + // Cycle already has server ID but marked dirty -> Update on server if needed + // For MVP we assume cycles are immutable except for status, skipping update logic to avoid complexity + await isar.writeTxn(() async { + cycle.isDirty = false; + await isar.cycleCollections.put(cycle); + }); + } + } catch (e) { + debugPrint('❌ Failed to sync cycle: $e'); + // We stop here because workouts depend on cycles. + return; + } + } + + // --------------------------------------------------------- + // STEP 2: Sync Workouts & User Stats + // --------------------------------------------------------- + + // 1. Gather local changes + final dirtyUser = + await isar.userCollections.filter().isDirtyEqualTo(true).findFirst(); + + final dirtyWorkouts = + await isar.workoutCollections.filter().isDirtyEqualTo(true).findAll(); + + if (dirtyUser == null && dirtyWorkouts.isEmpty) { + debugPrint('✅ Nothing to push.'); + } else { + // 2. Prepare Push Data + final pushData = { + 'workouts': dirtyWorkouts.where((w) { + // Filter out workouts that still don't have a valid cycle Server ID (e.g. if cycle sync failed) + // A valid PocketBase ID is 15 chars. A local ID is usually "1", "2". + // This is a heuristic check. + return w.cycleId.length > 5; + }).map((w) { + return { + 'id': w.serverId, + 'local_id': w.id, + 'cycle_id': w.cycleId, // Must be a Server ID + 'week': w.week, + 'day': w.day, + 'completed_at': w.completedAt?.toIso8601String(), + 'xp_earned': w.xpEarned, + 'notes': w.notes, + 'exercises': jsonDecode(w.exercisesJson), + }; + }).toList(), + 'user_stats': dirtyUser != null + ? { + 'xp': dirtyUser.xp, + 'level': dirtyUser.level, + 'current_bodyweight': dirtyUser.currentBodyweight, + } + : null, + }; + + // If we filtered out workouts, log it + if ((pushData['workouts'] as List).length < dirtyWorkouts.length) { + debugPrint( + '⚠️ Skipped some workouts because they lack a valid server cycle ID.'); + } + + // 3. Get Last Sync Timestamp + final lastSync = await _storage.read(key: AppConstants.keyLastSync); + + // 4. Call API + if ((pushData['workouts'] as List).isNotEmpty || + pushData['user_stats'] != null) { + debugPrint('📤 Pushing data...'); + final response = await apiClient.sync( + lastSyncTimestamp: lastSync ?? '', + pushData: pushData, + ); + + // 5. Process Response + await isar.writeTxn(() async { + // Update User + if (dirtyUser != null) { + dirtyUser.isDirty = false; + await isar.userCollections.put(dirtyUser); + } + + // Update pushed workouts (Clear dirty flags) + for (var w in dirtyWorkouts) { + // We assume success if no error thrown + // Ideally we match IDs from response, but for MVP optimistically clearing is okay + // providing we don't overwrite serverId if it was null. + // The server usually returns the new/updated records in 'pull_data' anyway. + w.isDirty = false; + await isar.workoutCollections.put(w); + } + + // Process Pulled Workouts (Updates from Server) + if (response['pull_data'] != null && + response['pull_data']['workouts'] != null) { + final pulledWorkouts = response['pull_data']['workouts'] as List; + for (var wJson in pulledWorkouts) { + final serverId = wJson['id']; + var workout = await isar.workoutCollections + .filter() + .serverIdEqualTo(serverId) + .findFirst(); + + workout ??= WorkoutCollection(); + + workout + ..serverId = serverId + ..cycleId = wJson['cycle_id'] + ..userId = wJson['user_id'] + ..week = wJson['week'] + ..day = wJson['day'] + ..completedAt = DateTime.tryParse(wJson['completed_at'] ?? '') + ..xpEarned = wJson['xp_earned'] ?? 0 + ..exercisesJson = jsonEncode(wJson['exercises']) + ..isDirty = false + ..updatedAt = DateTime.now(); + + await isar.workoutCollections.put(workout); + } + } + }); + + // 6. Save new Sync Timestamp + if (response['server_timestamp'] != null) { + await _storage.write( + key: AppConstants.keyLastSync, + value: response['server_timestamp'], + ); + } + } + } + + debugPrint('✅ Sync completed successfully'); + } catch (e) { + debugPrint('❌ Sync failed: $e'); + } finally { + _isSyncing = false; + } + } +} diff --git a/lib/src/shared/data/repositories/cycle_repository.dart b/lib/src/shared/data/repositories/cycle_repository.dart new file mode 100644 index 0000000..e16b63f --- /dev/null +++ b/lib/src/shared/data/repositories/cycle_repository.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'dart:convert'; + +import '../local/collections/cycle_collection.dart'; +import '../remote/api_client.dart'; +import '../../../../main.dart'; +import 'user_repository.dart'; +import '../../../core/constants/app_constants.dart'; +import '../local/collections/workout_collection.dart'; + +final cycleRepositoryProvider = Provider((ref) { + final isar = ref.watch(isarProvider); + final apiClient = ref.watch(apiClientProvider); + return CycleRepository(isar: isar, apiClient: apiClient); +}); + +class CycleRepository { + final Isar isar; + final ApiClient apiClient; + + CycleRepository({required this.isar, required this.apiClient}); + + Future getCurrentCycle() async { + return await isar.cycleCollections + .filter() + .isActiveEqualTo(true) + .findFirst(); + } + + Future> getAllCycles() async { + return await isar.cycleCollections.where().findAll(); + } + + Future createCycle(Map trainingMaxes) async { + try { + final currentCycle = await getCurrentCycle(); + if (currentCycle != null) { + currentCycle.isActive = false; + currentCycle.endDate = DateTime.now(); + await saveCycle(currentCycle); + } + + final allCycles = await getAllCycles(); + final nextNumber = allCycles.isEmpty + ? 1 + : allCycles + .map((c) => c.cycleNumber) + .reduce((a, b) => a > b ? a : b) + + 1; + + final userRepo = UserRepository(isar: isar, apiClient: ApiClient()); + final user = await userRepo.getLocalUser(); + + if (user == null) { + throw Exception('No user found for cycle creation'); + } + + final newCycle = CycleCollection() + ..userId = user.serverId ?? user.id.toString() + ..cycleNumber = nextNumber + ..startDate = DateTime.now() + ..isActive = true + ..trainingMaxesJson = jsonEncode(trainingMaxes) + ..isDirty = true; + + await saveCycle(newCycle); + + try { + final response = await apiClient.createCycle(trainingMaxes); + newCycle.serverId = response['id']; + newCycle.isDirty = false; + await saveCycle(newCycle); + } catch (e) {} + + return newCycle; + } catch (e, stackTrace) { + rethrow; + } + } + + Future finishCycle() async { + final currentCycle = await getCurrentCycle(); + if (currentCycle == null) { + throw Exception('No active cycle to finish'); + } + + final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString(); + + // --- FIX START: Vollständigkeitsprüfung --- + // Wir zählen, wie viele Workouts in Woche 1, 2 und 3 tatsächlich abgeschlossen wurden. + // Es müssen genau 9 sein (3 Wochen * 3 Tage). + final completedMainWorkouts = await isar.workoutCollections + .filter() + .weekLessThan( + 4) // Nur Woche 1-3 zählen (Deload Woche 4 ist optional für Finish) + .completedAtIsNotNull() // Nur abgeschlossene zählen + .group((q) => q + .cycleIdEqualTo(cycleIdRef) + .or() + .cycleIdEqualTo(currentCycle.id.toString())) + .count(); + + if (completedMainWorkouts < 9) { + final missing = 9 - completedMainWorkouts; + throw Exception( + 'Cycle incomplete! You still have $missing workouts left in the main phase (Weeks 1-3). Finish them before leveling up.'); + } + final currentTMs = + jsonDecode(currentCycle.trainingMaxesJson) as Map; + + final newTMs = { + 'squat': (currentTMs['squat'] as num?)?.toDouble() ?? 0.0, + 'pullup': (currentTMs['pullup'] as num?)?.toDouble() ?? 0.0, + 'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0, + }; + + // final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString(); + + final week3Workouts = await isar.workoutCollections + .filter() + .weekEqualTo(3) + .group((q) => q + .cycleIdEqualTo(cycleIdRef) + .or() + .cycleIdEqualTo(currentCycle.id.toString())) + .findAll(); + + bool checkSuccess(String exerciseId) { + for (var workout in week3Workouts) { + try { + final exercises = jsonDecode(workout.exercisesJson) as List; + for (var ex in exercises) { + if (ex['exerciseId'] == exerciseId) { + final sets = ex['sets'] as List; + for (var s in sets) { + if (s['isAmrap'] == true) { + final reps = s['repsActual'] as int? ?? 0; + if (reps >= 1) { + return true; + } + } + } + } + } + } catch (e) { + debugPrint('⚠️ Error checking lift success for $exerciseId: $e'); + } + } + return false; + } + + if (checkSuccess('squat')) { + newTMs['squat'] = newTMs['squat']! + AppConstants.lowerBodyIncrement; + debugPrint('✅ Squat Progress: TM increased'); + } else { + debugPrint('⚠️ Squat Stall: TM kept same'); + } + + if (checkSuccess('pullup')) { + newTMs['pullup'] = newTMs['pullup']! + AppConstants.upperBodyIncrement; + debugPrint('✅ Pullup Progress: TM increased'); + } else { + debugPrint('⚠️ Pullup Stall: TM kept same'); + } + + if (checkSuccess('dip')) { + newTMs['dip'] = newTMs['dip']! + AppConstants.upperBodyIncrement; + debugPrint('✅ Dip Progress: TM increased'); + } else { + debugPrint('⚠️ Dip Stall: TM kept same'); + } + + if (currentCycle.serverId != null) { + try { + await apiClient.finishCycle(currentCycle.serverId!); + } catch (e) { + // Fehler ignorieren, wird später gesynct + } + } + + return await createCycle(newTMs); + } + + Future saveCycle(CycleCollection cycle) async { + cycle.updatedAt = DateTime.now(); + await isar.writeTxn(() async { + await isar.cycleCollections.put(cycle); + }); + } + + Map getCurrentTrainingMaxes() { + final cycle = + isar.cycleCollections.filter().isActiveEqualTo(true).findFirstSync(); + + if (cycle != null) { + final tms = jsonDecode(cycle.trainingMaxesJson); + return { + 'squat': (tms['squat'] as num?)?.toDouble() ?? 0.0, + 'pullup': (tms['pullup'] as num?)?.toDouble() ?? 0.0, + 'dip': (tms['dip'] as num?)?.toDouble() ?? 0.0, + }; + } + + return {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0}; + } +} diff --git a/lib/src/shared/data/repositories/user_repository.dart b/lib/src/shared/data/repositories/user_repository.dart new file mode 100644 index 0000000..6ff570d --- /dev/null +++ b/lib/src/shared/data/repositories/user_repository.dart @@ -0,0 +1,253 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'dart:convert'; + +import '../local/collections/cycle_collection.dart'; +import '../local/collections/user_collection.dart'; +import '../local/collections/workout_collection.dart'; +import '../remote/api_client.dart'; +import '../../../../main.dart'; + +final userRepositoryProvider = Provider((ref) { + final isar = ref.watch(isarProvider); + final apiClient = ref.watch(apiClientProvider); + return UserRepository(isar: isar, apiClient: apiClient); +}); + +final apiClientProvider = Provider((ref) => ApiClient()); + +class UserRepository { + final Isar isar; + final ApiClient apiClient; + + UserRepository({required this.isar, required this.apiClient}); + + Future getLocalUser() async { + return await isar.userCollections.where().findFirst(); + } + + Future saveLocalUser(UserCollection user) async { + user.updatedAt = DateTime.now(); + await isar.writeTxn(() async { + await isar.userCollections.put(user); + }); + } + + Future updateXP(int xpToAdd) async { + final user = await getLocalUser(); + if (user != null) { + user.xp += xpToAdd; + user.isDirty = true; + await saveLocalUser(user); + } + } + + Future updateLevel(int newLevel) async { + final user = await getLocalUser(); + if (user != null) { + user.level = newLevel; + user.isDirty = true; + await saveLocalUser(user); + } + } + + Future updateBodyweight(double bodyweight) async { + final user = await getLocalUser(); + if (user != null) { + user.currentBodyweight = bodyweight; + user.isDirty = true; + await saveLocalUser(user); + + try { + await apiClient.updateBodyweight(bodyweight); + } catch (e) {} + } + } + + Future updateInventory(Map inventory) async { + final user = await getLocalUser(); + if (user != null) { + user.inventorySettingsJson = jsonEncode(inventory); + user.isDirty = true; + await saveLocalUser(user); + + try { + await apiClient.updateInventory(inventory); + } catch (e) {} + } + } + + Future login(String email, String password) async { + final response = await apiClient.login(email, password); + + final user = UserCollection() + ..serverId = response['record']['id'] + ..email = response['record']['email'] + ..xp = response['record']['xp'] ?? 0 + ..level = response['record']['level'] ?? 1 + ..currentBodyweight = + (response['record']['current_bodyweight'] ?? 70.0).toDouble() + ..inventorySettingsJson = + jsonEncode(response['record']['inventory_settings'] ?? {}) + ..avatarConfigJson = jsonEncode(response['record']['avatar_config'] ?? {}) + ..lastSyncAt = DateTime.now(); + + await saveLocalUser(user); + return user; + } + + Future register({ + required String email, + required String password, + required double bodyweight, + required Map inventorySettings, + }) async { + try { + final response = await apiClient.register( + email: email, + password: password, + bodyweight: bodyweight, + inventorySettings: inventorySettings, + ); + + final record = response['record'] ?? response; + + final user = UserCollection() + ..serverId = record['id']?.toString() + ..email = record['email']?.toString() ?? email + ..xp = (record['xp'] as num?)?.toInt() ?? 0 + ..level = (record['level'] as num?)?.toInt() ?? 1 + ..currentBodyweight = + (record['current_bodyweight'] as num?)?.toDouble() ?? bodyweight + ..inventorySettingsJson = + jsonEncode(record['inventory_settings'] ?? inventorySettings) + ..avatarConfigJson = jsonEncode(record['avatar_config'] ?? + { + 'skin_tone': 'medium', + 'hair_style': 'short_01', + 'clothing': 'basic_tee', + 'unlocked_items': ['basic_tee'], + }) + ..lastSyncAt = DateTime.now(); + + await saveLocalUser(user); + + try { + await apiClient.login(email, password); + } catch (e) {} + + return user; + } catch (e, stackTrace) { + rethrow; + } + } + + Future logout() async { + await apiClient.logout(); + await isar.writeTxn(() async { + await isar.userCollections.clear(); + }); + } + + Map getInventorySettings() { + final user = isar.userCollections.where().findFirstSync(); + if (user?.inventorySettingsJson != null) { + return jsonDecode(user!.inventorySettingsJson!); + } + return { + 'bar_weight': 20.0, + 'plates': [20, 20, 10, 10, 5, 5, 2.5, 2.5, 1.25, 1.25], + 'bands': [], + }; + } + + List getAvailablePlates() { + final inventory = getInventorySettings(); + final plates = inventory['plates'] as List?; + return plates?.map((e) => (e as num).toDouble()).toList() ?? []; + } + + double getBarWeight() { + final inventory = getInventorySettings(); + return (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0; + } + + Future changePassword(String oldPassword, String newPassword) async { + final user = await getLocalUser(); + if (user?.serverId != null) { + await apiClient.updatePassword( + userId: user!.serverId!, + oldPassword: oldPassword, + newPassword: newPassword, + newPasswordConfirm: newPassword, + ); + } else { + throw Exception('User not synced or offline'); + } + } + + Future deleteAccount() async { + final user = await getLocalUser(); + if (user?.serverId != null) { + await apiClient.deleteAccount(user!.serverId!); + } + // Lokal alles löschen + await logout(); + } + + // Future resetProgress() async { + // final user = await getLocalUser(); + // if (user != null) { + // // 1. User Stats reset + // user.xp = 0; + // user.level = 1; + // user.isDirty = true; + + // await isar.writeTxn(() async { + // await isar.userCollections.put(user); + + // // 2. Alle Cycles und Workouts löschen + // await isar.cycleCollections.clear(); + // await isar.workoutCollections.clear(); + // }); + + // // Sync anstoßen, um Server zu aktualisieren (User Stats) + // // Hinweis: Das Löschen der History auf dem Server erfordert ggf. separate Logik, + // // da der Sync aktuell nur "Updates" pusht, aber keine "Deletes" für Listen. + // // Für MVP reicht der lokale Reset + User Stats Update. + // } + // } + Future resetProgress() async { + final user = await getLocalUser(); + if (user != null) { + // 1. SERVER RESET (Zwingend zuerst!) + try { + // Wir versuchen, den Server zu bereinigen. + await apiClient.resetProgress(); + } catch (e) { + // Wenn das fehlschlägt (z.B. Offline), brechen wir ab. + // Ein lokaler Reset ohne Server-Reset führt sonst zu Daten-Chaos beim nächsten Sync. + throw Exception( + "Server connection required to reset progress. Please try again when online."); + } + + // 2. LOKALER RESET (Nur wenn Server erfolgreich war) + user.xp = 0; + user.level = 1; + + // Wichtig: Wir setzen isDirty auf FALSE. + // Der Server weiß schon Bescheid (durch den API Call oben). + // Wir müssen ihm nicht nochmal sagen, dass XP jetzt 0 ist. + user.isDirty = false; + + await isar.writeTxn(() async { + // User speichern + await isar.userCollections.put(user); + + // Alle lokalen Trainingsdaten löschen + await isar.cycleCollections.clear(); + await isar.workoutCollections.clear(); + }); + } + } +} diff --git a/lib/src/shared/data/repositories/workout_repository.dart b/lib/src/shared/data/repositories/workout_repository.dart new file mode 100644 index 0000000..2708001 --- /dev/null +++ b/lib/src/shared/data/repositories/workout_repository.dart @@ -0,0 +1,108 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'dart:convert'; + +import '../local/collections/workout_collection.dart'; +import '../remote/api_client.dart'; +import '../../../../main.dart'; +import 'user_repository.dart'; + +final workoutRepositoryProvider = Provider((ref) { + final isar = ref.watch(isarProvider); + final apiClient = ref.watch(apiClientProvider); + return WorkoutRepository(isar: isar, apiClient: apiClient); +}); + +class WorkoutRepository { + final Isar isar; + final ApiClient apiClient; + + WorkoutRepository({required this.isar, required this.apiClient}); + + Future> getAllWorkouts() async { + return await isar.workoutCollections.where().findAll(); + } + + Future> getWorkoutsForCycle(String cycleId) async { + return await isar.workoutCollections + .filter() + .cycleIdEqualTo(cycleId) + .findAll(); + } + + Future> getCompletedWorkouts(String userId) async { + return await isar.workoutCollections + .filter() + .userIdEqualTo(userId) + .completedAtIsNotNull() + .findAll(); + } + + Future saveWorkout(WorkoutCollection workout) async { + workout.updatedAt = DateTime.now(); + workout.isDirty = true; + await isar.writeTxn(() async { + await isar.workoutCollections.put(workout); + }); + } + + Future createWorkout({ + required String userId, + required String cycleId, + required int week, + required int day, + required String exercisesJson, + }) async { + final workout = WorkoutCollection() + ..userId = userId + ..cycleId = cycleId + ..week = week + ..day = day + ..exercisesJson = exercisesJson + ..scheduledDate = DateTime.now(); + + await saveWorkout(workout); + return workout; + } + + Future completeWorkout( + WorkoutCollection workout, { + required int xpEarned, + }) async { + workout.completedAt = DateTime.now(); + workout.xpEarned = xpEarned; + await saveWorkout(workout); + } + + // Future getWorkoutByWeekDay({ + // required String cycleId, + // required int week, + // required int day, + // }) async { + // return await isar.workoutCollections + // .filter() + // .cycleIdEqualTo(cycleId) + // .weekEqualTo(week) + // .dayEqualTo(day) + // .findFirst(); + // } + Future getWorkoutByWeekDay({ + required String cycleId, // Meist Server ID + String? localCycleId, // NEU: Backup Local ID + required int week, + required int day, + }) async { + return await isar.workoutCollections + .filter() + .weekEqualTo(week) + .dayEqualTo(day) + .group((q) { + // Wir suchen ENTWEDER nach der Server-ID ODER nach der lokalen ID + var query = q.cycleIdEqualTo(cycleId); + if (localCycleId != null) { + query = query.or().cycleIdEqualTo(localCycleId); + } + return query; + }).findFirst(); + } +} diff --git a/lib/src/shared/domain/entities/exercise.dart b/lib/src/shared/domain/entities/exercise.dart new file mode 100644 index 0000000..f58d3b7 --- /dev/null +++ b/lib/src/shared/domain/entities/exercise.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'workout_set.dart'; + +part 'exercise.freezed.dart'; +part 'exercise.g.dart'; + +@freezed +class Exercise with _$Exercise { + const factory Exercise({ + required String exerciseId, + required String exerciseName, + @Default(0.0) double bodyweightAtSession, + @Default([]) List sets, + }) = _Exercise; + + factory Exercise.fromJson(Map json) => + _$ExerciseFromJson(json); +} + diff --git a/lib/src/shared/domain/entities/exercise.freezed.dart b/lib/src/shared/domain/entities/exercise.freezed.dart new file mode 100644 index 0000000..db27c4c --- /dev/null +++ b/lib/src/shared/domain/entities/exercise.freezed.dart @@ -0,0 +1,226 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'exercise.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Exercise _$ExerciseFromJson(Map json) { + return _Exercise.fromJson(json); +} + +/// @nodoc +mixin _$Exercise { + String get exerciseId => throw _privateConstructorUsedError; + String get exerciseName => throw _privateConstructorUsedError; + double get bodyweightAtSession => throw _privateConstructorUsedError; + List get sets => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ExerciseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ExerciseCopyWith<$Res> { + factory $ExerciseCopyWith(Exercise value, $Res Function(Exercise) then) = + _$ExerciseCopyWithImpl<$Res, Exercise>; + @useResult + $Res call( + {String exerciseId, + String exerciseName, + double bodyweightAtSession, + List sets}); +} + +/// @nodoc +class _$ExerciseCopyWithImpl<$Res, $Val extends Exercise> + implements $ExerciseCopyWith<$Res> { + _$ExerciseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exerciseId = null, + Object? exerciseName = null, + Object? bodyweightAtSession = null, + Object? sets = null, + }) { + return _then(_value.copyWith( + exerciseId: null == exerciseId + ? _value.exerciseId + : exerciseId // ignore: cast_nullable_to_non_nullable + as String, + exerciseName: null == exerciseName + ? _value.exerciseName + : exerciseName // ignore: cast_nullable_to_non_nullable + as String, + bodyweightAtSession: null == bodyweightAtSession + ? _value.bodyweightAtSession + : bodyweightAtSession // ignore: cast_nullable_to_non_nullable + as double, + sets: null == sets + ? _value.sets + : sets // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ExerciseImplCopyWith<$Res> + implements $ExerciseCopyWith<$Res> { + factory _$$ExerciseImplCopyWith( + _$ExerciseImpl value, $Res Function(_$ExerciseImpl) then) = + __$$ExerciseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String exerciseId, + String exerciseName, + double bodyweightAtSession, + List sets}); +} + +/// @nodoc +class __$$ExerciseImplCopyWithImpl<$Res> + extends _$ExerciseCopyWithImpl<$Res, _$ExerciseImpl> + implements _$$ExerciseImplCopyWith<$Res> { + __$$ExerciseImplCopyWithImpl( + _$ExerciseImpl _value, $Res Function(_$ExerciseImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exerciseId = null, + Object? exerciseName = null, + Object? bodyweightAtSession = null, + Object? sets = null, + }) { + return _then(_$ExerciseImpl( + exerciseId: null == exerciseId + ? _value.exerciseId + : exerciseId // ignore: cast_nullable_to_non_nullable + as String, + exerciseName: null == exerciseName + ? _value.exerciseName + : exerciseName // ignore: cast_nullable_to_non_nullable + as String, + bodyweightAtSession: null == bodyweightAtSession + ? _value.bodyweightAtSession + : bodyweightAtSession // ignore: cast_nullable_to_non_nullable + as double, + sets: null == sets + ? _value._sets + : sets // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ExerciseImpl implements _Exercise { + const _$ExerciseImpl( + {required this.exerciseId, + required this.exerciseName, + this.bodyweightAtSession = 0.0, + final List sets = const []}) + : _sets = sets; + + factory _$ExerciseImpl.fromJson(Map json) => + _$$ExerciseImplFromJson(json); + + @override + final String exerciseId; + @override + final String exerciseName; + @override + @JsonKey() + final double bodyweightAtSession; + final List _sets; + @override + @JsonKey() + List get sets { + if (_sets is EqualUnmodifiableListView) return _sets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_sets); + } + + @override + String toString() { + return 'Exercise(exerciseId: $exerciseId, exerciseName: $exerciseName, bodyweightAtSession: $bodyweightAtSession, sets: $sets)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ExerciseImpl && + (identical(other.exerciseId, exerciseId) || + other.exerciseId == exerciseId) && + (identical(other.exerciseName, exerciseName) || + other.exerciseName == exerciseName) && + (identical(other.bodyweightAtSession, bodyweightAtSession) || + other.bodyweightAtSession == bodyweightAtSession) && + const DeepCollectionEquality().equals(other._sets, _sets)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, exerciseId, exerciseName, + bodyweightAtSession, const DeepCollectionEquality().hash(_sets)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith => + __$$ExerciseImplCopyWithImpl<_$ExerciseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ExerciseImplToJson( + this, + ); + } +} + +abstract class _Exercise implements Exercise { + const factory _Exercise( + {required final String exerciseId, + required final String exerciseName, + final double bodyweightAtSession, + final List sets}) = _$ExerciseImpl; + + factory _Exercise.fromJson(Map json) = + _$ExerciseImpl.fromJson; + + @override + String get exerciseId; + @override + String get exerciseName; + @override + double get bodyweightAtSession; + @override + List get sets; + @override + @JsonKey(ignore: true) + _$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/shared/domain/entities/exercise.g.dart b/lib/src/shared/domain/entities/exercise.g.dart new file mode 100644 index 0000000..8dcde41 --- /dev/null +++ b/lib/src/shared/domain/entities/exercise.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'exercise.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ExerciseImpl _$$ExerciseImplFromJson(Map json) => + _$ExerciseImpl( + exerciseId: json['exerciseId'] as String, + exerciseName: json['exerciseName'] as String, + bodyweightAtSession: + (json['bodyweightAtSession'] as num?)?.toDouble() ?? 0.0, + sets: (json['sets'] as List?) + ?.map((e) => WorkoutSet.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$$ExerciseImplToJson(_$ExerciseImpl instance) => + { + 'exerciseId': instance.exerciseId, + 'exerciseName': instance.exerciseName, + 'bodyweightAtSession': instance.bodyweightAtSession, + 'sets': instance.sets, + }; diff --git a/lib/src/shared/domain/entities/training_maxes.dart b/lib/src/shared/domain/entities/training_maxes.dart new file mode 100644 index 0000000..8f4296f --- /dev/null +++ b/lib/src/shared/domain/entities/training_maxes.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'training_maxes.freezed.dart'; +part 'training_maxes.g.dart'; + +@freezed +class TrainingMaxes with _$TrainingMaxes { + const factory TrainingMaxes({ + @Default(0.0) double squat, + @Default(0.0) double pullup, + @Default(0.0) double dip, + }) = _TrainingMaxes; + + factory TrainingMaxes.fromJson(Map json) => + _$TrainingMaxesFromJson(json); +} + diff --git a/lib/src/shared/domain/entities/training_maxes.freezed.dart b/lib/src/shared/domain/entities/training_maxes.freezed.dart new file mode 100644 index 0000000..1d92794 --- /dev/null +++ b/lib/src/shared/domain/entities/training_maxes.freezed.dart @@ -0,0 +1,190 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'training_maxes.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +TrainingMaxes _$TrainingMaxesFromJson(Map json) { + return _TrainingMaxes.fromJson(json); +} + +/// @nodoc +mixin _$TrainingMaxes { + double get squat => throw _privateConstructorUsedError; + double get pullup => throw _privateConstructorUsedError; + double get dip => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $TrainingMaxesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TrainingMaxesCopyWith<$Res> { + factory $TrainingMaxesCopyWith( + TrainingMaxes value, $Res Function(TrainingMaxes) then) = + _$TrainingMaxesCopyWithImpl<$Res, TrainingMaxes>; + @useResult + $Res call({double squat, double pullup, double dip}); +} + +/// @nodoc +class _$TrainingMaxesCopyWithImpl<$Res, $Val extends TrainingMaxes> + implements $TrainingMaxesCopyWith<$Res> { + _$TrainingMaxesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? squat = null, + Object? pullup = null, + Object? dip = null, + }) { + return _then(_value.copyWith( + squat: null == squat + ? _value.squat + : squat // ignore: cast_nullable_to_non_nullable + as double, + pullup: null == pullup + ? _value.pullup + : pullup // ignore: cast_nullable_to_non_nullable + as double, + dip: null == dip + ? _value.dip + : dip // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TrainingMaxesImplCopyWith<$Res> + implements $TrainingMaxesCopyWith<$Res> { + factory _$$TrainingMaxesImplCopyWith( + _$TrainingMaxesImpl value, $Res Function(_$TrainingMaxesImpl) then) = + __$$TrainingMaxesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({double squat, double pullup, double dip}); +} + +/// @nodoc +class __$$TrainingMaxesImplCopyWithImpl<$Res> + extends _$TrainingMaxesCopyWithImpl<$Res, _$TrainingMaxesImpl> + implements _$$TrainingMaxesImplCopyWith<$Res> { + __$$TrainingMaxesImplCopyWithImpl( + _$TrainingMaxesImpl _value, $Res Function(_$TrainingMaxesImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? squat = null, + Object? pullup = null, + Object? dip = null, + }) { + return _then(_$TrainingMaxesImpl( + squat: null == squat + ? _value.squat + : squat // ignore: cast_nullable_to_non_nullable + as double, + pullup: null == pullup + ? _value.pullup + : pullup // ignore: cast_nullable_to_non_nullable + as double, + dip: null == dip + ? _value.dip + : dip // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TrainingMaxesImpl implements _TrainingMaxes { + const _$TrainingMaxesImpl( + {this.squat = 0.0, this.pullup = 0.0, this.dip = 0.0}); + + factory _$TrainingMaxesImpl.fromJson(Map json) => + _$$TrainingMaxesImplFromJson(json); + + @override + @JsonKey() + final double squat; + @override + @JsonKey() + final double pullup; + @override + @JsonKey() + final double dip; + + @override + String toString() { + return 'TrainingMaxes(squat: $squat, pullup: $pullup, dip: $dip)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TrainingMaxesImpl && + (identical(other.squat, squat) || other.squat == squat) && + (identical(other.pullup, pullup) || other.pullup == pullup) && + (identical(other.dip, dip) || other.dip == dip)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, squat, pullup, dip); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith => + __$$TrainingMaxesImplCopyWithImpl<_$TrainingMaxesImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TrainingMaxesImplToJson( + this, + ); + } +} + +abstract class _TrainingMaxes implements TrainingMaxes { + const factory _TrainingMaxes( + {final double squat, + final double pullup, + final double dip}) = _$TrainingMaxesImpl; + + factory _TrainingMaxes.fromJson(Map json) = + _$TrainingMaxesImpl.fromJson; + + @override + double get squat; + @override + double get pullup; + @override + double get dip; + @override + @JsonKey(ignore: true) + _$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/shared/domain/entities/training_maxes.g.dart b/lib/src/shared/domain/entities/training_maxes.g.dart new file mode 100644 index 0000000..f391001 --- /dev/null +++ b/lib/src/shared/domain/entities/training_maxes.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'training_maxes.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TrainingMaxesImpl _$$TrainingMaxesImplFromJson(Map json) => + _$TrainingMaxesImpl( + squat: (json['squat'] as num?)?.toDouble() ?? 0.0, + pullup: (json['pullup'] as num?)?.toDouble() ?? 0.0, + dip: (json['dip'] as num?)?.toDouble() ?? 0.0, + ); + +Map _$$TrainingMaxesImplToJson(_$TrainingMaxesImpl instance) => + { + 'squat': instance.squat, + 'pullup': instance.pullup, + 'dip': instance.dip, + }; diff --git a/lib/src/shared/domain/entities/workout_set.dart b/lib/src/shared/domain/entities/workout_set.dart new file mode 100644 index 0000000..a8a8700 --- /dev/null +++ b/lib/src/shared/domain/entities/workout_set.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'workout_set.freezed.dart'; +part 'workout_set.g.dart'; + +@freezed +class WorkoutSet with _$WorkoutSet { + const factory WorkoutSet({ + @Default(1) int setNumber, + @Default(0) int targetPercentage, + @Default(0.0) double targetWeightTotal, + @Default(0.0) double plateWeight, + @Default(0) int repsTarget, + @Default(0) int repsActual, + @Default(false) bool isAmrap, + @Default(false) bool completed, + int? rpe, + }) = _WorkoutSet; + + factory WorkoutSet.fromJson(Map json) => + _$WorkoutSetFromJson(json); +} + diff --git a/lib/src/shared/domain/entities/workout_set.freezed.dart b/lib/src/shared/domain/entities/workout_set.freezed.dart new file mode 100644 index 0000000..7040af7 --- /dev/null +++ b/lib/src/shared/domain/entities/workout_set.freezed.dart @@ -0,0 +1,340 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'workout_set.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +WorkoutSet _$WorkoutSetFromJson(Map json) { + return _WorkoutSet.fromJson(json); +} + +/// @nodoc +mixin _$WorkoutSet { + int get setNumber => throw _privateConstructorUsedError; + int get targetPercentage => throw _privateConstructorUsedError; + double get targetWeightTotal => throw _privateConstructorUsedError; + double get plateWeight => throw _privateConstructorUsedError; + int get repsTarget => throw _privateConstructorUsedError; + int get repsActual => throw _privateConstructorUsedError; + bool get isAmrap => throw _privateConstructorUsedError; + bool get completed => throw _privateConstructorUsedError; + int? get rpe => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $WorkoutSetCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $WorkoutSetCopyWith<$Res> { + factory $WorkoutSetCopyWith( + WorkoutSet value, $Res Function(WorkoutSet) then) = + _$WorkoutSetCopyWithImpl<$Res, WorkoutSet>; + @useResult + $Res call( + {int setNumber, + int targetPercentage, + double targetWeightTotal, + double plateWeight, + int repsTarget, + int repsActual, + bool isAmrap, + bool completed, + int? rpe}); +} + +/// @nodoc +class _$WorkoutSetCopyWithImpl<$Res, $Val extends WorkoutSet> + implements $WorkoutSetCopyWith<$Res> { + _$WorkoutSetCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? setNumber = null, + Object? targetPercentage = null, + Object? targetWeightTotal = null, + Object? plateWeight = null, + Object? repsTarget = null, + Object? repsActual = null, + Object? isAmrap = null, + Object? completed = null, + Object? rpe = freezed, + }) { + return _then(_value.copyWith( + setNumber: null == setNumber + ? _value.setNumber + : setNumber // ignore: cast_nullable_to_non_nullable + as int, + targetPercentage: null == targetPercentage + ? _value.targetPercentage + : targetPercentage // ignore: cast_nullable_to_non_nullable + as int, + targetWeightTotal: null == targetWeightTotal + ? _value.targetWeightTotal + : targetWeightTotal // ignore: cast_nullable_to_non_nullable + as double, + plateWeight: null == plateWeight + ? _value.plateWeight + : plateWeight // ignore: cast_nullable_to_non_nullable + as double, + repsTarget: null == repsTarget + ? _value.repsTarget + : repsTarget // ignore: cast_nullable_to_non_nullable + as int, + repsActual: null == repsActual + ? _value.repsActual + : repsActual // ignore: cast_nullable_to_non_nullable + as int, + isAmrap: null == isAmrap + ? _value.isAmrap + : isAmrap // ignore: cast_nullable_to_non_nullable + as bool, + completed: null == completed + ? _value.completed + : completed // ignore: cast_nullable_to_non_nullable + as bool, + rpe: freezed == rpe + ? _value.rpe + : rpe // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$WorkoutSetImplCopyWith<$Res> + implements $WorkoutSetCopyWith<$Res> { + factory _$$WorkoutSetImplCopyWith( + _$WorkoutSetImpl value, $Res Function(_$WorkoutSetImpl) then) = + __$$WorkoutSetImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int setNumber, + int targetPercentage, + double targetWeightTotal, + double plateWeight, + int repsTarget, + int repsActual, + bool isAmrap, + bool completed, + int? rpe}); +} + +/// @nodoc +class __$$WorkoutSetImplCopyWithImpl<$Res> + extends _$WorkoutSetCopyWithImpl<$Res, _$WorkoutSetImpl> + implements _$$WorkoutSetImplCopyWith<$Res> { + __$$WorkoutSetImplCopyWithImpl( + _$WorkoutSetImpl _value, $Res Function(_$WorkoutSetImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? setNumber = null, + Object? targetPercentage = null, + Object? targetWeightTotal = null, + Object? plateWeight = null, + Object? repsTarget = null, + Object? repsActual = null, + Object? isAmrap = null, + Object? completed = null, + Object? rpe = freezed, + }) { + return _then(_$WorkoutSetImpl( + setNumber: null == setNumber + ? _value.setNumber + : setNumber // ignore: cast_nullable_to_non_nullable + as int, + targetPercentage: null == targetPercentage + ? _value.targetPercentage + : targetPercentage // ignore: cast_nullable_to_non_nullable + as int, + targetWeightTotal: null == targetWeightTotal + ? _value.targetWeightTotal + : targetWeightTotal // ignore: cast_nullable_to_non_nullable + as double, + plateWeight: null == plateWeight + ? _value.plateWeight + : plateWeight // ignore: cast_nullable_to_non_nullable + as double, + repsTarget: null == repsTarget + ? _value.repsTarget + : repsTarget // ignore: cast_nullable_to_non_nullable + as int, + repsActual: null == repsActual + ? _value.repsActual + : repsActual // ignore: cast_nullable_to_non_nullable + as int, + isAmrap: null == isAmrap + ? _value.isAmrap + : isAmrap // ignore: cast_nullable_to_non_nullable + as bool, + completed: null == completed + ? _value.completed + : completed // ignore: cast_nullable_to_non_nullable + as bool, + rpe: freezed == rpe + ? _value.rpe + : rpe // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$WorkoutSetImpl implements _WorkoutSet { + const _$WorkoutSetImpl( + {this.setNumber = 1, + this.targetPercentage = 0, + this.targetWeightTotal = 0.0, + this.plateWeight = 0.0, + this.repsTarget = 0, + this.repsActual = 0, + this.isAmrap = false, + this.completed = false, + this.rpe}); + + factory _$WorkoutSetImpl.fromJson(Map json) => + _$$WorkoutSetImplFromJson(json); + + @override + @JsonKey() + final int setNumber; + @override + @JsonKey() + final int targetPercentage; + @override + @JsonKey() + final double targetWeightTotal; + @override + @JsonKey() + final double plateWeight; + @override + @JsonKey() + final int repsTarget; + @override + @JsonKey() + final int repsActual; + @override + @JsonKey() + final bool isAmrap; + @override + @JsonKey() + final bool completed; + @override + final int? rpe; + + @override + String toString() { + return 'WorkoutSet(setNumber: $setNumber, targetPercentage: $targetPercentage, targetWeightTotal: $targetWeightTotal, plateWeight: $plateWeight, repsTarget: $repsTarget, repsActual: $repsActual, isAmrap: $isAmrap, completed: $completed, rpe: $rpe)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$WorkoutSetImpl && + (identical(other.setNumber, setNumber) || + other.setNumber == setNumber) && + (identical(other.targetPercentage, targetPercentage) || + other.targetPercentage == targetPercentage) && + (identical(other.targetWeightTotal, targetWeightTotal) || + other.targetWeightTotal == targetWeightTotal) && + (identical(other.plateWeight, plateWeight) || + other.plateWeight == plateWeight) && + (identical(other.repsTarget, repsTarget) || + other.repsTarget == repsTarget) && + (identical(other.repsActual, repsActual) || + other.repsActual == repsActual) && + (identical(other.isAmrap, isAmrap) || other.isAmrap == isAmrap) && + (identical(other.completed, completed) || + other.completed == completed) && + (identical(other.rpe, rpe) || other.rpe == rpe)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + setNumber, + targetPercentage, + targetWeightTotal, + plateWeight, + repsTarget, + repsActual, + isAmrap, + completed, + rpe); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith => + __$$WorkoutSetImplCopyWithImpl<_$WorkoutSetImpl>(this, _$identity); + + @override + Map toJson() { + return _$$WorkoutSetImplToJson( + this, + ); + } +} + +abstract class _WorkoutSet implements WorkoutSet { + const factory _WorkoutSet( + {final int setNumber, + final int targetPercentage, + final double targetWeightTotal, + final double plateWeight, + final int repsTarget, + final int repsActual, + final bool isAmrap, + final bool completed, + final int? rpe}) = _$WorkoutSetImpl; + + factory _WorkoutSet.fromJson(Map json) = + _$WorkoutSetImpl.fromJson; + + @override + int get setNumber; + @override + int get targetPercentage; + @override + double get targetWeightTotal; + @override + double get plateWeight; + @override + int get repsTarget; + @override + int get repsActual; + @override + bool get isAmrap; + @override + bool get completed; + @override + int? get rpe; + @override + @JsonKey(ignore: true) + _$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/shared/domain/entities/workout_set.g.dart b/lib/src/shared/domain/entities/workout_set.g.dart new file mode 100644 index 0000000..c3d2b5c --- /dev/null +++ b/lib/src/shared/domain/entities/workout_set.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'workout_set.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$WorkoutSetImpl _$$WorkoutSetImplFromJson(Map json) => + _$WorkoutSetImpl( + setNumber: (json['setNumber'] as num?)?.toInt() ?? 1, + targetPercentage: (json['targetPercentage'] as num?)?.toInt() ?? 0, + targetWeightTotal: (json['targetWeightTotal'] as num?)?.toDouble() ?? 0.0, + plateWeight: (json['plateWeight'] as num?)?.toDouble() ?? 0.0, + repsTarget: (json['repsTarget'] as num?)?.toInt() ?? 0, + repsActual: (json['repsActual'] as num?)?.toInt() ?? 0, + isAmrap: json['isAmrap'] as bool? ?? false, + completed: json['completed'] as bool? ?? false, + rpe: (json['rpe'] as num?)?.toInt(), + ); + +Map _$$WorkoutSetImplToJson(_$WorkoutSetImpl instance) => + { + 'setNumber': instance.setNumber, + 'targetPercentage': instance.targetPercentage, + 'targetWeightTotal': instance.targetWeightTotal, + 'plateWeight': instance.plateWeight, + 'repsTarget': instance.repsTarget, + 'repsActual': instance.repsActual, + 'isAmrap': instance.isAmrap, + 'completed': instance.completed, + 'rpe': instance.rpe, + }; diff --git a/lib/src/shared/domain/logic/plate_calculator.dart b/lib/src/shared/domain/logic/plate_calculator.dart new file mode 100644 index 0000000..5ddb1f4 --- /dev/null +++ b/lib/src/shared/domain/logic/plate_calculator.dart @@ -0,0 +1,343 @@ +// import 'dart:math'; + +// class PlateLoadResult { +// final bool success; +// final List plateConfiguration; +// final String? bandAssistance; // Name of the band needed +// final double totalAchieved; +// final String message; + +// PlateLoadResult({ +// required this.success, +// required this.plateConfiguration, +// this.bandAssistance, +// required this.totalAchieved, +// this.message = '', +// }); +// } + +// class PlateCalculator { +// /// Calculate plate loading for a target weight +// /// +// /// [targetWeight]: Total weight to achieve +// /// [barWeight]: Weight of the bar (or Bodyweight for calisthenics) +// /// [availablePlates]: List of available plate weights +// /// [availableBands]: Map of Band Name -> Resistance in KG +// /// [isTwoSided]: true for barbell, false for dip belt +// static PlateLoadResult calculate({ +// required double targetWeight, +// required double barWeight, +// required List availablePlates, +// Map availableBands = const {}, +// required bool isTwoSided, +// }) { +// double needed = targetWeight - barWeight; + +// if (needed < 0 && !isTwoSided) { +// final deficit = needed.abs(); +// String? bestBand; +// double minDiff = double.infinity; + +// availableBands.forEach((name, resistance) { +// final diff = (resistance - deficit).abs(); +// if (diff < minDiff) { +// minDiff = diff; +// bestBand = name; +// } +// }); + +// if (bestBand != null) { +// return PlateLoadResult( +// success: true, +// plateConfiguration: [], +// bandAssistance: bestBand, +// totalAchieved: targetWeight, +// message: 'Use $bestBand band for assistance', +// ); +// } + +// return PlateLoadResult( +// success: false, +// plateConfiguration: [], +// totalAchieved: barWeight, +// message: 'Need assistance but no bands available', +// ); +// } + +// if (needed <= 0.1) { +// return PlateLoadResult( +// success: true, +// plateConfiguration: [], +// totalAchieved: barWeight, +// message: isTwoSided ? 'Bar only' : 'Bodyweight Only', +// ); +// } + +// final sortedPlates = List.from(availablePlates) +// ..sort((a, b) => b.compareTo(a)); + +// if (sortedPlates.isEmpty) { +// return PlateLoadResult( +// success: false, +// plateConfiguration: [], +// totalAchieved: barWeight, +// message: 'No plates available', +// ); +// } + +// // ROUNDING LOGIC: +// // We must round the needed weight to the nearest increment of the smallest plate. +// // Example: Needed 9.7kg, Smallest plate 1.25kg -> Round to 10.0kg (8 * 1.25). +// final smallestPlate = sortedPlates.last; +// final targetPerSideRaw = isTwoSided ? needed / 2 : needed; + +// final roundedPerSide = +// (targetPerSideRaw / smallestPlate).round() * smallestPlate; + +// if (roundedPerSide <= 0.001) { +// return PlateLoadResult( +// success: true, +// plateConfiguration: [], +// totalAchieved: barWeight, +// message: isTwoSided ? 'Bar only' : 'Bodyweight Only', +// ); +// } + +// final result = _greedyFit(roundedPerSide, sortedPlates); + +// if (!result.success) { +// return PlateLoadResult( +// success: false, +// plateConfiguration: [], +// totalAchieved: barWeight, +// message: 'Cannot achieve target with available plates', +// ); +// } + +// final totalLoaded = isTwoSided +// ? barWeight + (result.totalWeight * 2) +// : barWeight + result.totalWeight; + +// return PlateLoadResult( +// success: true, +// plateConfiguration: result.plates, +// totalAchieved: totalLoaded, +// ); +// } + +// static _FitResult _greedyFit(double target, List plates) { +// final loaded = []; +// var remaining = target; + +// const epsilon = 0.01; + +// for (final plate in plates) { +// while (remaining >= plate - epsilon) { +// loaded.add(plate); +// remaining -= plate; +// } +// } + +// final success = remaining.abs() < epsilon; +// return _FitResult( +// success: success, +// plates: loaded, +// totalWeight: loaded.fold(0.0, (sum, p) => sum + p), +// ); +// } +// } + +// class _FitResult { +// final bool success; +// final List plates; +// final double totalWeight; + +// _FitResult({ +// required this.success, +// required this.plates, +// required this.totalWeight, +// }); +// } +import 'dart:math'; + +class PlateLoadResult { + final bool success; + final List plateConfiguration; + final String? bandAssistance; // Name of the band needed + final double totalAchieved; + final String message; + + PlateLoadResult({ + required this.success, + required this.plateConfiguration, + this.bandAssistance, + required this.totalAchieved, + this.message = '', + }); +} + +class PlateCalculator { + /// Calculate plate loading for a target weight + /// + /// [targetWeight]: Total weight to achieve + /// [barWeight]: Weight of the bar (or Bodyweight for calisthenics) + /// [availablePlates]: List of available plate weights + /// [availableBands]: Map of Band Name -> Resistance in KG + /// [isTwoSided]: true for barbell, false for dip belt + static PlateLoadResult calculate({ + required double targetWeight, + required double barWeight, + required List availablePlates, + Map availableBands = const {}, + required bool isTwoSided, + }) { + double needed = targetWeight - barWeight; + + // 1. Handle Assistance (Negative weight needed) + if (needed < 0 && !isTwoSided) { + final deficit = needed.abs(); + String? bestBand; + double closestResistance = 0.0; + double minDiff = double.infinity; + + // Finde das am besten passende Band + availableBands.forEach((name, resistance) { + final diff = (resistance - deficit).abs(); + if (diff < minDiff) { + minDiff = diff; + bestBand = name; + closestResistance = resistance; + } + }); + + // SMART FALLBACK LOGIK: + // Prüfen, ob "Kein Band" (Bodyweight) näher am Ziel liegt als das "Beste Band". + // Beispiel: Benötigt 2kg Support. Kleinstes Band 10kg. + // - Fehler mit Band: |10 - 2| = 8kg zu viel Hilfe. + // - Fehler ohne Band: 2kg zu wenig Hilfe. + // -> Da 2kg < 8kg, ist Bodyweight präziser (und trainingstechnisch besser als viel zu leicht). + + final deviationWithBand = minDiff; // Abweichung bei Band-Nutzung + final deviationWithBW = deficit; // Abweichung bei Bodyweight (0 Support) + + // Wir erlauben Bodyweight auch, wenn der Defizit extrem klein ist (< 1kg), + // da Bänder selten so fein abgestuft sind. + if (bestBand == null || + deviationWithBW <= deviationWithBand || + deficit < 1.0) { + return PlateLoadResult( + success: true, + plateConfiguration: [], + totalAchieved: barWeight, + message: 'Bodyweight Only (Closer to target than band)', + ); + } + + // Band gefunden und es ist sinnvoll + return PlateLoadResult( + success: true, + plateConfiguration: [], + bandAssistance: bestBand, + // Wir geben hier das echte erreichte Gewicht an (Körpergewicht - Bandstärke) + totalAchieved: barWeight - closestResistance, + message: 'Use $bestBand band for assistance', + ); + } + + // 2. Handle Added Weight (Plates) + // Check if we effectively need 0 weight (with small tolerance) + if (needed <= 0.1) { + return PlateLoadResult( + success: true, + plateConfiguration: [], + totalAchieved: barWeight, + message: isTwoSided ? 'Bar only' : 'Bodyweight Only', + ); + } + + // Sort plates descending to find smallest plate later + final sortedPlates = List.from(availablePlates) + ..sort((a, b) => b.compareTo(a)); + + if (sortedPlates.isEmpty) { + return PlateLoadResult( + success: false, + plateConfiguration: [], + totalAchieved: barWeight, + message: 'No plates available', + ); + } + + // ROUNDING LOGIC (wie vorher besprochen) + final smallestPlate = sortedPlates.last; + final targetPerSideRaw = isTwoSided ? needed / 2 : needed; + + // Round to nearest smallest plate + final roundedPerSide = + (targetPerSideRaw / smallestPlate).round() * smallestPlate; + + if (roundedPerSide <= 0.001) { + return PlateLoadResult( + success: true, + plateConfiguration: [], + totalAchieved: barWeight, + message: isTwoSided ? 'Bar only' : 'Bodyweight Only', + ); + } + + // Try to fit the ROUNDED weight + final result = _greedyFit(roundedPerSide, sortedPlates); + + if (!result.success) { + return PlateLoadResult( + success: false, + plateConfiguration: [], + totalAchieved: barWeight, + message: 'Cannot achieve target with available plates', + ); + } + + final totalLoaded = isTwoSided + ? barWeight + (result.totalWeight * 2) + : barWeight + result.totalWeight; + + return PlateLoadResult( + success: true, + plateConfiguration: result.plates, + totalAchieved: totalLoaded, + ); + } + + /// Greedy algorithm to fit plates + static _FitResult _greedyFit(double target, List plates) { + final loaded = []; + var remaining = target; + const epsilon = 0.01; + + for (final plate in plates) { + while (remaining >= plate - epsilon) { + loaded.add(plate); + remaining -= plate; + } + } + + final success = remaining.abs() < epsilon; + return _FitResult( + success: success, + plates: loaded, + totalWeight: loaded.fold(0.0, (sum, p) => sum + p), + ); + } +} + +class _FitResult { + final bool success; + final List plates; + final double totalWeight; + + _FitResult({ + required this.success, + required this.plates, + required this.totalWeight, + }); +} diff --git a/lib/src/shared/domain/logic/wendler_calculator.dart b/lib/src/shared/domain/logic/wendler_calculator.dart new file mode 100644 index 0000000..f430394 --- /dev/null +++ b/lib/src/shared/domain/logic/wendler_calculator.dart @@ -0,0 +1,106 @@ +import 'dart:math'; +import '../entities/workout_set.dart'; +import '../../../core/constants/app_constants.dart'; + +enum ExerciseType { squat, pullup, dip } + +class WendlerCalculator { + static const Map> weekPercentages = { + 1: [0.65, 0.75, 0.85], + 2: [0.70, 0.80, 0.90], + 3: [0.75, 0.85, 0.95], + 4: [0.40, 0.50, 0.60], + }; + + static const Map> weekReps = { + 1: [5, 5, 5], + 2: [3, 3, 3], + 3: [5, 3, 1], + 4: [5, 5, 5], + }; + + static List generateSets({ + required int week, + required double trainingMax, + required ExerciseType exerciseType, + required double currentBodyweight, + }) { + final percentages = weekPercentages[week]!; + final reps = weekReps[week]!; + final sets = []; + + for (int i = 0; i < 3; i++) { + final targetTotal = trainingMax * percentages[i]; + final rounded = _roundWeight(targetTotal, exerciseType); + + double plateWeight = 0; + if (exerciseType != ExerciseType.squat) { + plateWeight = max(0, rounded - currentBodyweight); + } + + sets.add(WorkoutSet( + setNumber: i + 1, + targetPercentage: (percentages[i] * 100).round(), + targetWeightTotal: rounded, + plateWeight: plateWeight, + repsTarget: reps[i], + isAmrap: (i == 2 && week != 4), + )); + } + + return sets; + } + + static double _roundWeight(double weight, ExerciseType type) { + final step = type == ExerciseType.squat + ? AppConstants.squatRoundingStep + : AppConstants.calisthenicsRoundingStep; + return (weight / step).floor() * step; + } + + static double calculate1RM(double weight, int reps) { + if (reps == 1) return weight; + return weight * (1 + reps / 30.0); + } + + static double calculateTrainingMax(double oneRM) { + return oneRM * AppConstants.trainingMaxPercentage; + } + + static double progressTrainingMax(double currentTM, ExerciseType type) { + if (type == ExerciseType.squat) { + return currentTM + AppConstants.lowerBodyIncrement; + } + return currentTM + AppConstants.upperBodyIncrement; + } + + static List generateFSLSets({ + required double trainingMax, + required ExerciseType exerciseType, + required double currentBodyweight, + }) { + final sets = []; + final firstSetPercentage = 0.65; + + final targetTotal = trainingMax * firstSetPercentage; + final rounded = _roundWeight(targetTotal, exerciseType); + + double plateWeight = 0; + if (exerciseType != ExerciseType.squat) { + plateWeight = max(0, rounded - currentBodyweight); + } + + for (int i = 0; i < 5; i++) { + sets.add(WorkoutSet( + setNumber: i + 1, + targetPercentage: 65, + targetWeightTotal: rounded, + plateWeight: plateWeight, + repsTarget: 5, + isAmrap: false, + )); + } + + return sets; + } +} diff --git a/lib/src/shared/domain/logic/xp_calculator.dart b/lib/src/shared/domain/logic/xp_calculator.dart new file mode 100644 index 0000000..98e2011 --- /dev/null +++ b/lib/src/shared/domain/logic/xp_calculator.dart @@ -0,0 +1,116 @@ +import 'dart:math'; +import '../../../core/constants/app_constants.dart'; +import '../entities/exercise.dart'; + +class XPCalculator { + /// Calculate level from total XP + static int calculateLevelFromXP(int xp) { + int level = 1; + while (level <= AppConstants.maxLevel) { + final requiredXP = xpRequiredForLevel(level + 1); + if (xp < requiredXP) { + return level; + } + level++; + } + return AppConstants.maxLevel; + } + + /// Calculate XP required to reach a specific level + static int xpRequiredForLevel(int level) { + if (level <= 1) return 0; + return (AppConstants.baseXP * + pow(AppConstants.xpMultiplier, level - 1)).round(); + } + + /// Calculate XP for next level + static int xpForNextLevel(int currentLevel) { + return xpRequiredForLevel(currentLevel + 1); + } + + /// Calculate XP progress percentage + static double xpProgressPercentage(int currentXP, int currentLevel) { + final currentLevelXP = xpRequiredForLevel(currentLevel); + final nextLevelXP = xpRequiredForLevel(currentLevel + 1); + final xpInCurrentLevel = currentXP - currentLevelXP; + final xpNeededForLevel = nextLevelXP - currentLevelXP; + + if (xpNeededForLevel <= 0) return 1.0; + return (xpInCurrentLevel / xpNeededForLevel).clamp(0.0, 1.0); + } + + /// Calculate XP earned from a completed workout + static int calculateWorkoutXP(List exercises, {bool isPR = false}) { + int totalXP = AppConstants.workoutCompleteXP; // Base completion bonus + + for (final exercise in exercises) { + for (final set in exercise.sets) { + if (!set.completed) continue; + + // Volume XP: 0.1 XP per kg moved + final volume = set.targetWeightTotal * set.repsActual; + totalXP += (volume * AppConstants.volumeXPRate).round(); + + // AMRAP bonus: 25 XP per extra rep + if (set.isAmrap) { + final extraReps = set.repsActual - set.repsTarget; + if (extraReps > 0) { + totalXP += extraReps * AppConstants.amrapBonusXPPerRep; + } + } + } + } + + // PR bonus + if (isPR) { + totalXP += AppConstants.prBonusXP; + } + + return totalXP; + } + + /// Add XP and check for level up + static LevelUpResult addXP({ + required int currentXP, + required int currentLevel, + required int xpToAdd, + }) { + int newXP = currentXP + xpToAdd; + int newLevel = currentLevel; + final levelsGained = []; + + while (newLevel < AppConstants.maxLevel) { + final requiredForNext = xpRequiredForLevel(newLevel + 1); + if (newXP >= requiredForNext) { + newLevel++; + levelsGained.add(newLevel); + } else { + break; + } + } + + return LevelUpResult( + newXP: newXP, + newLevel: newLevel, + levelsGained: levelsGained, + xpForNextLevel: xpRequiredForLevel(newLevel + 1), + ); + } +} + +class LevelUpResult { + final int newXP; + final int newLevel; + final List levelsGained; + final int xpForNextLevel; + + LevelUpResult({ + required this.newXP, + required this.newLevel, + required this.levelsGained, + required this.xpForNextLevel, + }); + + bool get didLevelUp => levelsGained.isNotEmpty; +} + diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..7232b1f --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "slrpg_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.slrpg_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..cf2a11d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); + isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..1fc3500 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux + isar_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..fa72504 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "slrpg_app"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "slrpg_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..a4aacca --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_secure_storage_macos +import isar_flutter_libs +import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2168874 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* slrpg_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "slrpg_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* slrpg_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* slrpg_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/slrpg_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/slrpg_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/slrpg_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/slrpg_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/slrpg_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/slrpg_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..2bf3a52 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..a5e5098 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = slrpg_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.slrpgApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..c924bb9 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1146 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + url: "https://pub.dev" + source: hosted + version: "8.12.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + url: "https://pub.dev" + source: hosted + version: "2.5.2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + isar: + dependency: "direct main" + description: + name: isar + sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_flutter_libs: + dependency: "direct main" + description: + name: isar_flutter_libs + sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_generator: + dependency: "direct dev" + description: + name: isar_generator + sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" + url: "https://pub.dev" + source: hosted + version: "2.4.17" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..629c051 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,73 @@ +name: slrpg_app +description: Streetlifting RPG - Gamified Training App +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # State Management + flutter_riverpod: ^2.5.1 + riverpod_annotation: ^2.3.5 + + # Local Database + isar: ^3.1.0+1 + isar_flutter_libs: ^3.1.0+1 + path_provider: ^2.1.3 + + # Networking + dio: ^5.4.3+1 + pretty_dio_logger: ^1.3.1 + + # Storage + flutter_secure_storage: ^9.0.0 + shared_preferences: ^2.2.3 + + # UI Components + cupertino_icons: ^1.0.6 + google_fonts: ^6.2.1 + flutter_svg: ^2.0.10+1 + cached_network_image: ^3.3.1 + shimmer: ^3.0.0 + + # Utilities + intl: ^0.19.0 + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + equatable: ^2.0.5 + logger: ^2.3.0 + fl_chart: ^0.66.0 + + go_router: ^14.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + + # Code Generation + build_runner: ^2.4.9 + riverpod_generator: ^2.4.0 + isar_generator: ^3.1.0+1 + freezed: ^2.5.2 + json_serializable: ^6.8.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/images/avatars/male/ + - assets/images/avatars/female/ + - assets/images/plates/ + - assets/images/enemies/ + - assets/images/backgrounds/ + + # fonts: + # - family: PixelFont + # fonts: + # - asset: assets/fonts/pixel.ttf diff --git a/test/plate_calculator_test.dart b/test/plate_calculator_test.dart new file mode 100644 index 0000000..577cbf8 --- /dev/null +++ b/test/plate_calculator_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:slrpg_app/src/shared/domain/logic/plate_calculator.dart'; + +void main() { + group('PlateCalculator', () { + final plates = [20.0, 10.0, 5.0, 2.5, 1.25]; + final bands = {'Green': 20.0, 'Blue': 10.0}; + + test('Calculates barbell plates correctly', () { + // Target 70kg, Bar 20kg -> Need 50kg -> 25kg per side -> 20 + 5 + final result = PlateCalculator.calculate( + targetWeight: 70.0, + barWeight: 20.0, + availablePlates: plates, + isTwoSided: true, + ); + + expect(result.success, true); + expect(result.plateConfiguration, [20.0, 5.0]); + expect(result.totalAchieved, 70.0); + }); + + test('Calculates belt weight correctly (One Sided)', () { + // Need 30kg added -> 20 + 10 + final result = PlateCalculator.calculate( + targetWeight: 110.0, // 80 BW + 30 Added + barWeight: 80.0, // BW + availablePlates: plates, + isTwoSided: false, + ); + + expect(result.success, true); + expect(result.plateConfiguration, [20.0, 10.0]); + expect(result.totalAchieved, 110.0); + }); + + test('Band Selection: Correct band chosen', () { + // Need -10kg support + final result = PlateCalculator.calculate( + targetWeight: 70.0, + barWeight: 80.0, // BW + availablePlates: plates, + availableBands: bands, + isTwoSided: false, + ); + + expect(result.success, true); + expect(result.plateConfiguration, isEmpty); + expect(result.bandAssistance, 'Blue'); // 10kg band + expect(result.totalAchieved, 70.0); + }); + + test('Smart Switch: Bodyweight is better than oversized band', () { + // Need -2kg support. Smallest band is 10kg. + // With band: -10kg (8kg error). Without band: 0kg (2kg error). + // Should choose Bodyweight Only. + + final result = PlateCalculator.calculate( + targetWeight: 78.0, + barWeight: 80.0, + availablePlates: plates, + availableBands: {'BigBand': 10.0}, + isTwoSided: false, + ); + + expect(result.success, true); + expect(result.bandAssistance, null); // No band! + expect(result.message, contains('Bodyweight Only')); + }); + + test('Rounding Logic handles uneven weights', () { + // Need 9kg. Smallest plate 1.25. + // 9 / 1.25 = 7.2. Should round to 7 * 1.25 = 8.75kg or 8 * 1.25 = 10kg. + // 8.75 is closer (0.25 diff) than 10 (1.0 diff). + + final result = PlateCalculator.calculate( + targetWeight: 29.0, + barWeight: 20.0, + availablePlates: [1.25], // Only small plates + isTwoSided: false, // Belt + ); + + // Needed 9.0. Found 8.75 (7x 1.25). Total 28.75. + expect(result.success, true); + expect(result.totalAchieved, 28.75); + expect(result.plateConfiguration.length, 7); + }); + }); +} diff --git a/test/wendler_calculator_test.dart b/test/wendler_calculator_test.dart new file mode 100644 index 0000000..fdfe1bb --- /dev/null +++ b/test/wendler_calculator_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:slrpg_app/src/shared/domain/logic/wendler_calculator.dart'; + +void main() { + group('WendlerCalculator', () { + const double bodyweight = 80.0; + + test('Week 1 (5/5/5+) generates correct percentages and reps', () { + // Setup: Squat TM 100kg + final sets = WendlerCalculator.generateSets( + week: 1, + trainingMax: 100.0, + exerciseType: ExerciseType.squat, + currentBodyweight: bodyweight, + ); + + expect(sets.length, 3); + + // Set 1: 65% = 65kg + expect(sets[0].targetPercentage, 65); + expect(sets[0].targetWeightTotal, 65.0); + expect(sets[0].repsTarget, 5); + expect(sets[0].isAmrap, false); + + // Set 2: 75% = 75kg + expect(sets[1].targetPercentage, 75); + expect(sets[1].targetWeightTotal, 75.0); + expect(sets[1].repsTarget, 5); + + // Set 3: 85% = 85kg (AMRAP) + expect(sets[2].targetPercentage, 85); + expect(sets[2].targetWeightTotal, 85.0); + expect(sets[2].repsTarget, 5); + expect(sets[2].isAmrap, true); + }); + + test('Squat rounding works (2.5kg steps)', () { + // 65% of 105kg = 68.25kg -> Should round down to 67.5kg + final sets = WendlerCalculator.generateSets( + week: 1, + trainingMax: 105.0, + exerciseType: ExerciseType.squat, + currentBodyweight: bodyweight, + ); + + expect(sets[0].targetWeightTotal, 67.5); + }); + + test('Pullup calculation considers bodyweight', () { + // TM 100kg, BW 80kg -> Target 65kg (65%) + // Since Target (65) < BW (80), plate weight should be 0 (assistance/bodyweight logic handles negative) + // But WendlerCalculator returns total system weight in targetWeightTotal + + final sets = WendlerCalculator.generateSets( + week: 1, + trainingMax: 100.0, + exerciseType: ExerciseType.pullup, + currentBodyweight: bodyweight, + ); + + expect(sets[0].targetWeightTotal, 65.0); // System weight + expect(sets[0].plateWeight, 0.0); // No added weight + }); + + test('Pullup calculation with added weight', () { + // TM 150kg -> 65% = 97.5kg + // BW 80kg -> Added weight = 17.5kg + + final sets = WendlerCalculator.generateSets( + week: 1, + trainingMax: 150.0, + exerciseType: ExerciseType.pullup, + currentBodyweight: bodyweight, + ); + + expect(sets[0].targetWeightTotal, 97.5); + expect(sets[0].plateWeight, 17.5); + }); + + test('FSL generates 5x5 at 65%', () { + final fslSets = WendlerCalculator.generateFSLSets( + trainingMax: 100.0, + exerciseType: ExerciseType.squat, + currentBodyweight: bodyweight, + ); + + expect(fslSets.length, 5); + for (var set in fslSets) { + expect(set.targetPercentage, 65); + expect(set.targetWeightTotal, 65.0); + expect(set.repsTarget, 5); + expect(set.isAmrap, false); + } + }); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4df70d2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + slrpg_app + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..bf97e65 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "slrpg_app", + "short_name": "slrpg_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..c924c9f --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(slrpg_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "slrpg_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..258c117 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + IsarFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0552f96 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows + isar_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..b17ae6d --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "slrpg_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "slrpg_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "slrpg_app.exe" "\0" + VALUE "ProductName", "slrpg_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..a0a25d9 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"slrpg_app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_