diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index aa724b7..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,15 +0,0 @@
-*.iml
-.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-.DS_Store
-/build
-/captures
-.externalNativeBuild
-.cxx
-local.properties
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
deleted file mode 100644
index ac4f03b..0000000
--- a/app/build.gradle.kts
+++ /dev/null
@@ -1,205 +0,0 @@
-//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")
-}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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
\ No newline at end of file
diff --git a/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt
deleted file mode 100644
index 4f6a8dd..0000000
--- a/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-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)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
deleted file mode 100644
index f314948..0000000
--- a/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt b/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt
deleted file mode 100644
index a6a7080..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-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 createViewModelFactory(modelClass: Class): ViewModelProvider.Factory {
- return object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): 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) -> 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)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt b/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt
deleted file mode 100644
index b6222ed..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-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 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 = 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
-)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt
deleted file mode 100644
index 087f901..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt
deleted file mode 100644
index 213a881..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-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
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt
deleted file mode 100644
index ec91096..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-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
-)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt
deleted file mode 100644
index 1772939..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-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>
-
- @Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 20")
- fun getHistory(): Flow>
-
- @Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 1")
- fun getLastSession(): Flow
-
- @Query("SELECT COUNT(*) FROM training_session")
- fun getTrainingCount(): Flow
-
- @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)
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt b/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt
deleted file mode 100644
index ff141cc..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-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
-
- @POST("trainings/recommend-rest")
- suspend fun getRecommendedRest(@Body request: RestRecommendationRequest): Response>
-
-}
-
-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(
- 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?
-)
diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt b/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt
deleted file mode 100644
index 01ad4a1..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package de.patani.kettlebelltracker.data.remote
-
-data class TrainingPayload(
- val reps: Int,
- val rest: Double,
- val sets: Int,
- val uuid: String
-)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt b/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt
deleted file mode 100644
index d6f4768..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-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
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt
deleted file mode 100644
index b79bd02..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-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)
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt
deleted file mode 100644
index ece7ac8..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt
+++ /dev/null
@@ -1,229 +0,0 @@
-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")
- }
- }
- )
-}
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt
deleted file mode 100644
index 56f6c3a..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-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)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt
deleted file mode 100644
index d85f717..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-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")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt
deleted file mode 100644
index 0a76673..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-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)
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt
deleted file mode 100644
index dba3d63..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt
deleted file mode 100644
index ad317fc..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-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
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt
deleted file mode 100644
index f6ed694..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-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
- )
-)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt b/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt
deleted file mode 100644
index 5bd9038..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-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)
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt
deleted file mode 100644
index 1827055..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-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)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt
deleted file mode 100644
index c662d01..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-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
-)
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt
deleted file mode 100644
index 0c61f5c..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-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
- )
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt
deleted file mode 100644
index 51aaf2e..0000000
--- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt
+++ /dev/null
@@ -1,256 +0,0 @@
-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)
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9..0000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index f8c6127..0000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index d1aa070..0000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- kettlebelltracker
-
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
deleted file mode 100644
index 9a13f97..0000000
--- a/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
deleted file mode 100644
index 4df9255..0000000
--- a/app/src/main/res/xml/backup_rules.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
deleted file mode 100644
index 9ee9997..0000000
--- a/app/src/main/res/xml/data_extraction_rules.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/test/java/de/patani/kettlebelltracker/ExampleUnitTest.kt b/app/src/test/java/de/patani/kettlebelltracker/ExampleUnitTest.kt
deleted file mode 100644
index 1a8aa6a..0000000
--- a/app/src/test/java/de/patani/kettlebelltracker/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-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)
- }
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index 952b930..0000000
--- a/build.gradle.kts
+++ /dev/null
@@ -1,6 +0,0 @@
-// 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
-}
\ No newline at end of file
diff --git a/cmd/FyneApp.toml b/cmd/FyneApp.toml
new file mode 100644
index 0000000..d8806a7
--- /dev/null
+++ b/cmd/FyneApp.toml
@@ -0,0 +1,8 @@
+Website = "https://patanix.de"
+
+[Details]
+ Icon = "Icon.png"
+ Name = "kettlebell_tracker"
+ ID = "de.patanix.kettlebell_tracker"
+ Version = "1.0.0"
+ Build = 5
diff --git a/cmd/Icon.png b/cmd/Icon.png
new file mode 100644
index 0000000..9de0222
Binary files /dev/null and b/cmd/Icon.png differ
diff --git a/cmd/build.sh b/cmd/build.sh
new file mode 100644
index 0000000..7f29a05
--- /dev/null
+++ b/cmd/build.sh
@@ -0,0 +1 @@
+fyne package -os android -release --tags -ldflags="-s -w"
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..594f4e3
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "log"
+ "path/filepath"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/theme"
+
+ "git.patanix.de/git/kettlebell-app/internal/data"
+ "git.patanix.de/git/kettlebell-app/internal/services"
+ "git.patanix.de/git/kettlebell-app/internal/ui"
+)
+
+func main() {
+ myApp := app.NewWithID("com.example.kettlebell-tracker")
+ mainWindow := myApp.NewWindow("Kettlebell Programm Tracker")
+
+ dbDir := myApp.Storage().RootURI().Path()
+ dbPath := filepath.Join(dbDir, "kb_training.db")
+ log.Println("Datenbankpfad:", dbPath)
+
+ dbService, err := data.NewDatabaseService(dbPath)
+ if err != nil {
+ log.Fatalf("Fehler bei der Initialisierung der Datenbank: %v", err)
+ }
+
+ settingsService := services.NewSettingsService(myApp)
+ apiService := services.NewApiService(myApp.UniqueID())
+
+ trainingService := services.NewTrainingService(dbService, settingsService, apiService)
+ // Dark Mode nach Systemeinstellung
+ if fyne.CurrentDevice().IsMobile() {
+ myApp.Settings().SetTheme(theme.DarkTheme())
+ }
+
+ // mainWindow := myApp.NewWindow("Kettlebell Tracker")
+ mainWindow.SetMaster()
+
+ // Responsive Layout
+ content := container.NewMax()
+ nav := buildNavigation(content, mainWindow, dbService, settingsService, apiService, trainingService) // Eigene Nav-Komponente
+
+ mainWindow.SetContent(container.NewBorder(nav, nil, nil, nil, content))
+ mainWindow.Resize(fyne.NewSize(400, 600))
+ mainWindow.ShowAndRun()
+}
+
+func buildNavigation(content *fyne.Container, mainWindow fyne.Window, dbService *data.DatabaseService, settingsService *services.SettingsService, apiService *services.ApiService, trainingService *services.TrainingService) fyne.CanvasObject {
+ navItems := []struct {
+ icon fyne.Resource
+ title string
+ view func() fyne.CanvasObject
+ }{
+ {theme.HomeIcon(), "Home", ui.MakeHomeScreen},
+ {theme.MediaPlayIcon(), "Training", func() fyne.CanvasObject {
+ return ui.MakeTrainingScreen(trainingService, settingsService, mainWindow)
+ }},
+ {theme.HistoryIcon(), "Historie", func() fyne.CanvasObject {
+ return ui.MakeHistoryScreen(dbService, mainWindow)
+ }},
+ {theme.SettingsIcon(), "Einstellungen", func() fyne.CanvasObject {
+ return ui.MakeSettingsScreen(settingsService, mainWindow)
+ }},
+ }
+
+ nav := container.NewAppTabs()
+ for _, item := range navItems {
+ nav.Append(container.NewTabItemWithIcon(item.title, item.icon, item.view()))
+ }
+ nav.SetTabLocation(container.TabLocationBottom)
+ nav.OnSelected = func(t *container.TabItem) {
+ content.Objects = []fyne.CanvasObject{t.Content}
+ content.Refresh()
+ }
+
+ return nav
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3975480
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,52 @@
+module git.patanix.de/git/kettlebell-app
+
+go 1.24.4
+
+require (
+ fyne.io/fyne/v2 v2.6.1
+ modernc.org/sqlite v1.38.0
+)
+
+require (
+ fyne.io/systray v1.11.0 // indirect
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/fredbi/uri v1.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fyne-io/gl-js v0.1.0 // indirect
+ github.com/fyne-io/glfw-js v0.2.0 // indirect
+ github.com/fyne-io/image v0.1.1 // indirect
+ github.com/fyne-io/oksvg v0.1.0 // indirect
+ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
+ github.com/go-text/render v0.2.0 // indirect
+ github.com/go-text/typesetting v0.2.1 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/hack-pad/go-indexeddb v0.3.2 // indirect
+ github.com/hack-pad/safejs v0.1.0 // indirect
+ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
+ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/rymdport/portal v0.4.1 // indirect
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
+ golang.org/x/image v0.24.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ modernc.org/libc v1.65.10 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f2fb41a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,123 @@
+fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
+fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
+fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
+fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
+github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
+github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
+github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
+github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
+github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
+github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
+github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
+github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
+github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
+github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
+github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
+github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
+github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
+github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
+github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
+github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
+github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
+github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
+modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
+modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
+modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
+modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
+modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
+modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/gradle.properties b/gradle.properties
deleted file mode 100644
index 20e2a01..0000000
--- a/gradle.properties
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
deleted file mode 100644
index 27a3ee0..0000000
--- a/gradle/libs.versions.toml
+++ /dev/null
@@ -1,36 +0,0 @@
-[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" }
-
diff --git a/internal/data/database.go b/internal/data/database.go
new file mode 100644
index 0000000..4989cc9
--- /dev/null
+++ b/internal/data/database.go
@@ -0,0 +1,117 @@
+package data
+
+import (
+ "database/sql"
+ "log"
+ "time"
+
+ _ "modernc.org/sqlite" // Importiert den SQLite-Treiber
+)
+
+type DatabaseService struct {
+ DB *sql.DB
+}
+
+func NewDatabaseService(dbPath string) (*DatabaseService, error) {
+ db, err := sql.Open("sqlite", dbPath)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = db.Ping(); err != nil {
+ return nil, err
+ }
+
+ createTableSQL := `
+ CREATE TABLE IF NOT EXISTS training (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ date TEXT NOT NULL,
+ sets INTEGER,
+ weightLeft REAL,
+ weightRight REAL,
+ repsPerSet INTEGER,
+ duration INTEGER,
+ program TEXT,
+ blockDay INTEGER
+ );`
+
+ _, err = db.Exec(createTableSQL)
+ if err != nil {
+ log.Printf("Fehler beim Erstellen der Tabelle: %v", err)
+ return nil, err
+ }
+
+ // Hier könnten wir auch komplexere Migrationen wie dein _onUpgrade handle,
+ // aber für den Anfang reicht das Erstellen der Tabelle.
+
+ log.Println("Datenbank erfolgreich initialisiert.")
+ return &DatabaseService{DB: db}, nil
+}
+
+func (s *DatabaseService) SaveTraining(session *TrainingSession) error {
+ dateStr := session.Date.Format(time.RFC3339)
+
+ query := `
+ INSERT INTO training (id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET
+ date = excluded.date,
+ sets = excluded.sets,
+ weightLeft = excluded.weightLeft,
+ weightRight = excluded.weightRight,
+ repsPerSet = excluded.repsPerSet,
+ duration = excluded.duration,
+ program = excluded.program,
+ blockDay = excluded.blockDay;
+ `
+ var id any
+ if session.ID != 0 {
+ id = session.ID
+ }
+
+ _, err := s.DB.Exec(query, id, dateStr, session.Sets, session.WeightLeft, session.WeightRight, session.RepsPerSet, session.Duration, session.Program, session.BlockDay)
+ return err
+}
+
+func (s *DatabaseService) GetTrainingCount() (int, error) {
+ var count int
+ query := "SELECT COUNT(*) FROM training;"
+ err := s.DB.QueryRow(query).Scan(&count)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return 0, nil
+ }
+ return 0, err
+ }
+ return count, nil
+}
+
+func (s *DatabaseService) GetHistory() ([]TrainingSession, error) {
+ query := `SELECT id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay FROM training ORDER BY date DESC LIMIT 20;`
+
+ rows, err := s.DB.Query(query)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var sessions []TrainingSession
+ for rows.Next() {
+ var s TrainingSession
+ var dateStr string
+
+ err := rows.Scan(&s.ID, &dateStr, &s.Sets, &s.WeightLeft, &s.WeightRight, &s.RepsPerSet, &s.Duration, &s.Program, &s.BlockDay)
+ if err != nil {
+ return nil, err
+ }
+
+ s.Date, err = time.Parse(time.RFC3339, dateStr)
+ if err != nil {
+ return nil, err
+ }
+
+ sessions = append(sessions, s)
+ }
+
+ return sessions, nil
+}
diff --git a/internal/data/models.go b/internal/data/models.go
new file mode 100644
index 0000000..8129f3c
--- /dev/null
+++ b/internal/data/models.go
@@ -0,0 +1,17 @@
+package data
+
+import "time"
+
+// TrainingSession repräsentiert eine einzelne Trainingseinheit.
+// Die `db`-Tags werden verwendet, um die Struct-Felder den Datenbankspalten zuzuordnen.
+type TrainingSession struct {
+ ID int64 `db:"id"`
+ Date time.Time `db:"date"`
+ Sets int64 `db:"sets"`
+ WeightLeft float64 `db:"weightLeft"`
+ WeightRight float64 `db:"weightRight"`
+ RepsPerSet int64 `db:"repsPerSet"`
+ Duration int64 `db:"duration"` // in Sekunden
+ Program string `db:"program"`
+ BlockDay int64 `db:"blockDay"`
+}
diff --git a/internal/services/api.go b/internal/services/api.go
new file mode 100644
index 0000000..8a3b782
--- /dev/null
+++ b/internal/services/api.go
@@ -0,0 +1,90 @@
+package services
+
+import (
+ "bytes"
+ "encoding/json"
+ "log"
+ "net/http"
+ "time"
+
+ "git.patanix.de/git/kettlebell-app/internal/data"
+)
+
+// TrainingPayload ist die JSON-Struktur, die an das Backend gesendet wird.
+// Die `json:"..."`-Tags stellen sicher, dass die Feldnamen im JSON korrekt sind.
+type TrainingPayload struct {
+ Reps int `json:"reps"`
+ Rest float64 `json:"rest"`
+ Sets int `json:"sets"`
+ UUID string `json:"uuid"`
+}
+
+// ApiService kümmert sich um die Kommunikation mit dem Backend.
+type ApiService struct {
+ client *http.Client
+ endpoint string
+ uuid string
+}
+
+// NewApiService erstellt einen neuen Service für die API-Kommunikation.
+func NewApiService(appUUID string) *ApiService {
+ return &ApiService{
+ // Erstellt einen HTTP-Client mit einem 5-Sekunden-Timeout, genau wie in deiner Flutter-App.
+ client: &http.Client{
+ Timeout: 5 * time.Second,
+ },
+ endpoint: "http://192.168.178.43:8080/trainings/",
+ uuid: appUUID,
+ }
+}
+
+// SendTrainingData sendet eine abgeschlossene Trainingseinheit an das Backend.
+func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
+ // Berechnung für 'rest' durchführen.
+ var rest float64
+ if session.Sets > 0 {
+ rest = float64(session.Duration) / float64(session.Sets)
+ }
+
+ // Die zu sendenden Daten vorbereiten.
+ payload := TrainingPayload{
+ Reps: int(session.RepsPerSet),
+ Rest: rest,
+ Sets: int(session.Sets),
+ UUID: s.uuid,
+ }
+
+ // Daten in JSON umwandeln.
+ jsonData, err := json.Marshal(payload)
+ if err != nil {
+ log.Printf("API Fehler: Konnte Payload nicht in JSON umwandeln: %v", err)
+ return
+ }
+
+ // Den HTTP-Request erstellen.
+ req, err := http.NewRequest("POST", s.endpoint, bytes.NewBuffer(jsonData))
+ if err != nil {
+ log.Printf("API Fehler: Konnte Request nicht erstellen: %v", err)
+ return
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ // Request senden.
+ log.Printf("Sende Training an Backend: %s", string(jsonData))
+ resp, err := s.client.Do(req)
+ if err != nil {
+ log.Printf("API Fehler: Fehler beim Senden des Trainings: %v", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ // Antwort des Servers prüfen.
+ if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
+ log.Println("Training erfolgreich an Backend gesendet.")
+ } else {
+ log.Printf("API Fehler: Unerwarteter Statuscode: %s", resp.Status)
+ // Optional: Den Body der Antwort lesen, um mehr Details zu erhalten.
+ // body, _ := io.ReadAll(resp.Body)
+ // log.Printf("Antwort-Body: %s", string(body))
+ }
+}
diff --git a/internal/services/settings.go b/internal/services/settings.go
new file mode 100644
index 0000000..a00e55b
--- /dev/null
+++ b/internal/services/settings.go
@@ -0,0 +1,41 @@
+package services
+
+import (
+ "fyne.io/fyne/v2"
+)
+
+type Settings struct {
+ TrainingTimeMinutes int
+ WeightLeft float64
+ WeightRight float64
+ GoalSets int
+ InitialProgram string
+}
+
+type SettingsService struct {
+ prefs fyne.Preferences
+}
+
+func NewSettingsService(app fyne.App) *SettingsService {
+ return &SettingsService{
+ prefs: app.Preferences(),
+ }
+}
+
+func (s *SettingsService) LoadSettings() *Settings {
+ return &Settings{
+ TrainingTimeMinutes: s.prefs.IntWithFallback("trainingTimeMinutes", 20),
+ WeightLeft: s.prefs.FloatWithFallback("weightLeft", 16.0),
+ WeightRight: s.prefs.FloatWithFallback("weightRight", 16.0),
+ GoalSets: s.prefs.IntWithFallback("goalSets", 5),
+ InitialProgram: s.prefs.StringWithFallback("initialProgram", "giant_1.0"),
+ }
+}
+
+func (s *SettingsService) SaveSettings(settings *Settings) {
+ s.prefs.SetInt("trainingTimeMinutes", settings.TrainingTimeMinutes)
+ s.prefs.SetFloat("weightLeft", settings.WeightLeft)
+ s.prefs.SetFloat("weightRight", settings.WeightRight)
+ s.prefs.SetInt("goalSets", settings.GoalSets)
+ s.prefs.SetString("initialProgram", settings.InitialProgram)
+}
diff --git a/internal/services/training.go b/internal/services/training.go
new file mode 100644
index 0000000..9a0c11d
--- /dev/null
+++ b/internal/services/training.go
@@ -0,0 +1,356 @@
+// package services
+//
+// import (
+//
+// "log"
+// "time"
+//
+// "git.patanix.de/git/kettlebell-app/internal/data"
+//
+// )
+//
+// // TrainingState hält den aktuellen Zustand einer laufenden Trainingseinheit.
+//
+// type TrainingState struct {
+// IsTrainingRunning bool
+// RemainingSeconds int
+// InitialDurationSeconds int
+// SetsDone int
+// GoalSets int
+// RepsPerSet int
+// SetTimes []time.Time
+// Progress float64
+// SecondsSinceLastSet int
+// LastSetTimestamp *time.Time
+// CurrentProgram string
+// CurrentBlockDay int
+// CurrentReps int
+// TotalTrainingDays int
+// }
+//
+// func NewTrainingState() *TrainingState {
+// return &TrainingState{
+// IsTrainingRunning: false,
+// RemainingSeconds: 0,
+// InitialDurationSeconds: 0,
+// SetsDone: 0,
+// GoalSets: 5,
+// RepsPerSet: 5,
+// Progress: 0.0,
+// SecondsSinceLastSet: 0,
+// LastSetTimestamp: nil,
+// CurrentProgram: "giant_1.0",
+// CurrentBlockDay: 1,
+// CurrentReps: 5,
+// TotalTrainingDays: 0,
+// SetTimes: []time.Time{},
+// }
+// }
+//
+// type TrainingService struct {
+// State *TrainingState
+// dbService *data.DatabaseService
+// settingsService *SettingsService
+// }
+//
+// func NewTrainingService(db *data.DatabaseService, settings *SettingsService) *TrainingService {
+// initialState := NewTrainingState()
+// trainingCount, err := db.GetTrainingCount()
+// if err != nil {
+// log.Printf("Fehler beim Abrufen der Trainingsanzahl, setze auf 0: %v", err)
+// initialState.TotalTrainingDays = 0
+// } else {
+// initialState.TotalTrainingDays = trainingCount
+// }
+// return &TrainingService{
+// State: initialState,
+// dbService: db,
+// settingsService: settings,
+// }
+// }
+//
+// func (s *TrainingService) updateProgram() {
+// st := s.State
+// newTotalDays := st.TotalTrainingDays + 1
+// newProgram := st.CurrentProgram
+// newDay := (st.CurrentBlockDay % 3) + 1
+// newReps := st.CurrentReps
+//
+// if newTotalDays > 0 && newTotalDays%12 == 0 {
+// switch st.CurrentProgram {
+// case "giant_1.0":
+// newProgram = "ksk_1.0"
+// case "giant_1.1":
+// newProgram = "ksk_1.1"
+// case "giant_1.2":
+// newProgram = "ksk_1.2"
+// case "ksk_1.0":
+// newProgram = "giant_1.1"
+// case "ksk_1.1":
+// newProgram = "giant_1.2"
+// case "ksk_1.2":
+// newProgram = "giant_1.0"
+// default:
+// newProgram = "giant_1.0"
+// }
+// newDay = 1
+// }
+//
+// repsMap := map[string][]int{
+// "giant_1.0": {5, 6, 4},
+// "giant_1.1": {6, 8, 7},
+// "giant_1.2": {7, 9, 8},
+// "ksk_1.0": {5, 6, 4},
+// "ksk_1.1": {6, 8, 7},
+// "ksk_1.2": {7, 9, 8},
+// }
+//
+// if reps, ok := repsMap[newProgram]; ok && len(reps) >= newDay {
+// newReps = reps[newDay-1]
+// } else {
+// newReps = 5
+// }
+//
+// st.CurrentProgram = newProgram
+// st.CurrentBlockDay = newDay
+// st.CurrentReps = newReps
+// st.TotalTrainingDays = newTotalDays
+// }
+//
+// func (s *TrainingService) StartTraining(minutes, goal int) {
+// s.updateProgram()
+// duration := minutes * 60
+// s.State = &TrainingState{
+// IsTrainingRunning: true,
+// InitialDurationSeconds: duration,
+// RemainingSeconds: duration,
+// GoalSets: goal,
+// RepsPerSet: s.State.CurrentReps,
+// CurrentProgram: s.State.CurrentProgram,
+// CurrentBlockDay: s.State.CurrentBlockDay,
+// TotalTrainingDays: s.State.TotalTrainingDays,
+// SetTimes: []time.Time{},
+// }
+// }
+//
+// func (s *TrainingService) Tick() {
+// if s.State.RemainingSeconds > 0 {
+// s.State.RemainingSeconds--
+// }
+// }
+//
+// func (s *TrainingService) TickLastSetTimer() {
+// if s.State.IsTrainingRunning && s.State.LastSetTimestamp != nil {
+// s.State.SecondsSinceLastSet = int(time.Since(*s.State.LastSetTimestamp).Seconds())
+// }
+// }
+//
+// func (s *TrainingService) CompleteSet() {
+// st := s.State
+// st.SetsDone++
+// now := time.Now()
+// st.SetTimes = append(st.SetTimes, now)
+// if st.GoalSets > 0 {
+// st.Progress = min(float64(st.SetsDone)/float64(st.GoalSets), 1.0)
+// }
+// st.LastSetTimestamp = &now
+// st.SecondsSinceLastSet = 0
+// }
+//
+// func (s *TrainingService) FinishTraining(session *data.TrainingSession) error {
+// session.Program = s.State.CurrentProgram
+// session.BlockDay = int64(s.State.CurrentBlockDay)
+//
+// err := s.dbService.SaveTraining(session)
+// if err != nil {
+// return err
+// }
+//
+// // Platzhalter für den API-Aufruf (aus api_service.dart)
+// s.sendToBackend(session)
+//
+// s.ResetTraining()
+// return nil
+// }
+//
+// func (s *TrainingService) ResetTraining() {
+// // Diesen Teil nochmals pruefen
+// s.State = NewTrainingState()
+// trainingCount, err := s.dbService.GetTrainingCount()
+// if err != nil {
+// log.Print("Unable to get training count")
+// }
+// s.State.CurrentBlockDay = trainingCount
+// // Hier müsste man TotalTrainingDays wieder korrekt laden.
+// }
+//
+// // sendToBackend ist ein Platzhalter für deinen API-Aufruf.
+//
+// func (s *TrainingService) sendToBackend(session *data.TrainingSession) {
+// // Hier würde die Logik aus deinem `api_service.dart` hinkommen.
+// // z.B. ein HTTP POST Request mit den Trainingsdaten.
+// // Da der Service nicht existiert, loggen wir es nur.
+// log.Println("Sende Trainingsdaten an das Backend (Platzhalter)...")
+// // rest := float64(session.Duration) / float64(session.Sets)
+// // log.Printf("Reps: %d, Rest: %.2f, Sets: %d", session.RepsPerSet, rest, session.Sets)
+// }
+package services
+
+import (
+ "log"
+ "time"
+
+ "git.patanix.de/git/kettlebell-app/internal/data"
+)
+
+type TrainingState struct {
+ IsTrainingRunning bool
+ RemainingSeconds int
+ InitialDurationSeconds int
+ SetsDone int
+ GoalSets int
+ RepsPerSet int
+ SetTimes []time.Time
+ Progress float64
+ SecondsSinceLastSet int
+ LastSetTimestamp *time.Time
+ CurrentProgram string
+ CurrentBlockDay int
+ CurrentReps int
+ TotalTrainingDays int
+}
+
+func calculateStateByDayCount(totalDays int) (program string, blockDay, reps int) {
+ program = "giant_1.0"
+ blockDay = 1
+ reps = 5
+
+ if totalDays > 0 {
+ cycleIndex := (totalDays / 12) % 6
+
+ programs := []string{"giant_1.0", "ksk_1.0", "giant_1.1", "ksk_1.1", "giant_1.2", "ksk_1.2"}
+ program = programs[cycleIndex]
+
+ blockDay = (totalDays % 3) + 1
+ }
+
+ repsMap := map[string][]int{
+ "giant_1.0": {5, 6, 4},
+ "giant_1.1": {6, 8, 7},
+ "giant_1.2": {7, 9, 8},
+ "ksk_1.0": {5, 6, 4},
+ "ksk_1.1": {6, 8, 7},
+ "ksk_1.2": {7, 9, 8},
+ }
+
+ if r, ok := repsMap[program]; ok && len(r) >= blockDay {
+ reps = r[blockDay-1]
+ }
+
+ return
+}
+
+func NewTrainingState(db *data.DatabaseService) *TrainingState {
+ trainingCount, err := db.GetTrainingCount()
+ if err != nil {
+ log.Printf("Fehler beim Abrufen der Trainingsanzahl, setze auf 0: %v", err)
+ trainingCount = 0
+ }
+
+ program, blockDay, reps := calculateStateByDayCount(trainingCount)
+
+ return &TrainingState{
+ IsTrainingRunning: false,
+ TotalTrainingDays: trainingCount,
+ CurrentProgram: program,
+ CurrentBlockDay: blockDay,
+ CurrentReps: reps,
+ SetTimes: []time.Time{},
+ GoalSets: 5,
+ RepsPerSet: reps,
+ }
+}
+
+type TrainingService struct {
+ State *TrainingState
+ dbService *data.DatabaseService
+ settingsService *SettingsService
+ apiService *ApiService
+}
+
+func NewTrainingService(db *data.DatabaseService, settings *SettingsService, api *ApiService) *TrainingService {
+ return &TrainingService{
+ State: NewTrainingState(db),
+ dbService: db,
+ settingsService: settings,
+ apiService: api,
+ }
+}
+
+func (s *TrainingService) StartTraining(minutes, goal int) {
+ program, blockDay, reps := calculateStateByDayCount(s.State.TotalTrainingDays)
+
+ st := s.State
+ st.IsTrainingRunning = true
+ st.InitialDurationSeconds = minutes * 60
+ st.RemainingSeconds = st.InitialDurationSeconds
+ st.GoalSets = goal
+ st.CurrentProgram = program
+ st.CurrentBlockDay = blockDay
+ st.CurrentReps = reps
+ st.RepsPerSet = reps
+
+ st.SetsDone = 0
+ st.Progress = 0.0
+ st.SetTimes = []time.Time{}
+ st.LastSetTimestamp = nil
+}
+
+func (s *TrainingService) Tick() {
+ if s.State.RemainingSeconds > 0 {
+ s.State.RemainingSeconds--
+ }
+}
+
+func (s *TrainingService) TickLastSetTimer() {
+ if s.State.IsTrainingRunning && s.State.LastSetTimestamp != nil {
+ s.State.SecondsSinceLastSet = int(time.Since(*s.State.LastSetTimestamp).Seconds())
+ }
+}
+
+func (s *TrainingService) CompleteSet() {
+ st := s.State
+ st.SetsDone++
+ now := time.Now()
+ st.SetTimes = append(st.SetTimes, now)
+ if st.GoalSets > 0 {
+ st.Progress = min(float64(st.SetsDone)/float64(st.GoalSets), 1.0)
+ }
+ st.LastSetTimestamp = &now
+ st.SecondsSinceLastSet = 0
+}
+
+func (s *TrainingService) FinishTraining(session *data.TrainingSession) error {
+ session.Program = s.State.CurrentProgram
+ session.BlockDay = int64(s.State.CurrentBlockDay)
+
+ err := s.dbService.SaveTraining(session)
+ if err != nil {
+ return err
+ }
+
+ // s.sendToBackend(session)
+ go s.apiService.SendTrainingData(session)
+
+ s.ResetTraining()
+ return nil
+}
+
+func (s *TrainingService) ResetTraining() {
+ s.State = NewTrainingState(s.dbService)
+}
+
+// sendToBackend ist ein Platzhalter für deinen API-Aufruf.
+func (s *TrainingService) sendToBackend(session *data.TrainingSession) {
+ log.Println("Sende Trainingsdaten an das Backend (Platzhalter)...")
+}
diff --git a/internal/ui/history.go b/internal/ui/history.go
new file mode 100644
index 0000000..29825ef
--- /dev/null
+++ b/internal/ui/history.go
@@ -0,0 +1,119 @@
+package ui
+
+import (
+ "fmt"
+ "log"
+
+ "git.patanix.de/git/kettlebell-app/internal/data"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+func formatDuration(totalSeconds int64) string {
+ mins := totalSeconds / 60
+ secs := totalSeconds % 60
+ return fmt.Sprintf("%02d:%02d", mins, secs)
+}
+
+// MakeHistoryScreen erstellt den Bildschirm für die Trainingshistorie.
+func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.CanvasObject {
+ var history []data.TrainingSession
+
+ // Platzhalter, wenn die Liste leer ist
+ placeholder := widget.NewLabel("Noch keine Trainingsdaten vorhanden.")
+ placeholder.Alignment = fyne.TextAlignCenter
+
+ list := widget.NewList(
+ func() int {
+ return len(history)
+ },
+ func() fyne.CanvasObject {
+ // Template für einen Listeneintrag
+ return widget.NewCard("", "", container.NewVBox(
+ widget.NewLabel(""), // Datum
+ widget.NewSeparator(),
+ widget.NewLabel(""), // Sätze
+ widget.NewLabel(""), // Gewicht
+ widget.NewLabel(""), // Reps
+ widget.NewLabel(""), // Dauer
+ ))
+ },
+ func(i widget.ListItemID, o fyne.CanvasObject) {
+ // Template mit Daten füllen
+ session := history[i]
+ card := o.(*widget.Card)
+
+ // Datum als Titel der Karte
+ card.SetTitle(session.Date.Format("02.01.2006 15:04"))
+
+ // Details im Inhalt der Karte
+ box := card.Content.(*fyne.Container)
+ labels := box.Objects
+ labels[0].(*widget.Label).SetText(fmt.Sprintf("Programm: %s - Tag %d", session.Program, session.BlockDay))
+ labels[2].(*widget.Label).SetText(fmt.Sprintf("Sätze: %d", session.Sets))
+ labels[3].(*widget.Label).SetText(fmt.Sprintf("Kettlebells: %.1fkg / %.1fkg", session.WeightLeft, session.WeightRight))
+ labels[4].(*widget.Label).SetText(fmt.Sprintf("Reps pro Satz: %d", session.RepsPerSet))
+ labels[5].(*widget.Label).SetText(fmt.Sprintf("Dauer: %s", formatDuration(session.Duration)))
+ },
+ )
+
+ // Funktion zum Neuladen der Daten
+ refreshData := func() {
+ var err error
+ history, err = db.GetHistory()
+ if err != nil {
+ log.Printf("Fehler beim Laden der Historie: %v", err)
+ dialog.ShowError(err, parent)
+ }
+
+ if len(history) == 0 {
+ placeholder.Show()
+ list.Hide()
+ } else {
+ placeholder.Hide()
+ list.Show()
+ }
+ list.Refresh()
+ }
+
+ // Initiales Laden
+ refreshData()
+
+ // Toolbar mit Refresh-Button
+ // toolbar := widget.NewToolbar(
+ // widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData),
+ // )
+ table := widget.NewTable(
+ func() (int, int) { return len(history), 4 },
+ func() fyne.CanvasObject { return widget.NewLabel("Template") },
+ func(id widget.TableCellID, cell fyne.CanvasObject) {
+ session := history[id.Row]
+ label := cell.(*widget.Label)
+ switch id.Col {
+ case 0:
+ label.SetText(session.Date.Format("02.01"))
+ case 1:
+ label.SetText(fmt.Sprintf("%d Sätze", session.Sets))
+ case 2:
+ label.SetText(fmt.Sprintf("%.1fkg", session.WeightLeft))
+ case 3:
+ label.SetText(formatDuration(session.Duration))
+ }
+ },
+ )
+ table.SetColumnWidth(0, 80)
+ table.SetColumnWidth(1, 100)
+ table.SetColumnWidth(2, 80)
+ table.SetColumnWidth(3, 80)
+
+ // return container.NewBorder(toolbar, nil, nil, nil, container.NewStack(list, placeholder))
+ return container.NewBorder(
+ widget.NewToolbar(widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData)),
+ nil, nil, nil,
+ table,
+ )
+}
diff --git a/internal/ui/home.go b/internal/ui/home.go
new file mode 100644
index 0000000..f8d8566
--- /dev/null
+++ b/internal/ui/home.go
@@ -0,0 +1,76 @@
+package ui
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+// // MakeHomeScreen erstellt den statischen Willkommensbildschirm.
+// func MakeHomeScreen() fyne.CanvasObject {
+// primaryColor := theme.PrimaryColor()
+//
+// title := canvas.NewText("Willkommen beim Giant Programm Tracker!", primaryColor)
+// title.TextStyle.Bold = true
+// title.Alignment = fyne.TextAlignCenter
+// title.TextSize = 24
+//
+// subtitle := widget.NewLabel("Verwalte deine Kettlebell-Trainings effizient.")
+// subtitle.Alignment = fyne.TextAlignCenter
+//
+// icon := widget.NewIcon(theme.MediaPlayIcon())
+// icon.Resize(fyne.NewSize(150, 150))
+//
+// // Layout erstellen, das dem Flutter-Layout entspricht
+// content := container.NewVBox(
+// title,
+// widget.NewSeparator(),
+// subtitle,
+// container.NewPadded(icon), // Icon mit etwas Abstand
+// )
+//
+// // Zentriert den Inhalt
+// return container.NewCenter(content)
+// }
+
+func MakeHomeScreen() fyne.CanvasObject {
+ primary := theme.PrimaryColor()
+ secondary := theme.WarningColor()
+
+ // Header mit personalisierter Begrüßung
+ greeting := canvas.NewText("Dein Kettlebell Fortschritt", primary)
+ greeting.TextSize = 20
+ greeting.Alignment = fyne.TextAlignCenter
+
+ // Fortschrittsvisualisierung (Beispiel)
+ progressRing := canvas.NewCircle(secondary)
+ progressRing.StrokeWidth = 8
+ progressRing.StrokeColor = primary
+ ringContainer := container.NewCenter(progressRing)
+
+ // Programmstatus
+ programInfo := widget.NewLabel("Aktuell: Giant - Woche 2/4")
+ programInfo.Alignment = fyne.TextAlignCenter
+
+ // Primäre Aktion
+ startBtn := widget.NewButtonWithIcon("Training starten", theme.MediaPlayIcon(), func() {})
+ startBtn.Importance = widget.HighImportance
+
+ return container.NewVBox(
+ container.NewPadded(greeting),
+ ringContainer,
+ programInfo,
+ container.NewCenter(startBtn),
+ widget.NewSeparator(),
+ buildQuickStats(), // Eigene Komponente für Statistiken
+ )
+}
+
+func buildQuickStats() fyne.CanvasObject {
+ return container.NewGridWithColumns(2,
+ widget.NewCard("Letztes Training", "12 Sätze", nil),
+ widget.NewCard("Aktuelle Serie", "5 Tage", nil),
+ )
+}
diff --git a/internal/ui/settings.go b/internal/ui/settings.go
new file mode 100644
index 0000000..ed20d57
--- /dev/null
+++ b/internal/ui/settings.go
@@ -0,0 +1,64 @@
+package ui
+
+import (
+ "fmt"
+ "strconv"
+
+ "git.patanix.de/git/kettlebell-app/internal/services"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/widget"
+)
+
+func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
+ currentSettings := settingsService.LoadSettings()
+
+ trainingTimeEntry := widget.NewEntry()
+ trainingTimeEntry.SetText(fmt.Sprintf("%d", currentSettings.TrainingTimeMinutes))
+
+ weightLeftEntry := widget.NewEntry()
+ weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft))
+
+ weightRightEntry := widget.NewEntry()
+ weightRightEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightRight))
+
+ goalSetsEntry := widget.NewEntry()
+ goalSetsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets))
+
+ form := &widget.Form{
+ Items: []*widget.FormItem{
+ {Text: "Trainingszeit (Minuten)", Widget: trainingTimeEntry},
+ {Text: "Linke Kettlebell (kg)", Widget: weightLeftEntry},
+ {Text: "Rechte Kettlebell (kg)", Widget: weightRightEntry},
+ {Text: "Ziel-Sätze", Widget: goalSetsEntry},
+ },
+ OnSubmit: func() {
+ timeMin, err1 := strconv.Atoi(trainingTimeEntry.Text)
+ weightL, err2 := strconv.ParseFloat(weightLeftEntry.Text, 64)
+ weightR, err3 := strconv.ParseFloat(weightRightEntry.Text, 64)
+ goal, err4 := strconv.Atoi(goalSetsEntry.Text)
+
+ if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
+ dialog.ShowError(fmt.Errorf("Bitte gib gültige Zahlen ein"), parent)
+ return
+ }
+
+ newSettings := &services.Settings{
+ TrainingTimeMinutes: timeMin,
+ WeightLeft: weightL,
+ WeightRight: weightR,
+ GoalSets: goal,
+ }
+ settingsService.SaveSettings(newSettings)
+
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: "Gespeichert",
+ Content: "Die Einstellungen wurden erfolgreich aktualisiert.",
+ })
+ },
+ }
+
+ return container.NewPadded(form)
+}
diff --git a/internal/ui/training.go b/internal/ui/training.go
new file mode 100644
index 0000000..e89d9e8
--- /dev/null
+++ b/internal/ui/training.go
@@ -0,0 +1,164 @@
+package ui
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "git.patanix.de/git/kettlebell-app/internal/data"
+ "git.patanix.de/git/kettlebell-app/internal/services"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
+ programLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
+ blockDayLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{})
+ repsLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
+
+ timerLabel := widget.NewLabelWithStyle("00:00", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
+
+ progressBar := widget.NewProgressBar()
+ progressLabel := widget.NewLabelWithStyle("Fortschritt: 0%", fyne.TextAlignCenter, fyne.TextStyle{})
+
+ var startButton, setButton, finishButton *widget.Button
+
+ setHistoryList := widget.NewList(
+ func() int {
+ return len(ts.State.SetTimes)
+ },
+ func() fyne.CanvasObject {
+ return widget.NewLabel("")
+ },
+ func(id widget.ListItemID, obj fyne.CanvasObject) {
+ t := ts.State.SetTimes[id]
+ obj.(*widget.Label).SetText(fmt.Sprintf("#%d um %s (%d Reps)", id+1, t.Format("15:04:05"), ts.State.CurrentReps))
+ },
+ )
+
+ var mainTimer, lastSetTimer *time.Ticker
+
+ updateUI := func() {
+ state := ts.State
+ programLabel.SetText(state.CurrentProgram)
+ blockDayLabel.SetText(fmt.Sprintf("Block Tag: %d", state.CurrentBlockDay))
+ repsLabel.SetText(fmt.Sprintf("Reps pro Satz: %d", state.CurrentReps))
+
+ timerLabel.SetText(formatDuration(int64(state.RemainingSeconds)))
+ progressBar.SetValue(state.Progress)
+ progressLabel.SetText(fmt.Sprintf("Fortschritt: %.0f%%", state.Progress*100))
+
+ if state.IsTrainingRunning {
+ startButton.Disable()
+ setButton.Enable()
+ finishButton.Enable()
+ } else {
+ startButton.Enable()
+ setButton.Disable()
+ finishButton.Disable()
+ }
+ setHistoryList.Refresh()
+ }
+
+ stopTimers := func() {
+ if mainTimer != nil {
+ mainTimer.Stop()
+ mainTimer = nil
+ }
+ if lastSetTimer != nil {
+ lastSetTimer.Stop()
+ lastSetTimer = nil
+ }
+ }
+
+ // startAction := func() {
+ // settings := ss.LoadSettings()
+ // ts.StartTraining(settings.TrainingTimeMinutes, settings.GoalSets)
+ // updateUI()
+ //
+ // mainTimer = time.NewTicker(time.Second)
+ // go func() {
+ // for range mainTimer.C {
+ // if ts.State.RemainingSeconds <= 0 {
+ // stopTimers()
+ // fyne.CurrentApp().SendNotification(&fyne.Notification{
+ // Title: "Zeit abgelaufen!",
+ // Content: "Training wird automatisch gespeichert.",
+ // })
+ // finishButton.OnTapped()
+ // return
+ // }
+ // ts.Tick()
+ // updateUI()
+ // }
+ // }()
+ //
+ // lastSetTimer = time.NewTicker(time.Second)
+ // go func() {
+ // for range lastSetTimer.C {
+ // ts.TickLastSetTimer()
+ // }
+ // }()
+ // }
+
+ setAction := func() {
+ ts.CompleteSet()
+ fyne.CurrentApp().SendNotification(&fyne.Notification{Title: "Satz gespeichert!", Content: ""})
+ updateUI()
+ }
+
+ finishAction := func() {
+ stopTimers()
+ settings := ss.LoadSettings()
+ state := ts.State
+
+ session := &data.TrainingSession{
+ Date: time.Now(),
+ Sets: int64(state.SetsDone),
+ WeightLeft: settings.WeightLeft,
+ WeightRight: settings.WeightRight,
+ RepsPerSet: int64(state.RepsPerSet),
+ Duration: int64(state.InitialDurationSeconds - state.RemainingSeconds),
+ }
+
+ if err := ts.FinishTraining(session); err != nil {
+ dialog.ShowError(err, parent)
+ log.Printf("Fehler beim Speichern des Trainings: %v", err)
+ } else {
+ fyne.CurrentApp().SendNotification(&fyne.Notification{Title: "Training gespeichert!", Content: "Gut gemacht!"})
+ }
+
+ updateUI()
+ }
+
+ timerDisplay := canvas.NewText("00:00", theme.PrimaryColor())
+ timerDisplay.TextSize = 48
+ timerDisplay.Alignment = fyne.TextAlignCenter
+
+ exerciseInfo := widget.NewLabel("Aktuelle Übung: Double Clean & Press")
+ exerciseInfo.Alignment = fyne.TextAlignCenter
+
+ progressRing := canvas.NewCircle(theme.BackgroundColor())
+ progressRing.StrokeWidth = 5
+ progressRing.StrokeColor = theme.PrimaryColor()
+
+ // Vereinfachte Steuerung
+ setBtn := widget.NewButtonWithIcon("Satz", theme.ConfirmIcon(), setAction)
+ finishBtn := widget.NewButtonWithIcon("Beenden", theme.CancelIcon(), finishAction)
+ actionBar := container.NewGridWithColumns(2, setBtn, finishBtn)
+
+ // Tastatur-Shortcut
+ canvasObj := container.NewVBox(
+ container.NewCenter(timerDisplay),
+ container.NewCenter(exerciseInfo),
+ container.NewCenter(progressRing),
+ actionBar,
+ )
+
+ return container.NewPadded(canvasObj)
+}
diff --git a/renovate.json b/renovate.json
deleted file mode 100644
index 7190a60..0000000
--- a/renovate.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json"
-}
diff --git a/settings.gradle.kts b/settings.gradle.kts
deleted file mode 100644
index 9142ee6..0000000
--- a/settings.gradle.kts
+++ /dev/null
@@ -1,24 +0,0 @@
-pluginManagement {
- repositories {
- google {
- content {
- includeGroupByRegex("com\\.android.*")
- includeGroupByRegex("com\\.google.*")
- includeGroupByRegex("androidx.*")
- }
- }
- mavenCentral()
- gradlePluginPortal()
- }
-}
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.name = "kettlebelltracker"
-include(":app")
-
\ No newline at end of file