diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.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
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..ac4f03b
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,205 @@
+//plugins {
+// id("com.android.application")
+// id("org.jetbrains.kotlin.android")
+// id("org.jetbrains.kotlin.plugin.compose")
+// id("kotlin-kapt")
+//}
+//
+//android {
+// namespace = "de.patani.kettlebelltracker"
+// compileSdk = 34
+//
+// defaultConfig {
+// applicationId = "de.patani.kettlebelltracker"
+// minSdk = 26
+// targetSdk = 34
+// versionCode = 1
+// versionName = "1.0"
+//
+// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+// vectorDrawables {
+// useSupportLibrary = true
+// }
+// }
+//
+// buildTypes {
+// release {
+// isMinifyEnabled = false
+// proguardFiles(
+// getDefaultProguardFile("proguard-android-optimize.txt"),
+// "proguard-rules.pro"
+// )
+// }
+// }
+// compileOptions {
+// sourceCompatibility = JavaVersion.VERSION_1_8
+// targetCompatibility = JavaVersion.VERSION_1_8
+// }
+// kotlinOptions {
+// jvmTarget = "1.8"
+// }
+// buildFeatures {
+// compose = true
+// }
+// composeOptions {
+// kotlinCompilerExtensionVersion = "1.5.1"
+// }
+// packaging {
+// resources {
+// excludes += "/META-INF/{AL2.0,LGPL2.1}"
+// excludes += "org/intellij/lang/annotations/**"
+// }
+// }
+//}
+//
+//configurations {
+// all {
+// exclude(group = "com.intellij", module = "annotations")
+// }
+//}
+//
+//dependencies {
+// // Core
+// implementation("androidx.core:core-ktx:1.12.0")
+// implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+// implementation("androidx.activity:activity-compose:1.8.2")
+//
+// // Compose
+// implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+// implementation("androidx.compose.ui:ui")
+// implementation("androidx.compose.ui:ui-graphics")
+// implementation("androidx.compose.ui:ui-tooling-preview")
+// implementation("androidx.compose.material3:material3")
+// implementation("androidx.compose.material:material-icons-extended")
+//
+//
+// // Navigation
+// implementation("androidx.navigation:navigation-compose:2.7.6")
+//
+// // ViewModel
+// implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+//
+// // Room (Database)
+// implementation("androidx.room:room-runtime:2.6.1")
+// implementation("androidx.room:room-ktx:2.6.1")
+// implementation(libs.androidx.room.common.jvm)
+// implementation(libs.androidx.room.compiler)
+// kapt("androidx.room:room-compiler:2.6.1")
+//
+// // DataStore (Settings)
+// implementation("androidx.datastore:datastore-preferences:1.0.0")
+//
+// // Retrofit (API)
+// implementation("com.squareup.retrofit2:retrofit:2.9.0")
+// implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+//
+// // Hilt (Dependency Injection - Optional, but recommended)
+// // implementation("com.google.dagger:hilt-android:2.48")
+// // kapt("com.google.dagger:hilt-compiler:2.48")
+// // implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
+//}
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.plugin.compose")
+ id("org.jetbrains.kotlin.kapt") // Wichtig: Hier auf 'org.jetbrains.kotlin.kapt' geändert
+}
+
+android {
+ namespace = "de.patani.kettlebelltracker"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "de.patani.kettlebelltracker"
+ minSdk = 26
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ excludes += "org/intellij/lang/annotations/**"
+ }
+ }
+}
+
+// Den 'configurations' Block entfernen, es sei denn, du benötigst ihn explizit für einen spezifischen Ausschluss.
+// Wenn du ihn absichtlich hinzugefügt hast, um ein bekanntes Problem zu lösen, kannst du ihn behalten.
+// Ansonsten kommentiere ihn aus oder entferne ihn:
+/*
+configurations {
+ all {
+ exclude(group = "com.intellij", module = "annotations")
+ }
+}
+*/
+
+dependencies {
+ // Core
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ implementation("androidx.activity:activity-compose:1.8.2")
+
+ // Compose
+ implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+
+ // Navigation
+ implementation("androidx.navigation:navigation-compose:2.7.6")
+
+ // ViewModel
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+
+ // Room (Database)
+ implementation("androidx.room:room-runtime:2.6.1")
+ implementation("androidx.room:room-ktx:2.6.1")
+ // Wichtig: Die folgenden Zeilen für den Room Compiler wurden entfernt/korrigiert!
+ // KEIN implementation("libs.androidx.room.common.jvm")
+ // KEIN implementation("libs.androidx.room.compiler")
+ kapt("androidx.room:room-compiler:2.6.1") // NUR diese Zeile für den Compiler!
+
+ // DataStore (Settings)
+ implementation("androidx.datastore:datastore-preferences:1.0.0")
+
+ // Retrofit (API)
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+
+ // Hilt (Dependency Injection - Optional, but recommended)
+ // implementation("com.google.dagger:hilt-android:2.48")
+ // kapt("com.google.dagger:hilt-compiler:2.48")
+ // implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ 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
new file mode 100644
index 0000000..4f6a8dd
--- /dev/null
+++ b/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package de.patani.kettlebelltracker
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("de.patani.kettlebelltracker", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..88e6e44
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt b/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt
new file mode 100644
index 0000000..f25377e
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt
@@ -0,0 +1,159 @@
+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
+
+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("http://192.168.178.43:8080/") // WICHTIG: Deine lokale IP
+ .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).apply()
+ }
+ 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) ->
+ appUUID?.let {
+ TrainingViewModel(db.trainingSessionDao(), settingsDataStore, apiRepository,
+ it
+ )
+ } 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
new file mode 100644
index 0000000..b6222ed
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt
@@ -0,0 +1,54 @@
+package de.patani.kettlebelltracker.data.datastore
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.doublePreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+val Context.dataStore: DataStore 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
new file mode 100644
index 0000000..087f901
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt
@@ -0,0 +1,11 @@
+package de.patani.kettlebelltracker.data.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+
+@Database(entities = [TrainingSession::class], version = 1, exportSchema = false)
+@TypeConverters(Converters::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun trainingSessionDao(): TrainingSessionDao
+}
\ 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
new file mode 100644
index 0000000..213a881
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt
@@ -0,0 +1,16 @@
+package de.patani.kettlebelltracker.data.local
+
+import androidx.room.TypeConverter
+import java.util.Date
+
+class Converters {
+ @TypeConverter
+ fun fromTimestamp(value: Long?): Date? {
+ return value?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun dateToTimestamp(date: Date?): Long? {
+ return date?.time
+ }
+}
\ 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
new file mode 100644
index 0000000..ec91096
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt
@@ -0,0 +1,19 @@
+package de.patani.kettlebelltracker.data.local
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.util.Date
+
+@Entity(tableName = "training_session")
+data class TrainingSession(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+ val date: Date,
+ val sets: Int,
+ val weightLeft: Double,
+ val weightRight: Double,
+ val repsPerSet: Int,
+ val duration: Long, // in seconds
+ val program: String,
+ val blockDay: Int
+)
\ 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
new file mode 100644
index 0000000..1772939
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt
@@ -0,0 +1,36 @@
+package de.patani.kettlebelltracker.data.local
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface TrainingSessionDao {
+ @Query("SELECT * FROM training_session ORDER BY date DESC")
+ fun getAllSessions(): Flow>
+
+ @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
new file mode 100644
index 0000000..482dd4a
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt
@@ -0,0 +1,10 @@
+package de.patani.kettlebelltracker.data.remote
+
+import retrofit2.http.Body
+import retrofit2.http.POST
+import retrofit2.Response
+
+interface ApiService {
+ @POST("trainings/")
+ suspend fun sendTrainingData(@Body payload: TrainingPayload): Response
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..01ad4a1
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt
@@ -0,0 +1,8 @@
+package de.patani.kettlebelltracker.data.remote
+
+data class TrainingPayload(
+ val reps: Int,
+ val rest: Double,
+ val sets: Int,
+ val uuid: String
+)
\ 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
new file mode 100644
index 0000000..53f3020
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt
@@ -0,0 +1,32 @@
+package de.patani.kettlebelltracker.repositories
+
+import android.util.Log
+import de.patani.kettlebelltracker.data.remote.TrainingPayload
+import de.patani.kettlebelltracker.data.remote.ApiService
+
+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.d("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)
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..b79bd02
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt
@@ -0,0 +1,15 @@
+package de.patani.kettlebelltracker.ui.navigation
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.ui.graphics.vector.ImageVector
+
+sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
+ object Home : Screen("home", "Home", Icons.Default.Home)
+ object Training : Screen("training", "Training", Icons.Default.PlayArrow)
+ object History : Screen("history", "Historie", Icons.Default.DateRange)
+ object Settings : Screen("settings", "Einstellungen", Icons.Default.Settings)
+}
\ 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
new file mode 100644
index 0000000..106b281
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt
@@ -0,0 +1,322 @@
+//
+//import androidx.compose.foundation.layout.*
+//import androidx.compose.foundation.lazy.LazyColumn
+//import androidx.compose.foundation.lazy.items
+//import androidx.compose.material3.*
+//import androidx.compose.runtime.*
+//import androidx.compose.ui.Modifier
+//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
+//
+//@Composable
+//fun HistoryScreen(viewModel: HistoryViewModel) {
+// val history by viewModel.history.collectAsState()
+//
+// if (history.isEmpty()) {
+// Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.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) {
+// // Implement a detailed dialog for editing if needed, similar to the Go app's dialog.
+// // For brevity, this example uses a simpler confirmation.
+// AlertDialog(
+// onDismissRequest = onDismiss,
+// title = { Text("Eintrag verwalten") },
+// text = { Text("Möchtest du diesen Eintrag löschen? Das Bearbeiten kann in einer zukünftigen Version implementiert werden.") },
+// confirmButton = {
+// Button(onClick = onDelete) {
+// Text("Löschen")
+// }
+// },
+// dismissButton = {
+// Button(onClick = onDismiss) {
+// Text("Abbrechen")
+// }
+// }
+// )
+//}
+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
new file mode 100644
index 0000000..56f6c3a
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt
@@ -0,0 +1,85 @@
+package de.patani.kettlebelltracker.ui.screens
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import de.patani.kettlebelltracker.viewmodels.HomeViewModel
+import de.patani.kettlebelltracker.util.formatDate
+import de.patani.kettlebelltracker.util.formatDuration
+import java.util.Calendar
+
+@Composable
+fun HomeScreen(
+ viewModel: HomeViewModel,
+ onStartTrainingClicked: () -> Unit
+) {
+ val state by viewModel.homeScreenState.collectAsState()
+
+ val isTrainedToday = remember(state.lastTrainingSession) {
+ val lastDate = state.lastTrainingSession?.date
+ if (lastDate == null) false
+ else {
+ val cal1 = Calendar.getInstance().apply { time = lastDate }
+ val cal2 = Calendar.getInstance()
+ cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+ cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceAround
+ ) {
+ Text(
+ text = "Kettlebell Workout Tracker",
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center
+ )
+
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Nächstes Training", style = MaterialTheme.typography.titleLarge)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text("${state.nextTrainingProgram} - Tag ${state.nextTrainingBlockDay}")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text("Ziel: ${state.nextTrainingReps} Wiederholungen pro Satz")
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = onStartTrainingClicked, enabled = !isTrainedToday) {
+ Text(if (isTrainedToday) "Heute bereits trainiert" else "Training starten")
+ }
+ }
+ }
+
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
+ Text("Letzte Leistung", style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally))
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround
+ ) {
+ StatItem("Datum", state.lastTrainingSession?.date?.formatDate() ?: "–")
+ StatItem("Sätze", state.lastTrainingSession?.sets?.toString() ?: "–")
+ StatItem("Dauer", formatDuration(state.lastTrainingSession?.duration ?: 0))
+ StatItem("Gewicht", "${state.lastTrainingSession?.weightLeft ?: "–"}kg")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun StatItem(label: String, value: String) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = label, style = MaterialTheme.typography.labelMedium)
+ Text(text = value, style = MaterialTheme.typography.bodyLarge, fontSize = 18.sp)
+ }
+}
\ 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
new file mode 100644
index 0000000..d85f717
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt
@@ -0,0 +1,84 @@
+package de.patani.kettlebelltracker.ui.screens
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import de.patani.kettlebelltracker.viewmodels.SettingsViewModel
+import kotlinx.coroutines.launch
+
+@Composable
+fun SettingsScreen(viewModel: SettingsViewModel) {
+ val settings by viewModel.settings.collectAsState()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+
+ var time by remember(settings.trainingTimeMinutes) { mutableStateOf(settings.trainingTimeMinutes.toString()) }
+ var sets by remember(settings.goalSets) { mutableStateOf(settings.goalSets.toString()) }
+ var weightLeft by remember(settings.weightLeft) { mutableStateOf(settings.weightLeft.toString()) }
+ var weightRight by remember(settings.weightRight) { mutableStateOf(settings.weightRight.toString()) }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text("Einstellungen", style = MaterialTheme.typography.headlineSmall)
+
+ OutlinedTextField(
+ value = time,
+ onValueChange = { time = it },
+ label = { Text("Trainingszeit (Minuten)") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.fillMaxWidth()
+ )
+ OutlinedTextField(
+ value = sets,
+ onValueChange = { sets = it },
+ label = { Text("Ziel-Sätze") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.fillMaxWidth()
+ )
+ OutlinedTextField(
+ value = weightLeft,
+ onValueChange = { weightLeft = it },
+ label = { Text("Gewicht Links (kg)") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
+ modifier = Modifier.fillMaxWidth()
+ )
+ OutlinedTextField(
+ value = weightRight,
+ onValueChange = { weightRight = it },
+ label = { Text("Gewicht Rechts (kg)") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Button(
+ onClick = {
+ val timeInt = time.toIntOrNull() ?: settings.trainingTimeMinutes
+ val setsInt = sets.toIntOrNull() ?: settings.goalSets
+ val weightL = weightLeft.toDoubleOrNull() ?: settings.weightLeft
+ val weightR = weightRight.toDoubleOrNull() ?: settings.weightRight
+
+ viewModel.saveSettings(timeInt, setsInt, weightL, weightR)
+
+ scope.launch {
+ snackbarHostState.showSnackbar("Einstellungen gespeichert!")
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Speichern")
+ }
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..518c6bb
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt
@@ -0,0 +1,74 @@
+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.text.style.TextAlign
+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 state by viewModel.trainingState.collectAsState()
+ val showFinishButton = state.remainingSeconds <= 0 && state.isTrainingRunning
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Verbleibende Zeit", style = MaterialTheme.typography.titleLarge)
+ Text(
+ text = formatDuration(state.remainingSeconds.toLong()),
+ fontSize = 72.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Sätze", style = MaterialTheme.typography.titleLarge)
+ Text(
+ text = "${state.setsDone} / ${state.goalSets}",
+ fontSize = 60.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.tertiary
+ )
+ Text(
+ text = "${state.repsPerSet} Wiederholungen",
+ fontSize = 24.sp,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Button(
+ onClick = viewModel::completeSet,
+ enabled = state.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)
+ }
+ }
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..dba3d63
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package de.patani.kettlebelltracker.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ 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
new file mode 100644
index 0000000..f105e9d
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt
@@ -0,0 +1,38 @@
+package de.patani.kettlebelltracker.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+import androidx.compose.material3.darkColorScheme
+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
new file mode 100644
index 0000000..06a5fb5
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+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
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.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
new file mode 100644
index 0000000..77c4782
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt
@@ -0,0 +1,17 @@
+package de.patani.kettlebelltracker.util
+
+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)
+}
+
+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
new file mode 100644
index 0000000..1827055
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt
@@ -0,0 +1,24 @@
+package de.patani.kettlebelltracker.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
+import de.patani.kettlebelltracker.data.local.TrainingSessionDao
+import de.patani.kettlebelltracker.data.local.TrainingSession
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.util.Date
+
+class HistoryViewModel(private val dao: TrainingSessionDao) : ViewModel() {
+ val history = dao.getHistory()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ fun updateSession(session: TrainingSession) = viewModelScope.launch {
+ dao.update(session)
+ }
+
+ fun deleteSession(session: TrainingSession) = viewModelScope.launch {
+ dao.delete(session)
+ }
+}
\ 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
new file mode 100644
index 0000000..c662d01
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt
@@ -0,0 +1,33 @@
+package de.patani.kettlebelltracker.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import de.patani.kettlebelltracker.data.local.TrainingSessionDao
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+class HomeViewModel(
+ dao: TrainingSessionDao,
+ trainingViewModel: TrainingViewModel
+) : ViewModel() {
+
+ val lastSession = dao.getLastSession()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+
+ val homeScreenState = combine(lastSession, trainingViewModel.trainingState) { last, trainingState ->
+ HomeScreenState(
+ lastTrainingSession = last,
+ nextTrainingProgram = trainingState.currentProgram,
+ nextTrainingBlockDay = trainingState.currentBlockDay,
+ nextTrainingReps = trainingState.currentReps
+ )
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomeScreenState())
+}
+
+data class HomeScreenState(
+ val lastTrainingSession: de.patani.kettlebelltracker.data.local.TrainingSession? = null,
+ val nextTrainingProgram: String = "",
+ val nextTrainingBlockDay: Int = 0,
+ val nextTrainingReps: Int = 0
+)
\ 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
new file mode 100644
index 0000000..0c61f5c
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt
@@ -0,0 +1,31 @@
+package de.patani.kettlebelltracker.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import de.patani.kettlebelltracker.data.datastore.Settings
+import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class SettingsViewModel(private val settingsDataStore: SettingsDataStore) : ViewModel() {
+ val settings = settingsDataStore.settingsFlow
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Settings(0,0.0,0.0,0,""))
+
+ fun saveSettings(
+ time: Int,
+ sets: Int,
+ weightLeft: Double,
+ weightRight: Double
+ ) = viewModelScope.launch {
+ val currentSettings = settings.value
+ settingsDataStore.saveSettings(
+ currentSettings.copy(
+ trainingTimeMinutes = time,
+ goalSets = sets,
+ weightLeft = weightLeft,
+ weightRight = weightRight
+ )
+ )
+ }
+}
\ 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
new file mode 100644
index 0000000..83c8f1f
--- /dev/null
+++ b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt
@@ -0,0 +1,169 @@
+package de.patani.kettlebelltracker.viewmodels
+
+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
+)
+
+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
+
+ init {
+ // Load initial state based on past trainings
+ 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
+ )
+ }
+ }
+ }
+
+ 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
+ )
+ }
+ startTimer()
+ }
+ }
+
+ 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()
+ }
+ }
+ }
+
+ fun completeSet() {
+ if (!_trainingState.value.isTrainingRunning) return
+
+ _trainingState.update {
+ val newSetsDone = it.setsDone + 1
+ val newProgress = if (it.goalSets > 0) {
+ min(newSetsDone.toFloat() / it.goalSets.toFloat(), 1.0f)
+ } else 0.0f
+ it.copy(setsDone = newSetsDone, progress = newProgress)
+ }
+ }
+
+ fun finishTraining() {
+ timerJob?.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)
+ 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)
+ }
+
+ 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
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp 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
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp 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
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp 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
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp 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
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp 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
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #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
new file mode 100644
index 0000000..d1aa070
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ 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
new file mode 100644
index 0000000..9a13f97
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ 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
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..1a8aa6a
--- /dev/null
+++ b/app/src/test/java/de/patani/kettlebelltracker/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package de.patani.kettlebelltracker
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..952b930
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
\ No newline at end of file
diff --git a/cmd/FyneApp.toml b/cmd/FyneApp.toml
deleted file mode 100644
index 160f95e..0000000
--- a/cmd/FyneApp.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-Website = "https://patanix.de"
-
-[Details]
- Icon = "Icon.png"
- Name = "kettlebell_tracker"
- ID = "de.patanix.kettlebell_tracker"
- Version = "1.0.0"
- Build = 9
diff --git a/cmd/Icon.png b/cmd/Icon.png
deleted file mode 100644
index 9de0222..0000000
Binary files a/cmd/Icon.png and /dev/null differ
diff --git a/cmd/build.sh b/cmd/build.sh
deleted file mode 100755
index a9b4b72..0000000
--- a/cmd/build.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-# fyne package -os android -release --tags -ldflags="-s -w"
-fyne package -os android/arm64 -release --tags -ldflags="-s -w" -certificate my-release-key.keystore
diff --git a/cmd/main.go b/cmd/main.go
deleted file mode 100644
index 2fffb8f..0000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package main
-
-import (
- "log"
- "path/filepath"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/app"
- "fyne.io/fyne/v2/container"
- "git.patanix.de/git/kettlebell-app/internal/data"
- "git.patanix.de/git/kettlebell-app/internal/services"
- "git.patanix.de/git/kettlebell-app/internal/ui"
- "git.patanix.de/git/kettlebell-app/internal/ui/theme"
-)
-
-func main() {
- myApp := app.NewWithID("com.patani.kettlebell-tracker")
- myApp.Settings().SetTheme(&theme.KettlebellThemeOneDark{})
-
- mainWindow := myApp.NewWindow("Kettlebell Tracker")
-
- dbDir := myApp.Storage().RootURI().Path()
- dbPath := filepath.Join(dbDir, "giant_training.db")
- 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)
-
- contentContainer := container.NewStack()
- var navigateTo func(string)
-
- trainingScreen, startTrainingAction := ui.MakeTrainingScreen(trainingService, settingsService, mainWindow)
-
- homeScreen := ui.MakeHomeScreen(trainingService, dbService, func() {
- startTrainingAction()
- navigateTo("training")
- })
-
- historyScreen := ui.MakeHistoryScreen(dbService, mainWindow)
- settingsScreen := ui.MakeSettingsScreen(settingsService, mainWindow)
-
- screens := map[string]fyne.CanvasObject{
- "home": homeScreen,
- "training": trainingScreen,
- "history": historyScreen,
- "settings": settingsScreen,
- }
-
- for _, s := range screens {
- contentContainer.Add(s)
- }
-
- navBar, navigateFunc := ui.MakeNavBar(screens, contentContainer)
- navigateTo = navigateFunc
-
- navigateTo("home")
-
- mainLayout := container.NewBorder(nil, navBar, nil, nil, contentContainer)
-
- mainWindow.SetContent(mainLayout)
- mainWindow.Resize(fyne.NewSize(360, 740))
- mainWindow.SetMaster()
- mainWindow.ShowAndRun()
-}
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 3975480..0000000
--- a/go.mod
+++ /dev/null
@@ -1,52 +0,0 @@
-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
deleted file mode 100644
index f2fb41a..0000000
--- a/go.sum
+++ /dev/null
@@ -1,123 +0,0 @@
-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
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..27a3ee0
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,36 @@
+[versions]
+agp = "8.11.1"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.0"
+composeBom = "2024.09.00"
+roomCommonJvm = "2.7.2"
+roomCompiler = "2.7.2"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "roomCommonJvm" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomCompiler" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/internal/data/database.go b/internal/data/database.go
deleted file mode 100644
index d4ad757..0000000
--- a/internal/data/database.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package data
-
-import (
- "database/sql"
- "fmt"
- "log"
- "time"
-
- _ "modernc.org/sqlite"
-)
-
-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
- }
- 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) 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 sess TrainingSession
- var dateStr string
- err := rows.Scan(&sess.ID, &dateStr, &sess.Sets, &sess.WeightLeft, &sess.WeightRight, &sess.RepsPerSet, &sess.Duration, &sess.Program, &sess.BlockDay)
- if err != nil {
- return nil, err
- }
- sess.Date, err = time.Parse(time.RFC3339, dateStr)
- if err != nil {
- return nil, err
- }
- sessions = append(sessions, sess)
- }
- return sessions, nil
-}
-
-func (s *DatabaseService) GetLastTraining() (*TrainingSession, error) {
- query := `SELECT id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay FROM training ORDER BY date DESC LIMIT 1;`
- row := s.DB.QueryRow(query)
-
- var session TrainingSession
- var dateStr string
- err := row.Scan(&session.ID, &dateStr, &session.Sets, &session.WeightLeft, &session.WeightRight, &session.RepsPerSet, &session.Duration, &session.Program, &session.BlockDay)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil
- }
- return nil, err
- }
-
- session.Date, err = time.Parse(time.RFC3339, dateStr)
- if err != nil {
- return nil, err
- }
-
- return &session, nil
-}
-
-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) DeleteTraining(id int64) error {
- query := "DELETE FROM training WHERE id = ?;"
- _, err := s.DB.Exec(query, id)
- return err
-}
-
-func (s *DatabaseService) UpdateTraining(session *TrainingSession) error {
- dateStr := session.Date.Format(time.RFC3339)
- query := `
- UPDATE training
- SET date = ?, sets = ?, weightLeft = ?, weightRight = ?, repsPerSet = ?, duration = ?, program = ?, blockDay = ?
- WHERE id = ?;
- `
- res, err := s.DB.Exec(query, dateStr, session.Sets, session.WeightLeft, session.WeightRight, session.RepsPerSet, session.Duration, session.Program, session.BlockDay, session.ID)
- if err != nil {
- log.Printf("UpdateTraining Fehler: %v", err)
- return err
- }
- rowsAffected, err := res.RowsAffected()
- if err != nil {
- log.Printf("UpdateTraining RowsAffected Fehler: %v", err)
- return err
- }
- if rowsAffected == 0 {
- log.Printf("UpdateTraining: Kein Datensatz mit ID %d gefunden", session.ID)
- return fmt.Errorf("kein Datensatz mit ID %d gefunden", session.ID)
- }
- log.Printf("UpdateTraining erfolgreich für ID %d", session.ID)
- return nil
-}
diff --git a/internal/data/models.go b/internal/data/models.go
deleted file mode 100644
index 17baa75..0000000
--- a/internal/data/models.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package data
-
-import "time"
-
-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"`
- Program string `db:"program"`
- BlockDay int64 `db:"blockDay"`
-}
diff --git a/internal/services/api.go b/internal/services/api.go
deleted file mode 100644
index 9c7afad..0000000
--- a/internal/services/api.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package services
-
-import (
- "bytes"
- "encoding/json"
- "log"
- "net/http"
- "time"
-
- "git.patanix.de/git/kettlebell-app/internal/data"
-)
-
-type TrainingPayload struct {
- Reps int `json:"reps"`
- Rest float64 `json:"rest"`
- Sets int `json:"sets"`
- UUID string `json:"uuid"`
-}
-
-type ApiService struct {
- client *http.Client
- endpoint string
- uuid string
-}
-
-func NewApiService(appUUID string) *ApiService {
- return &ApiService{
- client: &http.Client{
- Timeout: 5 * time.Second,
- },
- endpoint: "http://192.168.178.43:8080/trainings/",
- uuid: appUUID,
- }
-}
-
-func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
- var rest float64
- if session.Sets > 0 {
- rest = float64(session.Duration) / float64(session.Sets)
- }
-
- payload := TrainingPayload{
- Reps: int(session.RepsPerSet),
- Rest: rest,
- Sets: int(session.Sets),
- UUID: s.uuid,
- }
-
- jsonData, err := json.Marshal(payload)
- if err != nil {
- log.Printf("API Fehler: Konnte Payload nicht in JSON umwandeln: %v", err)
- return
- }
-
- 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")
-
- 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()
-
- 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
deleted file mode 100644
index a00e55b..0000000
--- a/internal/services/settings.go
+++ /dev/null
@@ -1,41 +0,0 @@
-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
deleted file mode 100644
index db5ad67..0000000
--- a/internal/services/training.go
+++ /dev/null
@@ -1,355 +0,0 @@
-// 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 = "clean_1.0"
- blockDay = 1
- reps = 5
-
- if totalDays > 0 {
- cycleIndex := (totalDays / 12) % 6
-
- programs := []string{"clean_1.0", "snatch_1.0", "clean_1.1", "snatch_1.1", "clean_1.2", "snatch_1.2"}
- program = programs[cycleIndex]
-
- blockDay = (totalDays % 3) + 1
- }
-
- repsMap := map[string][]int{
- "clean_1.0": {5, 6, 4},
- "clean_1.1": {6, 8, 7},
- "clean_1.2": {7, 9, 8},
- "snatch_1.0": {5, 6, 4},
- "snatch_1.1": {6, 8, 7},
- "snatch_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
- }
-
- 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/components/navbutton.go b/internal/ui/components/navbutton.go
deleted file mode 100644
index 7e03c81..0000000
--- a/internal/ui/components/navbutton.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package components
-
-import (
- "image/color"
-
- "git.patanix.de/git/kettlebell-app/internal/ui/theme"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/widget"
-)
-
-type NavButton struct {
- widget.BaseWidget
- icon *canvas.Image
- label *canvas.Text
- onTapped func()
- isActive bool
- container *fyne.Container
-}
-
-func NewNavButton(label string, iconRes fyne.Resource, active bool, tapped func()) *NavButton {
- icon := canvas.NewImageFromResource(iconRes)
- icon.FillMode = canvas.ImageFillContain
- icon.SetMinSize(fyne.NewSize(28, 28))
-
- text := canvas.NewText(label, color.White)
- text.TextSize = 12
- text.Alignment = fyne.TextAlignCenter
-
- button := &NavButton{
- icon: icon,
- label: text,
- onTapped: tapped,
- }
- button.ExtendBaseWidget(button)
- button.container = container.NewVBox(icon, text)
- button.SetActive(active)
- return button
-}
-
-func (b *NavButton) CreateRenderer() fyne.WidgetRenderer {
- return widget.NewSimpleRenderer(b.container)
-}
-
-func (b *NavButton) Tapped(*fyne.PointEvent) {
- b.onTapped()
-}
-
-func (b *NavButton) SetActive(active bool) {
- b.isActive = active
- if b.isActive {
- b.label.Color = theme.OneDarkGreen
- } else {
- b.label.Color = theme.OneDarkText
- }
- b.Refresh()
-}
diff --git a/internal/ui/history.go b/internal/ui/history.go
deleted file mode 100644
index 3c25520..0000000
--- a/internal/ui/history.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package ui
-
-import (
- "fmt"
- "log"
- "strconv"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/dialog"
- "fyne.io/fyne/v2/theme"
- "fyne.io/fyne/v2/widget"
- "git.patanix.de/git/kettlebell-app/internal/data"
- "git.patanix.de/git/kettlebell-app/internal/ui/utils"
-)
-
-func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.CanvasObject {
- var history []data.TrainingSession
-
- placeholder := widget.NewLabel("Noch keine Trainingsdaten vorhanden.")
-
- var showDetailDialog func(session *data.TrainingSession, id int)
- var refreshData func()
-
- list := widget.NewList(
- func() int { return len(history) },
- func() fyne.CanvasObject {
- return container.NewGridWithColumns(4,
- widget.NewLabel(""),
- widget.NewLabel(""),
- widget.NewLabel(""),
- widget.NewLabel(""),
- )
- },
- func(i widget.ListItemID, o fyne.CanvasObject) {
- session := history[i]
- row := o.(*fyne.Container)
- row.Objects[0].(*widget.Label).SetText(session.Date.Format("02.01.2006"))
- row.Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", session.Sets))
- row.Objects[2].(*widget.Label).SetText(utils.FormatDuration(session.Duration))
- row.Objects[3].(*widget.Label).SetText(fmt.Sprintf("%d", session.RepsPerSet))
- },
- )
-
- list.OnSelected = func(id widget.ListItemID) {
- session := &history[id]
- showDetailDialog(session, id)
- list.Unselect(id)
- }
-
- showDetailDialog = func(session *data.TrainingSession, id int) {
- setsEntry := widget.NewEntry()
- setsEntry.SetText(fmt.Sprintf("%d", session.Sets))
-
- durationEntry := widget.NewEntry()
- durationEntry.SetText(fmt.Sprintf("%d", session.Duration))
-
- repsEntry := widget.NewEntry()
- repsEntry.SetText(fmt.Sprintf("%d", session.RepsPerSet))
-
- weightLeftEntry := widget.NewEntry()
- weightLeftEntry.SetText(fmt.Sprintf("%.1f", session.WeightLeft))
-
- weightRightEntry := widget.NewEntry()
- weightRightEntry.SetText(fmt.Sprintf("%.1f", session.WeightRight))
-
- info := container.NewVBox(
- widget.NewLabel(fmt.Sprintf("Datum: %s", session.Date.Format("02.01.2006 15:04"))),
- widget.NewLabel(fmt.Sprintf("Programm: %s", session.Program)),
- widget.NewLabel(fmt.Sprintf("Block-Tag: %d", session.BlockDay)),
- )
-
- form := widget.NewForm(
- &widget.FormItem{Text: "Sätze", Widget: setsEntry},
- &widget.FormItem{Text: "Dauer (Sekunden)", Widget: durationEntry},
- &widget.FormItem{Text: "Reps/Satz", Widget: repsEntry},
- &widget.FormItem{Text: "Gewicht links (kg)", Widget: weightLeftEntry},
- &widget.FormItem{Text: "Gewicht rechts (kg)", Widget: weightRightEntry},
- )
-
- // Lokale Dialog-Referenz deklarieren
- var detailDialog dialog.Dialog
-
- saveBtn := widget.NewButtonWithIcon("Speichern", theme.ConfirmIcon(), func() {
- sets, err := strconv.Atoi(setsEntry.Text)
- if err != nil {
- dialog.ShowError(fmt.Errorf("Ungültige Sätze-Zahl"), parent)
- return
- }
- duration, err := strconv.Atoi(durationEntry.Text)
- if err != nil {
- dialog.ShowError(fmt.Errorf("Ungültige Dauer"), parent)
- return
- }
- reps, err := strconv.Atoi(repsEntry.Text)
- if err != nil {
- dialog.ShowError(fmt.Errorf("Ungültige Wiederholungszahl"), parent)
- return
- }
- weightLeft, err := strconv.ParseFloat(weightLeftEntry.Text, 64)
- if err != nil {
- dialog.ShowError(fmt.Errorf("Ungültiges Gewicht links"), parent)
- return
- }
- weightRight, err := strconv.ParseFloat(weightRightEntry.Text, 64)
- if err != nil {
- dialog.ShowError(fmt.Errorf("Ungültiges Gewicht rechts"), parent)
- return
- }
-
- session.Sets = int64(sets)
- session.Duration = int64(duration)
- session.RepsPerSet = int64(reps)
- session.WeightLeft = weightLeft
- session.WeightRight = weightRight
-
- if err := db.UpdateTraining(session); err != nil {
- dialog.ShowError(err, parent)
- return
- }
- dialog.ShowInformation("Erfolg", "Trainingseintrag aktualisiert.", parent)
- refreshData()
- detailDialog.Hide()
- })
-
- deleteBtn := widget.NewButtonWithIcon("Löschen", theme.DeleteIcon(), func() {
- dialog.ShowConfirm("Eintrag löschen", "Möchtest du diesen Eintrag wirklich löschen?", func(ok bool) {
- if ok {
- err := db.DeleteTraining(session.ID)
- if err != nil {
- dialog.ShowError(err, parent)
- return
- }
- refreshData()
- detailDialog.Hide()
- }
- }, parent)
- })
-
- cancelBtn := widget.NewButton("Abbrechen", func() {
- detailDialog.Hide()
- })
-
- btns := container.NewHBox(saveBtn, deleteBtn, cancelBtn)
-
- content := container.NewVBox(
- info,
- form,
- btns,
- )
-
- detailDialog = dialog.NewCustom("Training bearbeiten/löschen", "Schließen", content, parent)
- detailDialog.Show()
- }
-
- 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)
- return
- }
- if len(history) == 0 {
- placeholder.Show()
- list.Hide()
- } else {
- placeholder.Hide()
- list.Show()
- }
- list.Refresh()
- }
-
- header := container.NewGridWithColumns(4,
- widget.NewLabelWithStyle("Datum", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
- widget.NewLabelWithStyle("Sätze", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
- widget.NewLabelWithStyle("Dauer", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
- widget.NewLabelWithStyle("Reps/Satz", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
- )
-
- toolbar := widget.NewToolbar(widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData))
- // content := container.NewVBox(header, container.NewStack(list, container.NewCenter(placeholder)))
- // layout := container.NewBorder(toolbar, nil, nil, nil, content)
-
- content := container.NewBorder(
- header,
- nil,
- nil,
- nil,
- container.NewVScroll(list),
- )
-
- layout := container.NewBorder(
- toolbar,
- nil,
- nil,
- nil,
- content,
- )
- if layout.Visible() {
- refreshData()
- }
- return layout
-}
diff --git a/internal/ui/home.go b/internal/ui/home.go
deleted file mode 100644
index 19dedf8..0000000
--- a/internal/ui/home.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package ui
-
-import (
- "fmt"
- "log"
- "time"
-
- "git.patanix.de/git/kettlebell-app/internal/data"
- "git.patanix.de/git/kettlebell-app/internal/services"
-
- "git.patanix.de/git/kettlebell-app/internal/ui/theme"
- "git.patanix.de/git/kettlebell-app/internal/ui/utils"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/widget"
-)
-
-func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onStart func()) fyne.CanvasObject {
- headerTitle := canvas.NewText("Kettlebell Workout Tracker", theme.OneDarkText)
- headerTitle.TextSize = 28
- headerTitle.TextStyle.Bold = true
-
- header := container.NewCenter(
- widget.NewSeparator(),
- headerTitle,
- )
-
- state := ts.State
- startButton := widget.NewButton("Training starten", onStart)
- startButton.Importance = widget.HighImportance
-
- nextTrainingCard := widget.NewCard(
- "Nächstes Training",
- fmt.Sprintf("%s - Tag %d", state.CurrentProgram, state.CurrentBlockDay),
- container.NewVBox(
- widget.NewLabel(fmt.Sprintf("Ziel: %d Wiederholungen pro Satz", state.CurrentReps)),
- startButton,
- ),
- )
- centerContent := container.NewCenter(nextTrainingCard)
-
- setsValue := widget.NewLabelWithStyle("–", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
- durationValue := widget.NewLabelWithStyle("–", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
- weightValue := widget.NewLabelWithStyle("–", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
-
- statsCard := widget.NewCard("Letzte Leistung", "", container.NewGridWithColumns(3,
- container.NewVBox(widget.NewLabel("Sätze"), setsValue),
- container.NewVBox(widget.NewLabel("Dauer"), durationValue),
- container.NewVBox(widget.NewLabel("Gewicht"), weightValue),
- ))
-
- loadLastPerformance := func() {
- lastSession, err := db.GetLastTraining()
- if err != nil {
- log.Printf("Fehler beim Laden der letzten Session: %v", err)
- return
- }
- if lastSession != nil {
- setsValue.SetText(fmt.Sprintf("%d", lastSession.Sets))
- durationValue.SetText(utils.FormatDuration(lastSession.Duration))
- weightValue.SetText(fmt.Sprintf("%.1fkg", lastSession.WeightLeft))
- trainedToday := false
- trainedToday = EqualDate(lastSession.Date, time.Now())
- if trainedToday {
- startButton.Disabled()
- }
- }
- }
-
- borderLayout := container.NewBorder(
- header,
- statsCard,
- nil,
- nil,
- centerContent,
- )
-
- paddedLayout := container.NewPadded(borderLayout)
- if paddedLayout.Visible() {
- loadLastPerformance()
- }
-
- return paddedLayout
-}
-
-func EqualDate(date1, date2 time.Time) bool {
- y1, m1, d1 := date1.Date()
- y2, m2, d2 := date2.Date()
- return y1 == y2 && m1 == m2 && d1 == d2
-}
diff --git a/internal/ui/navbar.go b/internal/ui/navbar.go
deleted file mode 100644
index c367107..0000000
--- a/internal/ui/navbar.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package ui
-
-import (
- "git.patanix.de/git/kettlebell-app/internal/ui/components"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/theme"
-)
-
-func MakeNavBar(screens map[string]fyne.CanvasObject, content *fyne.Container) (fyne.CanvasObject, func(string)) {
- buttons := make(map[string]*components.NavButton)
-
- navigateTo := func(name string) {
- for key, screen := range screens {
- screen.Hide()
- if key == name {
- screen.Show()
- }
- }
- for key, button := range buttons {
- button.SetActive(false)
- if key == name {
- button.SetActive(true)
- }
- }
- content.Refresh()
- }
-
- buttons["home"] = components.NewNavButton("Home", theme.HomeIcon(), false, func() { navigateTo("home") })
- buttons["training"] = components.NewNavButton("Training", theme.MediaPlayIcon(), false, func() { navigateTo("training") })
- buttons["history"] = components.NewNavButton("Historie", theme.ListIcon(), false, func() { navigateTo("history") })
- buttons["settings"] = components.NewNavButton("Einstellungen", theme.SettingsIcon(), false, func() { navigateTo("settings") })
-
- navContainer := container.NewGridWithColumns(4,
- buttons["home"],
- buttons["training"],
- buttons["history"],
- buttons["settings"],
- )
-
- return navContainer, navigateTo
-}
diff --git a/internal/ui/settings.go b/internal/ui/settings.go
deleted file mode 100644
index 9f240d8..0000000
--- a/internal/ui/settings.go
+++ /dev/null
@@ -1,82 +0,0 @@
-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/widget"
-)
-
-func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
- var timeEntry, setsEntry, weightLeftEntry, weightRightEntry *widget.Entry
-
- loadData := func() {
- currentSettings := settingsService.LoadSettings()
- timeEntry.SetText(fmt.Sprintf("%d", currentSettings.TrainingTimeMinutes))
- setsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets))
- weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft))
- weightRightEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightRight))
- }
-
- timeEntry = widget.NewEntry()
- timeEntry.Validator = func(s string) error {
- if _, err := strconv.Atoi(s); err != nil {
- return fmt.Errorf("muss eine Zahl sein")
- }
- return nil
- }
-
- setsEntry = widget.NewEntry()
- setsEntry.Validator = timeEntry.Validator
-
- weightLeftEntry = widget.NewEntry()
- weightLeftEntry.Validator = func(s string) error {
- if _, err := strconv.ParseFloat(s, 64); err != nil {
- return fmt.Errorf("muss eine Zahl sein")
- }
- return nil
- }
- weightRightEntry = widget.NewEntry()
- weightRightEntry.Validator = weightLeftEntry.Validator
-
- form := &widget.Form{
- Items: []*widget.FormItem{
- {Text: "Trainingszeit (Minuten)", Widget: timeEntry},
- {Text: "Ziel-Sätze", Widget: setsEntry},
- {Text: "Links (kg)", Widget: weightLeftEntry},
- {Text: "Rechts (kg)", Widget: weightRightEntry},
- },
- OnSubmit: func() {
- timeMin, _ := strconv.Atoi(timeEntry.Text)
- goal, _ := strconv.Atoi(setsEntry.Text)
- weightL, _ := strconv.ParseFloat(weightLeftEntry.Text, 64)
- weightR, _ := strconv.ParseFloat(weightRightEntry.Text, 64)
-
- newSettings := &services.Settings{
- TrainingTimeMinutes: timeMin,
- GoalSets: goal,
- WeightLeft: weightL,
- WeightRight: weightR,
- InitialProgram: settingsService.LoadSettings().InitialProgram,
- }
- settingsService.SaveSettings(newSettings)
- fyne.CurrentApp().SendNotification(&fyne.Notification{
- Title: "Gespeichert",
- Content: "Die Einstellungen wurden erfolgreich aktualisiert.",
- })
- },
- }
-
- title := widget.NewLabelWithStyle("Einstellungen", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
- layout := container.NewVBox(title, widget.NewSeparator(), form)
- paddedLayout := container.NewPadded(layout)
- if paddedLayout.Visible() {
- loadData()
- }
-
- return paddedLayout
-}
diff --git a/internal/ui/theme/theme.go b/internal/ui/theme/theme.go
deleted file mode 100644
index daf6492..0000000
--- a/internal/ui/theme/theme.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package theme
-
-import (
- "image/color"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/theme"
-)
-
-var (
- OneDarkBackground = color.NRGBA{R: 40, G: 44, B: 52, A: 0xff} // #282c34 (helleres Grau)
- OneDarkCardBackground = color.NRGBA{R: 30, G: 32, B: 40, A: 0xff} // #1e2028 (dunkler für Cards)
- OneDarkText = color.NRGBA{R: 171, G: 178, B: 191, A: 0xff} // #abb2bf (Standard-Text)
- OneDarkSubtleText = color.NRGBA{R: 110, G: 115, B: 141, A: 0xff} // #6e738d (deaktiviert, Placeholder)
-
- OneDarkGreen = color.NRGBA{R: 152, G: 195, B: 121, A: 0xff} // #98c379
- OneDarkRed = color.NRGBA{R: 224, G: 108, B: 117, A: 0xff} // #e06c75
- OneDarkYellow = color.NRGBA{R: 229, G: 192, B: 123, A: 0xff} // #e5c07b
-)
-
-type KettlebellThemeOneDark struct{}
-
-func (t *KettlebellThemeOneDark) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
- switch name {
- case theme.ColorNameBackground:
- return OneDarkBackground
- case theme.ColorNameButton:
- return OneDarkGreen
- case theme.ColorNameDisabledButton:
- return OneDarkSubtleText
- case theme.ColorNamePrimary:
- return OneDarkGreen
- case theme.ColorNamePlaceHolder:
- return OneDarkSubtleText
- case theme.ColorNameHover:
- return OneDarkYellow
- case theme.ColorNameForeground:
- return OneDarkText
- case theme.ColorNameDisabled:
- return OneDarkSubtleText
- case theme.ColorNameError:
- return OneDarkRed
- case theme.ColorNameInputBackground:
- return OneDarkCardBackground
- case theme.ColorNameSeparator:
- return OneDarkSubtleText
- case theme.ColorNameSelection:
- return OneDarkYellow
- case theme.ColorNameShadow:
- return OneDarkCardBackground
- case theme.ColorNameFocus:
- return OneDarkYellow
- default:
- return theme.DefaultTheme().Color(name, variant)
- }
-}
-
-func (t *KettlebellThemeOneDark) Icon(name fyne.ThemeIconName) fyne.Resource {
- return theme.DefaultTheme().Icon(name)
-}
-
-func (t *KettlebellThemeOneDark) Font(style fyne.TextStyle) fyne.Resource {
- return theme.DefaultTheme().Font(style)
-}
-
-func (t *KettlebellThemeOneDark) Size(name fyne.ThemeSizeName) float32 {
- switch name {
- case theme.SizeNamePadding:
- return 8
- case theme.SizeNameText:
- return 16
- case theme.SizeNameHeadingText:
- return 24
- case theme.SizeNameSubHeadingText:
- return 20
- default:
- return theme.DefaultTheme().Size(name)
- }
-}
diff --git a/internal/ui/training.go b/internal/ui/training.go
deleted file mode 100644
index 49bd708..0000000
--- a/internal/ui/training.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package ui
-
-import (
- "fmt"
- "time"
-
- "git.patanix.de/git/kettlebell-app/internal/data"
- "git.patanix.de/git/kettlebell-app/internal/services"
- "git.patanix.de/git/kettlebell-app/internal/ui/theme"
- "git.patanix.de/git/kettlebell-app/internal/ui/utils"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/widget"
-)
-
-func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) (fyne.CanvasObject, func()) {
- timerLabel := canvas.NewText("00:00", theme.OneDarkText)
- timerLabel.TextSize = 60
- timerLabel.TextStyle.Bold = true
- timerLabel.Alignment = fyne.TextAlignCenter
-
- setsLabel := canvas.NewText("0 / 0", theme.OneDarkGreen)
- setsLabel.TextSize = 48
- setsLabel.TextStyle.Bold = true
- setsLabel.Alignment = fyne.TextAlignCenter
-
- repsLabel := canvas.NewText("0 Wiederholungen", theme.OneDarkSubtleText)
- repsLabel.TextSize = 20
- repsLabel.Alignment = fyne.TextAlignCenter
-
- var finishButton *widget.Button
-
- var mainTimer *time.Ticker
-
- updateUI := func() {
- state := ts.State
- timerLabel.Text = utils.FormatDuration(int64(state.RemainingSeconds))
- setsLabel.Text = fmt.Sprintf("%d / %d", state.SetsDone, state.GoalSets)
- repsLabel.Text = fmt.Sprintf("%d Wiederholungen", state.RepsPerSet)
-
- if finishButton != nil {
- if state.RemainingSeconds <= 0 && state.IsTrainingRunning {
- finishButton.Show()
- } else {
- finishButton.Hide()
- }
- }
-
- timerLabel.Refresh()
- setsLabel.Refresh()
- repsLabel.Refresh()
- }
-
- finishAction := func() {
- if ts.State.RemainingSeconds > 0 {
- return
- }
- if mainTimer != nil {
- mainTimer.Stop()
- mainTimer = nil
- }
- if !ts.State.IsTrainingRunning {
- return
- }
- session := &data.TrainingSession{
- Date: time.Now(),
- Sets: int64(ts.State.SetsDone),
- WeightLeft: ss.LoadSettings().WeightLeft,
- WeightRight: ss.LoadSettings().WeightRight,
- RepsPerSet: int64(ts.State.RepsPerSet),
- Duration: int64(ts.State.InitialDurationSeconds - ts.State.RemainingSeconds),
- }
- ts.FinishTraining(session)
- fyne.CurrentApp().SendNotification(&fyne.Notification{Title: "Training gespeichert!", Content: "Gut gemacht!"})
- updateUI()
- }
-
- startAction := func() {
- if ts.State.IsTrainingRunning {
- return
- }
- settings := ss.LoadSettings()
- ts.StartTraining(settings.TrainingTimeMinutes, settings.GoalSets)
- updateUI()
-
- mainTimer = time.NewTicker(time.Second)
- go func() {
- for mainTimer != nil {
- <-mainTimer.C
- if ts.State.RemainingSeconds <= 0 {
- finishAction()
- return
- }
- ts.Tick()
- updateUI()
- }
- }()
- }
-
- setAction := func() {
- if !ts.State.IsTrainingRunning {
- return
- }
- ts.CompleteSet()
- updateUI()
- }
-
- topPart := container.NewVBox(widget.NewLabelWithStyle("Verbleibende Zeit", fyne.TextAlignCenter, fyne.TextStyle{}), timerLabel)
- middlePart := container.NewVBox(widget.NewLabelWithStyle("Sätze", fyne.TextAlignCenter, fyne.TextStyle{}), setsLabel, repsLabel)
- finishButton = widget.NewButton("Training beenden", finishAction)
- finishButton.Hide() // oder: finishButton.Hide() // finishButton.Disable()
- setButton := widget.NewButton("Satz abschließen", setAction)
- setButton.Importance = widget.HighImportance
- setButton.Resize(fyne.NewSize(120, 60))
-
- bottomPart := container.NewVBox(
- setButton,
- finishButton,
- )
-
- layout := container.NewBorder(topPart, bottomPart, nil, nil, container.NewCenter(middlePart))
- if layout.Visible() {
- updateUI()
- }
-
- return layout, startAction
-}
diff --git a/internal/ui/utils/format.go b/internal/ui/utils/format.go
deleted file mode 100644
index 1a03138..0000000
--- a/internal/ui/utils/format.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package utils
-
-import "fmt"
-
-func FormatDuration(totalSeconds int64) string {
- if totalSeconds < 0 {
- totalSeconds = 0
- }
- mins := totalSeconds / 60
- secs := totalSeconds % 60
- return fmt.Sprintf("%02d:%02d", mins, secs)
-}
diff --git a/internal/ui/utils/utils.go b/internal/ui/utils/utils.go
deleted file mode 100644
index 5373c71..0000000
--- a/internal/ui/utils/utils.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package utils
-
-import "fmt"
-
-func formatDuration(totalSeconds int64) string {
- mins := totalSeconds / 60
- secs := totalSeconds % 60
- return fmt.Sprintf("%02d:%02d", mins, secs)
-}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..9142ee6
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+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