Compare commits

...

19 commits
v0.0.2 ... main

Author SHA1 Message Date
6241efca58 fix: fix wrong url for sending finished trainings 2025-10-08 21:55:12 +02:00
cfbd2a313b feat: add Training Timer and calculation of optimal Round Times 2025-10-08 21:55:12 +02:00
git
6d9151c8ec Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-09-07 12:41:32 +02:00
Renovate Bot
5377dfce27 Add renovate.json 2025-09-07 12:38:27 +02:00
217623efe2 refactor: add missing files 2025-07-31 09:00:48 +02:00
b94c6e1ec1 refactor: remove old golang files 2025-07-30 22:33:34 +02:00
e6dc77116b change from golang fyne to kotlin and Jetpack Compose 2025-07-30 22:31:57 +02:00
e3eb2c9aa4 refactor: finish refactor by cleaning up codebase 2025-07-04 18:49:05 +02:00
519daeec40 refactor: update edit functionality to be less confusing 2025-07-04 18:40:47 +02:00
4f8e353d48 refactor: update wrong list behaviour in history screen 2025-07-04 17:40:51 +02:00
084ea252a2 refactor: change history screen to use a list instead of cards 2025-07-04 17:12:28 +02:00
059db8f2fb refactor: show finish Button only if time elapsed and center training card on home screen 2025-07-04 12:56:02 +02:00
16b2409ae8 refactor: update coding style 2025-07-04 10:57:09 +02:00
a7e427ca14 refactor: perform clean up 2025-06-28 14:13:14 +02:00
c15cdea57d refactor: new ui step3 2025-06-27 20:18:32 +02:00
24430d0fae refactor: ui refactor step 2 2025-06-27 19:55:34 +02:00
9cae00d2a5 refactor: first step to refactor the ui 2025-06-27 19:37:15 +02:00
a8ed9c9ed1 refactor: rebuild the app in golang with the fyne framework 2025-06-27 08:16:24 +02:00
479e4dffa8 docs: add changelog 2025-06-22 17:12:51 +02:00
197 changed files with 2067 additions and 6623 deletions

58
.gitignore vendored
View file

@ -1,45 +1,15 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml *.iml
*.ipr .gradle
*.iws /local.properties
.idea/ /.idea/caches
/.idea/libraries
# The .vscode folder contains launch configuration and tasks you configure in /.idea/modules.xml
# VS Code which you may wish to be included in version control, so this line /.idea/workspace.xml
# is commented out by default. /.idea/navEditor.xml
#.vscode/ /.idea/assetWizardSettings.xml
.DS_Store
# Flutter/Dart/Pub related /build
**/doc/api/ /captures
**/ios/Flutter/.last_build_id .externalNativeBuild
.dart_tool/ .cxx
.flutter-plugins local.properties
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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

View file

@ -1,45 +0,0 @@
# 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: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: linux
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: macos
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: windows
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# 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'

BIN
Icon.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,16 +0,0 @@
# kettlebell_tracker
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.

View file

@ -1,28 +0,0 @@
# 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

14
android/.gitignore vendored
View file

@ -1,14 +0,0 @@
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

View file

@ -1,45 +0,0 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.kettlebell_tracker"
compileSdk = flutter.compileSdkVersion
// ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.kettlebell_tracker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -1,45 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="kettlebell_tracker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -1,5 +0,0 @@
package com.example.kettlebell_tracker
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -1,21 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View file

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View file

@ -1,25 +0,0 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

205
app/build.gradle.kts Normal file
View file

@ -0,0 +1,205 @@
//plugins {
// id("com.android.application")
// id("org.jetbrains.kotlin.android")
// id("org.jetbrains.kotlin.plugin.compose")
// id("kotlin-kapt")
//}
//
//android {
// namespace = "de.patani.kettlebelltracker"
// compileSdk = 34
//
// defaultConfig {
// applicationId = "de.patani.kettlebelltracker"
// minSdk = 26
// targetSdk = 34
// versionCode = 1
// versionName = "1.0"
//
// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// vectorDrawables {
// useSupportLibrary = true
// }
// }
//
// buildTypes {
// release {
// isMinifyEnabled = false
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
// }
// }
// compileOptions {
// sourceCompatibility = JavaVersion.VERSION_1_8
// targetCompatibility = JavaVersion.VERSION_1_8
// }
// kotlinOptions {
// jvmTarget = "1.8"
// }
// buildFeatures {
// compose = true
// }
// composeOptions {
// kotlinCompilerExtensionVersion = "1.5.1"
// }
// packaging {
// resources {
// excludes += "/META-INF/{AL2.0,LGPL2.1}"
// excludes += "org/intellij/lang/annotations/**"
// }
// }
//}
//
//configurations {
// all {
// exclude(group = "com.intellij", module = "annotations")
// }
//}
//
//dependencies {
// // Core
// implementation("androidx.core:core-ktx:1.12.0")
// implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
// implementation("androidx.activity:activity-compose:1.8.2")
//
// // Compose
// implementation(platform("androidx.compose:compose-bom:2023.08.00"))
// implementation("androidx.compose.ui:ui")
// implementation("androidx.compose.ui:ui-graphics")
// implementation("androidx.compose.ui:ui-tooling-preview")
// implementation("androidx.compose.material3:material3")
// implementation("androidx.compose.material:material-icons-extended")
//
//
// // Navigation
// implementation("androidx.navigation:navigation-compose:2.7.6")
//
// // ViewModel
// implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
//
// // Room (Database)
// implementation("androidx.room:room-runtime:2.6.1")
// implementation("androidx.room:room-ktx:2.6.1")
// implementation(libs.androidx.room.common.jvm)
// implementation(libs.androidx.room.compiler)
// kapt("androidx.room:room-compiler:2.6.1")
//
// // DataStore (Settings)
// implementation("androidx.datastore:datastore-preferences:1.0.0")
//
// // Retrofit (API)
// implementation("com.squareup.retrofit2:retrofit:2.9.0")
// implementation("com.squareup.retrofit2:converter-gson:2.9.0")
//
// // Hilt (Dependency Injection - Optional, but recommended)
// // implementation("com.google.dagger:hilt-android:2.48")
// // kapt("com.google.dagger:hilt-compiler:2.48")
// // implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
//}
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.kapt") // Wichtig: Hier auf 'org.jetbrains.kotlin.kapt' geändert
}
android {
namespace = "de.patani.kettlebelltracker"
compileSdk = 34
defaultConfig {
applicationId = "de.patani.kettlebelltracker"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "org/intellij/lang/annotations/**"
}
}
}
// Den 'configurations' Block entfernen, es sei denn, du benötigst ihn explizit für einen spezifischen Ausschluss.
// Wenn du ihn absichtlich hinzugefügt hast, um ein bekanntes Problem zu lösen, kannst du ihn behalten.
// Ansonsten kommentiere ihn aus oder entferne ihn:
/*
configurations {
all {
exclude(group = "com.intellij", module = "annotations")
}
}
*/
dependencies {
// Core
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.2")
// Compose
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.6")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
// Room (Database)
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
// Wichtig: Die folgenden Zeilen für den Room Compiler wurden entfernt/korrigiert!
// KEIN implementation("libs.androidx.room.common.jvm")
// KEIN implementation("libs.androidx.room.compiler")
kapt("androidx.room:room-compiler:2.6.1") // NUR diese Zeile für den Compiler!
// DataStore (Settings)
implementation("androidx.datastore:datastore-preferences:1.0.0")
// Retrofit (API)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Hilt (Dependency Injection - Optional, but recommended)
// implementation("com.google.dagger:hilt-android:2.48")
// kapt("com.google.dagger:hilt-compiler:2.48")
// implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,24 @@
package de.patani.kettlebelltracker
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("de.patani.kettlebelltracker", appContext.packageName)
}
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Kettlebelltracker">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Kettlebelltracker">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,158 @@
package de.patani.kettlebelltracker
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.*
import androidx.room.Room
import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
import de.patani.kettlebelltracker.data.local.AppDatabase
import de.patani.kettlebelltracker.repositories.ApiRepository
import de.patani.kettlebelltracker.ui.navigation.Screen
import de.patani.kettlebelltracker.ui.screens.*
import de.patani.kettlebelltracker.ui.theme.KettlebellTrackerTheme
import de.patani.kettlebelltracker.viewmodels.*
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.UUID
import androidx.core.content.edit
class MainActivity : ComponentActivity() {
private val db by lazy {
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "kettlebell_tracker.db").build()
}
private val settingsDataStore by lazy { SettingsDataStore(applicationContext) }
private val apiService by lazy {
Retrofit.Builder()
.baseUrl("https://kb.patanix.de/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(de.patani.kettlebelltracker.data.remote.ApiService::class.java)
}
private val apiRepository by lazy { ApiRepository(apiService) }
private val appUUID by lazy {
val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE)
var uuid = prefs.getString("app_uuid", null)
if (uuid == null) {
uuid = UUID.randomUUID().toString()
prefs.edit { putString("app_uuid", uuid) }
}
uuid
}
private val trainingViewModel by lazy {
ViewModelProvider(this, createViewModelFactory(TrainingViewModel::class.java)).get(TrainingViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KettlebellTrackerTheme {
App(createViewModelFactory = { modelClass -> createViewModelFactory(modelClass) })
}
}
}
@Suppress("UNCHECKED_CAST")
private fun <T : ViewModel> createViewModelFactory(modelClass: Class<T>): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(TrainingViewModel::class.java) ->
TrainingViewModel(db.trainingSessionDao(), settingsDataStore, apiRepository,
appUUID
) as T
modelClass.isAssignableFrom(HomeViewModel::class.java) ->
HomeViewModel(db.trainingSessionDao(), trainingViewModel) as T
modelClass.isAssignableFrom(HistoryViewModel::class.java) ->
HistoryViewModel(db.trainingSessionDao()) as T
modelClass.isAssignableFrom(SettingsViewModel::class.java) ->
SettingsViewModel(settingsDataStore) as T
else -> throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
}
}
@Composable
fun App(createViewModelFactory: (Class<out ViewModel>) -> ViewModelProvider.Factory) {
val navController = rememberNavController()
val screens = listOf(
Screen.Home,
Screen.Training,
Screen.History,
Screen.Settings
)
val sharedTrainingViewModel: TrainingViewModel =
androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(TrainingViewModel::class.java))
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
screens.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.title) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController,
startDestination = Screen.Home.route,
Modifier.padding(innerPadding)
) {
composable(Screen.Home.route) {
val homeViewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(HomeViewModel::class.java))
HomeScreen(
viewModel = homeViewModel,
onStartTrainingClicked = {
sharedTrainingViewModel.startTraining()
navController.navigate(Screen.Training.route)
}
)
}
composable(Screen.Training.route) {
TrainingScreen(viewModel = sharedTrainingViewModel)
}
composable(Screen.History.route) {
val historyViewModel: HistoryViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(HistoryViewModel::class.java))
HistoryScreen(viewModel = historyViewModel)
}
composable(Screen.Settings.route) {
val settingsViewModel: SettingsViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(SettingsViewModel::class.java))
SettingsScreen(viewModel = settingsViewModel)
}
}
}
}

View file

@ -0,0 +1,54 @@
package de.patani.kettlebelltracker.data.datastore
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class SettingsDataStore(context: Context) {
private val dataStore = context.dataStore
companion object {
val TRAINING_TIME_MINUTES = intPreferencesKey("trainingTimeMinutes")
val WEIGHT_LEFT = doublePreferencesKey("weightLeft")
val WEIGHT_RIGHT = doublePreferencesKey("weightRight")
val GOAL_SETS = intPreferencesKey("goalSets")
val INITIAL_PROGRAM = stringPreferencesKey("initialProgram")
}
val settingsFlow: Flow<Settings> = dataStore.data.map { preferences ->
Settings(
trainingTimeMinutes = preferences[TRAINING_TIME_MINUTES] ?: 20,
weightLeft = preferences[WEIGHT_LEFT] ?: 16.0,
weightRight = preferences[WEIGHT_RIGHT] ?: 16.0,
goalSets = preferences[GOAL_SETS] ?: 5,
initialProgram = preferences[INITIAL_PROGRAM] ?: "giant_1.0"
)
}
suspend fun saveSettings(settings: Settings) {
dataStore.edit { preferences ->
preferences[TRAINING_TIME_MINUTES] = settings.trainingTimeMinutes
preferences[WEIGHT_LEFT] = settings.weightLeft
preferences[WEIGHT_RIGHT] = settings.weightRight
preferences[GOAL_SETS] = settings.goalSets
preferences[INITIAL_PROGRAM] = settings.initialProgram
}
}
}
data class Settings(
val trainingTimeMinutes: Int,
val weightLeft: Double,
val weightRight: Double,
val goalSets: Int,
val initialProgram: String
)

View file

@ -0,0 +1,11 @@
package de.patani.kettlebelltracker.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(entities = [TrainingSession::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun trainingSessionDao(): TrainingSessionDao
}

View file

@ -0,0 +1,16 @@
package de.patani.kettlebelltracker.data.local
import androidx.room.TypeConverter
import java.util.Date
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}

View file

@ -0,0 +1,19 @@
package de.patani.kettlebelltracker.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date
@Entity(tableName = "training_session")
data class TrainingSession(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val date: Date,
val sets: Int,
val weightLeft: Double,
val weightRight: Double,
val repsPerSet: Int,
val duration: Long, // in seconds
val program: String,
val blockDay: Int
)

View file

@ -0,0 +1,36 @@
package de.patani.kettlebelltracker.data.local
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface TrainingSessionDao {
@Query("SELECT * FROM training_session ORDER BY date DESC")
fun getAllSessions(): Flow<List<TrainingSession>>
@Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 20")
fun getHistory(): Flow<List<TrainingSession>>
@Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 1")
fun getLastSession(): Flow<TrainingSession?>
@Query("SELECT COUNT(*) FROM training_session")
fun getTrainingCount(): Flow<Int>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(session: TrainingSession)
@Update
suspend fun update(session: TrainingSession)
@Delete
suspend fun delete(session: TrainingSession)
@Query("DELETE FROM training_session WHERE id = :sessionId")
suspend fun deleteById(sessionId: Long)
}

View file

@ -0,0 +1,38 @@
package de.patani.kettlebelltracker.data.remote
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface ApiService {
@POST("trainings")
suspend fun sendTrainingData(@Body payload: TrainingPayload): Response<Unit>
@POST("trainings/recommend-rest")
suspend fun getRecommendedRest(@Body request: RestRecommendationRequest): Response<ApiResponse<RestRecommendationData>>
}
data class RestRecommendationRequest(
val uuid: String,
val reps_per_set: Int,
val current_sets: Int
)
data class RestRecommendationResponse(
val recommended_rest_seconds: Int
)
data class ApiResponse<T>(
val status: String,
val message: String?,
val data: T?
)
data class RestRecommendationData(
val recommended_rest: Int,
val expected_sets: Int,
val last_training_rest: Int?,
val last_training_sets: Int?,
val reasoning: String?
)

View file

@ -0,0 +1,8 @@
package de.patani.kettlebelltracker.data.remote
data class TrainingPayload(
val reps: Int,
val rest: Double,
val sets: Int,
val uuid: String
)

View file

@ -0,0 +1,54 @@
package de.patani.kettlebelltracker.repositories
import android.util.Log
import de.patani.kettlebelltracker.data.remote.TrainingPayload
import de.patani.kettlebelltracker.data.remote.ApiService
import de.patani.kettlebelltracker.data.remote.RestRecommendationData
import de.patani.kettlebelltracker.data.remote.RestRecommendationRequest
import de.patani.kettlebelltracker.data.remote.RestRecommendationResponse
class ApiRepository(private val apiService: ApiService) {
suspend fun sendTrainingData(session: de.patani.kettlebelltracker.data.local.TrainingSession, uuid: String) {
try {
val rest = if (session.sets > 0) {
session.duration.toDouble() / session.sets.toDouble()
} else {
0.0
}
val payload = TrainingPayload(
reps = session.repsPerSet,
rest = rest,
sets = session.sets,
uuid = uuid
)
val response = apiService.sendTrainingData(payload)
if (response.isSuccessful) {
Log.i("ApiRepository", "Training successfully sent to backend.")
} else {
Log.e("ApiRepository", "API Error: Unexpected status code: ${response.code()}")
}
} catch (e: Exception) {
Log.e("ApiRepository", "API Error: Failed to send training data", e)
}
}
suspend fun getRecommendedRest(uuid: String, repsPerSet: Int, currentSets: Int): RestRecommendationData? {
return try {
val request = RestRecommendationRequest(uuid, repsPerSet, currentSets)
val response = apiService.getRecommendedRest(request)
if (response.isSuccessful) {
Log.i("ApiRepository", "Got Rest Recommendation:")
val body = response.body()?.data
Log.i("ApiRepository", body.toString())
response.body()?.data
} else {
null
}
} catch (e: Exception) {
null
}
}
}

View file

@ -0,0 +1,15 @@
package de.patani.kettlebelltracker.ui.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
object Home : Screen("home", "Home", Icons.Default.Home)
object Training : Screen("training", "Training", Icons.Default.PlayArrow)
object History : Screen("history", "Historie", Icons.Default.DateRange)
object Settings : Screen("settings", "Einstellungen", Icons.Default.Settings)
}

View file

@ -0,0 +1,229 @@
package de.patani.kettlebelltracker.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import de.patani.kettlebelltracker.viewmodels.HistoryViewModel
import de.patani.kettlebelltracker.util.formatDate
import de.patani.kettlebelltracker.util.formatDuration
import de.patani.kettlebelltracker.data.local.TrainingSession
import androidx.compose.ui.Alignment
@Composable
fun HistoryScreen(viewModel: HistoryViewModel) {
val history by viewModel.history.collectAsState()
if (history.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Noch keine Trainingsdaten vorhanden.")
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
Row(Modifier.fillMaxWidth()) {
Text("Datum", modifier = Modifier.weight(1f))
Text("Sätze", modifier = Modifier.weight(0.5f))
Text("Dauer", modifier = Modifier.weight(0.7f))
Text("Reps", modifier = Modifier.weight(0.5f))
}
Divider(modifier = Modifier.padding(vertical = 8.dp))
}
items(history) { session ->
HistoryItem(
session = session,
onUpdate = viewModel::updateSession,
onDelete = viewModel::deleteSession
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HistoryItem(session: TrainingSession, onUpdate: (TrainingSession) -> Unit, onDelete: (TrainingSession) -> Unit) {
var showDialog by remember { mutableStateOf(false) }
Card(onClick = { showDialog = true }, modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp)) {
Text(text = session.date.formatDate(), modifier = Modifier.weight(1f))
Text(text = session.sets.toString(), modifier = Modifier.weight(0.5f))
Text(text = formatDuration(session.duration), modifier = Modifier.weight(0.7f))
Text(text = session.repsPerSet.toString(), modifier = Modifier.weight(0.5f))
}
}
if (showDialog) {
EditHistoryDialog(
session = session,
onDismiss = { showDialog = false },
onSave = { updatedSession ->
onUpdate(updatedSession)
showDialog = false
},
onDelete = {
onDelete(session)
showDialog = false
}
)
}
}
@Composable
fun EditHistoryDialog(session: TrainingSession, onDismiss: () -> Unit, onSave: (TrainingSession) -> Unit, onDelete: () -> Unit) {
var editedSets by remember { mutableStateOf(session.sets.toString()) }
var editedRepsPerSet by remember { mutableStateOf(session.repsPerSet.toString()) }
var editedWeightLeft by remember { mutableStateOf(session.weightLeft.toString()) }
var editedWeightRight by remember { mutableStateOf(session.weightRight.toString()) }
var editedDurationMinutes by remember { mutableStateOf((session.duration / 60).toString()) }
var editedDurationSeconds by remember { mutableStateOf((session.duration % 60).toString()) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Eintrag bearbeiten") },
text = {
Column {
Text("Datum: ${session.date.formatDate()}", style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = editedSets,
onValueChange = { newValue ->
if (newValue.all { it.isDigit() } || newValue.isEmpty()) {
editedSets = newValue
}
},
label = { Text("Sätze") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
singleLine = true
)
OutlinedTextField(
value = editedRepsPerSet,
onValueChange = { newValue ->
if (newValue.all { it.isDigit() } || newValue.isEmpty()) {
editedRepsPerSet = newValue
}
},
label = { Text("Wiederholungen pro Satz") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
singleLine = true
)
OutlinedTextField(
value = editedWeightLeft,
onValueChange = { newValue ->
if (newValue.matches(Regex("^\\d*\\.?\\d*\$"))) {
editedWeightLeft = newValue
}
},
label = { Text("Gewicht Links (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
singleLine = true
)
OutlinedTextField(
value = editedWeightRight,
onValueChange = { newValue ->
if (newValue.matches(Regex("^\\d*\\.?\\d*\$"))) {
editedWeightRight = newValue
}
},
label = { Text("Gewicht Rechts (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
singleLine = true
)
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
OutlinedTextField(
value = editedDurationMinutes,
onValueChange = { newValue ->
if (newValue.all { it.isDigit() } || newValue.isEmpty()) {
editedDurationMinutes = newValue
}
},
label = { Text("Dauer (Min)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f).padding(end = 4.dp),
singleLine = true
)
OutlinedTextField(
value = editedDurationSeconds,
onValueChange = { newValue ->
if (newValue.all { it.isDigit() } || newValue.isEmpty()) {
editedDurationSeconds = newValue
}
},
label = { Text("Dauer (Sek)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f).padding(start = 4.dp),
singleLine = true
)
}
Text("Programm: ${session.program}", style = MaterialTheme.typography.bodySmall)
Text("Block Tag: ${session.blockDay}", style = MaterialTheme.typography.bodySmall)
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = onDelete,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
modifier = Modifier.weight(1f).padding(end = 4.dp)
) {
Icon(Icons.Default.Delete, contentDescription = "Löschen")
Spacer(Modifier.width(4.dp))
Text("Löschen")
}
Button(
onClick = {
val newSets = editedSets.toIntOrNull() ?: session.sets
val newRepsPerSet = editedRepsPerSet.toIntOrNull() ?: session.repsPerSet
val newWeightLeft = editedWeightLeft.toDoubleOrNull() ?: session.weightLeft
val newWeightRight = editedWeightRight.toDoubleOrNull() ?: session.weightRight
val newDurationMinutes = editedDurationMinutes.toLongOrNull() ?: 0L
val newDurationSeconds = editedDurationSeconds.toLongOrNull() ?: 0L
val newDuration = newDurationMinutes * 60 + newDurationSeconds
val updatedSession = session.copy(
sets = newSets,
repsPerSet = newRepsPerSet,
weightLeft = newWeightLeft,
weightRight = newWeightRight,
duration = newDuration
)
onSave(updatedSession)
},
modifier = Modifier.weight(1f).padding(start = 4.dp)
) {
Text("Speichern")
}
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text("Abbrechen")
}
}
)
}

View file

@ -0,0 +1,85 @@
package de.patani.kettlebelltracker.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.patani.kettlebelltracker.viewmodels.HomeViewModel
import de.patani.kettlebelltracker.util.formatDate
import de.patani.kettlebelltracker.util.formatDuration
import java.util.Calendar
@Composable
fun HomeScreen(
viewModel: HomeViewModel,
onStartTrainingClicked: () -> Unit
) {
val state by viewModel.homeScreenState.collectAsState()
val isTrainedToday = remember(state.lastTrainingSession) {
val lastDate = state.lastTrainingSession?.date
if (lastDate == null) false
else {
val cal1 = Calendar.getInstance().apply { time = lastDate }
val cal2 = Calendar.getInstance()
cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(
text = "Kettlebell Workout Tracker",
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Nächstes Training", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
Text("${state.nextTrainingProgram} - Tag ${state.nextTrainingBlockDay}")
Spacer(modifier = Modifier.height(8.dp))
Text("Ziel: ${state.nextTrainingReps} Wiederholungen pro Satz")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onStartTrainingClicked, enabled = !isTrainedToday) {
Text(if (isTrainedToday) "Heute bereits trainiert" else "Training starten")
}
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text("Letzte Leistung", style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
StatItem("Datum", state.lastTrainingSession?.date?.formatDate() ?: "")
StatItem("Sätze", state.lastTrainingSession?.sets?.toString() ?: "")
StatItem("Dauer", formatDuration(state.lastTrainingSession?.duration ?: 0))
StatItem("Gewicht", "${state.lastTrainingSession?.weightLeft ?: ""}kg")
}
}
}
}
}
@Composable
fun StatItem(label: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = label, style = MaterialTheme.typography.labelMedium)
Text(text = value, style = MaterialTheme.typography.bodyLarge, fontSize = 18.sp)
}
}

View file

@ -0,0 +1,84 @@
package de.patani.kettlebelltracker.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import de.patani.kettlebelltracker.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
val settings by viewModel.settings.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
var time by remember(settings.trainingTimeMinutes) { mutableStateOf(settings.trainingTimeMinutes.toString()) }
var sets by remember(settings.goalSets) { mutableStateOf(settings.goalSets.toString()) }
var weightLeft by remember(settings.weightLeft) { mutableStateOf(settings.weightLeft.toString()) }
var weightRight by remember(settings.weightRight) { mutableStateOf(settings.weightRight.toString()) }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Einstellungen", style = MaterialTheme.typography.headlineSmall)
OutlinedTextField(
value = time,
onValueChange = { time = it },
label = { Text("Trainingszeit (Minuten)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = sets,
onValueChange = { sets = it },
label = { Text("Ziel-Sätze") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = weightLeft,
onValueChange = { weightLeft = it },
label = { Text("Gewicht Links (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = weightRight,
onValueChange = { weightRight = it },
label = { Text("Gewicht Rechts (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
val timeInt = time.toIntOrNull() ?: settings.trainingTimeMinutes
val setsInt = sets.toIntOrNull() ?: settings.goalSets
val weightL = weightLeft.toDoubleOrNull() ?: settings.weightLeft
val weightR = weightRight.toDoubleOrNull() ?: settings.weightRight
viewModel.saveSettings(timeInt, setsInt, weightL, weightR)
scope.launch {
snackbarHostState.showSnackbar("Einstellungen gespeichert!")
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Speichern")
}
}
}
}

View file

@ -0,0 +1,173 @@
package de.patani.kettlebelltracker.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.patani.kettlebelltracker.viewmodels.TrainingViewModel
import de.patani.kettlebelltracker.util.formatDuration
@Composable
fun TrainingScreen(viewModel: TrainingViewModel) {
val trainingState by viewModel.trainingState.collectAsState()
val showFinishButton = trainingState.remainingSeconds <= 0 && trainingState.isTrainingRunning
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
if (trainingState.isRoundActive) {
RoundTimerCard(
timeRemaining = trainingState.currentRoundTime,
totalTime = trainingState.totalRoundTime,
)
}
if (trainingState.isRoundActive) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Verbleibende Zeit", style = MaterialTheme.typography.titleLarge)
Text(
text = formatDuration(trainingState.remainingSeconds.toLong()),
fontSize = 72.sp,
fontWeight = FontWeight.Bold,
color = if (trainingState.remainingSeconds <= 10)
MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.onSurface
)
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Sätze", style = MaterialTheme.typography.titleLarge)
Text(
text = "${trainingState.setsDone} / ${trainingState.goalSets}",
fontSize = 60.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
Text(
text = "${trainingState.repsPerSet} Wiederholungen",
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (trainingState.isRoundActive) {
Button(
onClick = viewModel::completeSet,
enabled = trainingState.isTrainingRunning,
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
) {
Text("Satz abschließen", fontSize = 18.sp)
}
}
if (showFinishButton) {
Button(
onClick = viewModel::finishTraining,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text("Training beenden & Speichern", fontSize = 16.sp)
}
}
}
}
}
@Composable
fun RoundTimerCard(
timeRemaining: Int,
totalTime: Int
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(
containerColor = when {
timeRemaining <= 0 -> MaterialTheme.colorScheme.errorContainer
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f)
else -> MaterialTheme.colorScheme.primaryContainer
}
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (timeRemaining <= 0) "Zeit abgelaufen!" else "Rundenzeit",
style = MaterialTheme.typography.titleLarge,
color = when {
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.primary
}
)
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier.size(120.dp),
contentAlignment = Alignment.Center
) {
val progress = if (totalTime > 0 && timeRemaining >= 0) {
(totalTime - timeRemaining).toFloat() / totalTime.toFloat()
} else if (timeRemaining < 0) 1f else 0f
CircularProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxSize(),
strokeWidth = 8.dp,
color = when {
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (timeRemaining >= 0) {
formatDuration(timeRemaining.toLong())
} else {
"+${formatDuration((-timeRemaining).toLong())}"
},
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = when {
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
)
Text(
text = if (timeRemaining <= 0) "überzogen" else "verbleibend",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
}
}
}

View file

@ -0,0 +1,11 @@
package de.patani.kettlebelltracker.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View file

@ -0,0 +1,30 @@
package de.patani.kettlebelltracker.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF61AFEF), // Blue
secondary = Color(0xFFC678DD), // Purple
tertiary = Color(0xFF98C379), // Green
background = Color(0xFF282C34),
surface = Color(0xFF2C313A),
onPrimary = Color.Black,
onSecondary = Color.Black,
onTertiary = Color.Black,
onBackground = Color(0xFFABB2BF),
onSurface = Color(0xFFABB2BF),
error = Color(0xFFE06C75), // Red
)
@Composable
fun KettlebellTrackerTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = DarkColorScheme,
typography = Typography,
content = content
)
}

View file

@ -0,0 +1,17 @@
package de.patani.kettlebelltracker.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)

View file

@ -0,0 +1,19 @@
package de.patani.kettlebelltracker.util
import android.annotation.SuppressLint
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun Date.formatDate(): String {
val formatter = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
return formatter.format(this)
}
@SuppressLint("DefaultLocale")
fun formatDuration(totalSeconds: Long): String {
if (totalSeconds < 0) return "00:00"
val minutes = totalSeconds / 60
val seconds = totalSeconds % 60
return String.format("%02d:%02d", minutes, seconds)
}

View file

@ -0,0 +1,24 @@
package de.patani.kettlebelltracker.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
import de.patani.kettlebelltracker.data.local.TrainingSessionDao
import de.patani.kettlebelltracker.data.local.TrainingSession
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.Date
class HistoryViewModel(private val dao: TrainingSessionDao) : ViewModel() {
val history = dao.getHistory()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun updateSession(session: TrainingSession) = viewModelScope.launch {
dao.update(session)
}
fun deleteSession(session: TrainingSession) = viewModelScope.launch {
dao.delete(session)
}
}

View file

@ -0,0 +1,33 @@
package de.patani.kettlebelltracker.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.patani.kettlebelltracker.data.local.TrainingSessionDao
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
class HomeViewModel(
dao: TrainingSessionDao,
trainingViewModel: TrainingViewModel
) : ViewModel() {
val lastSession = dao.getLastSession()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
val homeScreenState = combine(lastSession, trainingViewModel.trainingState) { last, trainingState ->
HomeScreenState(
lastTrainingSession = last,
nextTrainingProgram = trainingState.currentProgram,
nextTrainingBlockDay = trainingState.currentBlockDay,
nextTrainingReps = trainingState.currentReps
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomeScreenState())
}
data class HomeScreenState(
val lastTrainingSession: de.patani.kettlebelltracker.data.local.TrainingSession? = null,
val nextTrainingProgram: String = "",
val nextTrainingBlockDay: Int = 0,
val nextTrainingReps: Int = 0
)

View file

@ -0,0 +1,31 @@
package de.patani.kettlebelltracker.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.patani.kettlebelltracker.data.datastore.Settings
import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(private val settingsDataStore: SettingsDataStore) : ViewModel() {
val settings = settingsDataStore.settingsFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Settings(0,0.0,0.0,0,""))
fun saveSettings(
time: Int,
sets: Int,
weightLeft: Double,
weightRight: Double
) = viewModelScope.launch {
val currentSettings = settings.value
settingsDataStore.saveSettings(
currentSettings.copy(
trainingTimeMinutes = time,
goalSets = sets,
weightLeft = weightLeft,
weightRight = weightRight
)
)
}
}

View file

@ -0,0 +1,256 @@
package de.patani.kettlebelltracker.viewmodels
import android.media.AudioManager
import android.media.ToneGenerator
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.patani.kettlebelltracker.data.local.TrainingSessionDao
import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
import de.patani.kettlebelltracker.repositories.ApiRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.math.min
data class TrainingState(
val isTrainingRunning: Boolean = false,
val remainingSeconds: Int = 0,
val initialDurationSeconds: Int = 0,
val setsDone: Int = 0,
val goalSets: Int = 5,
val repsPerSet: Int = 5,
val progress: Float = 0.0f,
val currentProgram: String = "clean_1.0",
val currentBlockDay: Int = 1,
val currentReps: Int = 5,
val totalTrainingDays: Int = 0,
val isRoundActive: Boolean = false,
val currentRoundTime: Int = 0,
val totalRoundTime: Int = 0
)
class TrainingViewModel(
private val dao: TrainingSessionDao,
private val settingsDataStore: SettingsDataStore,
private val apiRepository: ApiRepository,
private val appUUID: String
) : ViewModel() {
private val _trainingState = MutableStateFlow(TrainingState())
val trainingState = _trainingState.asStateFlow()
private var timerJob: Job? = null
private var roundTimerJob: Job? = null
init {
viewModelScope.launch {
val trainingCount = dao.getTrainingCount().first()
val initialState = calculateStateByDayCount(trainingCount)
_trainingState.update {
it.copy(
totalTrainingDays = trainingCount,
currentProgram = initialState.program,
currentBlockDay = initialState.blockDay,
currentReps = initialState.reps,
repsPerSet = initialState.reps
)
}
}
}
private var recommendedRestSeconds: Int = 90
fun startTraining() {
if (_trainingState.value.isTrainingRunning) return
viewModelScope.launch {
val settings = settingsDataStore.settingsFlow.first()
val durationSeconds = settings.trainingTimeMinutes * 60
_trainingState.update {
it.copy(
isTrainingRunning = true,
initialDurationSeconds = durationSeconds,
remainingSeconds = durationSeconds,
goalSets = settings.goalSets,
setsDone = 0,
progress = 0.0f
)
}
val recommendation = apiRepository.getRecommendedRest(
uuid = appUUID,
repsPerSet = _trainingState.value.repsPerSet,
currentSets = 0 // ggf. 0 oder deinen Startwert
)
recommendedRestSeconds = recommendation?.recommended_rest ?: 90
Log.d("Training", "using rest time ${recommendedRestSeconds.toString()}")
startTimer()
startFirstRound()
}
}
private fun startTimer() {
timerJob?.cancel()
timerJob = viewModelScope.launch {
while (_trainingState.value.remainingSeconds > 0 && _trainingState.value.isTrainingRunning) {
delay(1000)
_trainingState.update { it.copy(remainingSeconds = it.remainingSeconds - 1) }
}
if (_trainingState.value.isTrainingRunning) {
finishTraining()
}
}
}
private fun startFirstRound() {
startRoundTimer(recommendedRestSeconds)
}
fun completeSet() {
if (!_trainingState.value.isTrainingRunning) return
val currentState = _trainingState.value
val newSetsDone = currentState.setsDone + 1
val newProgress = if (currentState.goalSets > 0) {
min(newSetsDone.toFloat() / currentState.goalSets.toFloat(), 1.0f)
} else 0.0f
_trainingState.update {
it.copy(
setsDone = newSetsDone,
progress = newProgress
)
}
stopRoundTimer()
startNextRound()
}
private fun startNextRound() {
startRoundTimer(recommendedRestSeconds)
}
private fun startRoundTimer(seconds: Int) {
roundTimerJob?.cancel()
_trainingState.update {
it.copy(
isRoundActive = true,
currentRoundTime = seconds,
totalRoundTime = seconds
)
}
roundTimerJob = viewModelScope.launch {
while (_trainingState.value.currentRoundTime > 0 && _trainingState.value.isRoundActive) {
delay(1000)
_trainingState.update {
it.copy(currentRoundTime = it.currentRoundTime - 1)
}
}
if (_trainingState.value.isRoundActive) {
playRoundCompleteSound()
}
}
}
private fun stopRoundTimer() {
roundTimerJob?.cancel()
_trainingState.update {
it.copy(
isRoundActive = false,
currentRoundTime = 0,
totalRoundTime = 0
)
}
}
private fun playRoundCompleteSound() {
viewModelScope.launch {
try {
val toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100)
toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 1000)
} catch (e: Exception) {
Log.e("TrainingViewModel", "Could not play sound", e)
}
}
}
fun finishTraining() {
timerJob?.cancel()
roundTimerJob?.cancel()
if (!_trainingState.value.isTrainingRunning) return
viewModelScope.launch {
val state = _trainingState.value
val settings = settingsDataStore.settingsFlow.first()
val session = de.patani.kettlebelltracker.data.local.TrainingSession(
date = Date(),
sets = state.setsDone,
weightLeft = settings.weightLeft,
weightRight = settings.weightRight,
repsPerSet = state.repsPerSet,
duration = (state.initialDurationSeconds - state.remainingSeconds).toLong(),
program = state.currentProgram,
blockDay = state.currentBlockDay
)
dao.insert(session)
Log.d("Training", "Sending Trainingsession to backend: $appUUID")
apiRepository.sendTrainingData(session, appUUID)
resetTraining()
}
}
private suspend fun resetTraining() {
val trainingCount = dao.getTrainingCount().first()
val nextState = calculateStateByDayCount(trainingCount)
_trainingState.value = TrainingState(
totalTrainingDays = trainingCount,
currentProgram = nextState.program,
currentBlockDay = nextState.blockDay,
currentReps = nextState.reps,
repsPerSet = nextState.reps
)
}
private fun calculateStateByDayCount(totalDays: Int): ProgramState {
if (totalDays <= 0) {
return ProgramState("clean_1.0", 1, 5)
}
val cycleIndex = (totalDays / 12) % 6
val programs = listOf("clean_1.0", "snatch_1.0", "clean_1.1", "snatch_1.1", "clean_1.2", "snatch_1.2")
val program = programs[cycleIndex]
val blockDay = (totalDays % 3) + 1
val repsMap = mapOf(
"clean_1.0" to listOf(5, 6, 4),
"clean_1.1" to listOf(6, 8, 7),
"clean_1.2" to listOf(7, 9, 8),
"snatch_1.0" to listOf(5, 6, 4),
"snatch_1.1" to listOf(6, 8, 7),
"snatch_1.2" to listOf(7, 9, 8)
)
val reps = repsMap[program]?.getOrNull(blockDay - 1) ?: 5
return ProgramState(program, blockDay, reps)
}
override fun onCleared() {
super.onCleared()
timerJob?.cancel()
roundTimerJob?.cancel()
}
data class ProgramState(val program: String, val blockDay: Int, val reps: Int)
}

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">kettlebelltracker</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Kettlebelltracker" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View file

@ -0,0 +1,17 @@
package de.patani.kettlebelltracker
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

Binary file not shown.

Binary file not shown.

6
build.gradle.kts Normal file
View file

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View file

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

36
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,36 @@
[versions]
agp = "8.11.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
roomCommonJvm = "2.7.2"
roomCompiler = "2.7.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "roomCommonJvm" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomCompiler" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

34
ios/.gitignore vendored
View file

@ -1,34 +0,0 @@
**/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

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View file

@ -1 +0,0 @@
#include "Generated.xcconfig"

View file

@ -1 +0,0 @@
#include "Generated.xcconfig"

View file

@ -1,616 +0,0 @@
// !$*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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 = 12.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.kettlebellTracker;
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.kettlebellTracker.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.kettlebellTracker.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.kettlebellTracker.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 = 12.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 = 12.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.kettlebellTracker;
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.kettlebellTracker;
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 */;
}

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -1,13 +0,0 @@
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)
}
}

View file

@ -1,122 +0,0 @@
{
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Some files were not shown because too many files have changed in this diff Show more