diff --git a/.gitignore b/.gitignore deleted file mode 100644 index aa724b7..0000000 --- a/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts deleted file mode 100644 index ac4f03b..0000000 --- a/app/build.gradle.kts +++ /dev/null @@ -1,205 +0,0 @@ -//plugins { -// id("com.android.application") -// id("org.jetbrains.kotlin.android") -// id("org.jetbrains.kotlin.plugin.compose") -// id("kotlin-kapt") -//} -// -//android { -// namespace = "de.patani.kettlebelltracker" -// compileSdk = 34 -// -// defaultConfig { -// applicationId = "de.patani.kettlebelltracker" -// minSdk = 26 -// targetSdk = 34 -// versionCode = 1 -// versionName = "1.0" -// -// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" -// vectorDrawables { -// useSupportLibrary = true -// } -// } -// -// buildTypes { -// release { -// isMinifyEnabled = false -// proguardFiles( -// getDefaultProguardFile("proguard-android-optimize.txt"), -// "proguard-rules.pro" -// ) -// } -// } -// compileOptions { -// sourceCompatibility = JavaVersion.VERSION_1_8 -// targetCompatibility = JavaVersion.VERSION_1_8 -// } -// kotlinOptions { -// jvmTarget = "1.8" -// } -// buildFeatures { -// compose = true -// } -// composeOptions { -// kotlinCompilerExtensionVersion = "1.5.1" -// } -// packaging { -// resources { -// excludes += "/META-INF/{AL2.0,LGPL2.1}" -// excludes += "org/intellij/lang/annotations/**" -// } -// } -//} -// -//configurations { -// all { -// exclude(group = "com.intellij", module = "annotations") -// } -//} -// -//dependencies { -// // Core -// implementation("androidx.core:core-ktx:1.12.0") -// implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") -// implementation("androidx.activity:activity-compose:1.8.2") -// -// // Compose -// implementation(platform("androidx.compose:compose-bom:2023.08.00")) -// implementation("androidx.compose.ui:ui") -// implementation("androidx.compose.ui:ui-graphics") -// implementation("androidx.compose.ui:ui-tooling-preview") -// implementation("androidx.compose.material3:material3") -// implementation("androidx.compose.material:material-icons-extended") -// -// -// // Navigation -// implementation("androidx.navigation:navigation-compose:2.7.6") -// -// // ViewModel -// implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") -// -// // Room (Database) -// implementation("androidx.room:room-runtime:2.6.1") -// implementation("androidx.room:room-ktx:2.6.1") -// implementation(libs.androidx.room.common.jvm) -// implementation(libs.androidx.room.compiler) -// kapt("androidx.room:room-compiler:2.6.1") -// -// // DataStore (Settings) -// implementation("androidx.datastore:datastore-preferences:1.0.0") -// -// // Retrofit (API) -// implementation("com.squareup.retrofit2:retrofit:2.9.0") -// implementation("com.squareup.retrofit2:converter-gson:2.9.0") -// -// // Hilt (Dependency Injection - Optional, but recommended) -// // implementation("com.google.dagger:hilt-android:2.48") -// // kapt("com.google.dagger:hilt-compiler:2.48") -// // implementation("androidx.hilt:hilt-navigation-compose:1.1.0") -//} -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") - id("org.jetbrains.kotlin.plugin.compose") - id("org.jetbrains.kotlin.kapt") // Wichtig: Hier auf 'org.jetbrains.kotlin.kapt' geändert -} - -android { - namespace = "de.patani.kettlebelltracker" - compileSdk = 34 - - defaultConfig { - applicationId = "de.patani.kettlebelltracker" - minSdk = 26 - targetSdk = 34 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary = true - } - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.1" - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - excludes += "org/intellij/lang/annotations/**" - } - } -} - -// Den 'configurations' Block entfernen, es sei denn, du benötigst ihn explizit für einen spezifischen Ausschluss. -// Wenn du ihn absichtlich hinzugefügt hast, um ein bekanntes Problem zu lösen, kannst du ihn behalten. -// Ansonsten kommentiere ihn aus oder entferne ihn: -/* -configurations { - all { - exclude(group = "com.intellij", module = "annotations") - } -} -*/ - -dependencies { - // Core - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - implementation("androidx.activity:activity-compose:1.8.2") - - // Compose - implementation(platform("androidx.compose:compose-bom:2023.08.00")) - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-graphics") - implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.material:material-icons-extended") - - // Navigation - implementation("androidx.navigation:navigation-compose:2.7.6") - - // ViewModel - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") - - // Room (Database) - implementation("androidx.room:room-runtime:2.6.1") - implementation("androidx.room:room-ktx:2.6.1") - // Wichtig: Die folgenden Zeilen für den Room Compiler wurden entfernt/korrigiert! - // KEIN implementation("libs.androidx.room.common.jvm") - // KEIN implementation("libs.androidx.room.compiler") - kapt("androidx.room:room-compiler:2.6.1") // NUR diese Zeile für den Compiler! - - // DataStore (Settings) - implementation("androidx.datastore:datastore-preferences:1.0.0") - - // Retrofit (API) - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - - // Hilt (Dependency Injection - Optional, but recommended) - // implementation("com.google.dagger:hilt-android:2.48") - // kapt("com.google.dagger:hilt-compiler:2.48") - // implementation("androidx.hilt:hilt-navigation-compose:1.1.0") -} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt deleted file mode 100644 index 4f6a8dd..0000000 --- a/app/src/androidTest/java/de/patani/kettlebelltracker/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package de.patani.kettlebelltracker - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("de.patani.kettlebelltracker", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index f314948..0000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt b/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt deleted file mode 100644 index a6a7080..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/MainActivity.kt +++ /dev/null @@ -1,158 +0,0 @@ -package de.patani.kettlebelltracker - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.* -import androidx.room.Room -import de.patani.kettlebelltracker.data.datastore.SettingsDataStore -import de.patani.kettlebelltracker.data.local.AppDatabase -import de.patani.kettlebelltracker.repositories.ApiRepository -import de.patani.kettlebelltracker.ui.navigation.Screen -import de.patani.kettlebelltracker.ui.screens.* -import de.patani.kettlebelltracker.ui.theme.KettlebellTrackerTheme -import de.patani.kettlebelltracker.viewmodels.* -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.UUID -import androidx.core.content.edit - -class MainActivity : ComponentActivity() { - - private val db by lazy { - Room.databaseBuilder(applicationContext, AppDatabase::class.java, "kettlebell_tracker.db").build() - } - private val settingsDataStore by lazy { SettingsDataStore(applicationContext) } - - private val apiService by lazy { - Retrofit.Builder() - .baseUrl("https://kb.patanix.de/") - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(de.patani.kettlebelltracker.data.remote.ApiService::class.java) - } - - private val apiRepository by lazy { ApiRepository(apiService) } - - private val appUUID by lazy { - val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE) - var uuid = prefs.getString("app_uuid", null) - if (uuid == null) { - uuid = UUID.randomUUID().toString() - prefs.edit { putString("app_uuid", uuid) } - } - uuid - } - - private val trainingViewModel by lazy { - ViewModelProvider(this, createViewModelFactory(TrainingViewModel::class.java)).get(TrainingViewModel::class.java) - } - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - KettlebellTrackerTheme { - App(createViewModelFactory = { modelClass -> createViewModelFactory(modelClass) }) - } - } - } - - @Suppress("UNCHECKED_CAST") - private fun createViewModelFactory(modelClass: Class): ViewModelProvider.Factory { - return object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return when { - modelClass.isAssignableFrom(TrainingViewModel::class.java) -> - TrainingViewModel(db.trainingSessionDao(), settingsDataStore, apiRepository, - appUUID - ) as T - modelClass.isAssignableFrom(HomeViewModel::class.java) -> - HomeViewModel(db.trainingSessionDao(), trainingViewModel) as T - modelClass.isAssignableFrom(HistoryViewModel::class.java) -> - HistoryViewModel(db.trainingSessionDao()) as T - modelClass.isAssignableFrom(SettingsViewModel::class.java) -> - SettingsViewModel(settingsDataStore) as T - else -> throw IllegalArgumentException("Unknown ViewModel class") - } - } - } - } -} - -@Composable -fun App(createViewModelFactory: (Class) -> ViewModelProvider.Factory) { - val navController = rememberNavController() - val screens = listOf( - Screen.Home, - Screen.Training, - Screen.History, - Screen.Settings - ) - - val sharedTrainingViewModel: TrainingViewModel = - androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(TrainingViewModel::class.java)) - - - Scaffold( - bottomBar = { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - screens.forEach { screen -> - NavigationBarItem( - icon = { Icon(screen.icon, contentDescription = null) }, - label = { Text(screen.title) }, - selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - } - } - ) { innerPadding -> - NavHost( - navController, - startDestination = Screen.Home.route, - Modifier.padding(innerPadding) - ) { - composable(Screen.Home.route) { - val homeViewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(HomeViewModel::class.java)) - HomeScreen( - viewModel = homeViewModel, - onStartTrainingClicked = { - sharedTrainingViewModel.startTraining() - navController.navigate(Screen.Training.route) - } - ) - } - composable(Screen.Training.route) { - TrainingScreen(viewModel = sharedTrainingViewModel) - } - composable(Screen.History.route) { - val historyViewModel: HistoryViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(HistoryViewModel::class.java)) - HistoryScreen(viewModel = historyViewModel) - } - composable(Screen.Settings.route) { - val settingsViewModel: SettingsViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = createViewModelFactory(SettingsViewModel::class.java)) - SettingsScreen(viewModel = settingsViewModel) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt b/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt deleted file mode 100644 index b6222ed..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/datastore/SettingsDataStore.kt +++ /dev/null @@ -1,54 +0,0 @@ -package de.patani.kettlebelltracker.data.datastore - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.doublePreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -val Context.dataStore: DataStore by preferencesDataStore(name = "settings") - -class SettingsDataStore(context: Context) { - private val dataStore = context.dataStore - - companion object { - val TRAINING_TIME_MINUTES = intPreferencesKey("trainingTimeMinutes") - val WEIGHT_LEFT = doublePreferencesKey("weightLeft") - val WEIGHT_RIGHT = doublePreferencesKey("weightRight") - val GOAL_SETS = intPreferencesKey("goalSets") - val INITIAL_PROGRAM = stringPreferencesKey("initialProgram") - } - - val settingsFlow: Flow = dataStore.data.map { preferences -> - Settings( - trainingTimeMinutes = preferences[TRAINING_TIME_MINUTES] ?: 20, - weightLeft = preferences[WEIGHT_LEFT] ?: 16.0, - weightRight = preferences[WEIGHT_RIGHT] ?: 16.0, - goalSets = preferences[GOAL_SETS] ?: 5, - initialProgram = preferences[INITIAL_PROGRAM] ?: "giant_1.0" - ) - } - - suspend fun saveSettings(settings: Settings) { - dataStore.edit { preferences -> - preferences[TRAINING_TIME_MINUTES] = settings.trainingTimeMinutes - preferences[WEIGHT_LEFT] = settings.weightLeft - preferences[WEIGHT_RIGHT] = settings.weightRight - preferences[GOAL_SETS] = settings.goalSets - preferences[INITIAL_PROGRAM] = settings.initialProgram - } - } -} - -data class Settings( - val trainingTimeMinutes: Int, - val weightLeft: Double, - val weightRight: Double, - val goalSets: Int, - val initialProgram: String -) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt deleted file mode 100644 index 087f901..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/local/AppDatabase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.patani.kettlebelltracker.data.local - -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters - -@Database(entities = [TrainingSession::class], version = 1, exportSchema = false) -@TypeConverters(Converters::class) -abstract class AppDatabase : RoomDatabase() { - abstract fun trainingSessionDao(): TrainingSessionDao -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt deleted file mode 100644 index 213a881..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/local/Converters.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.patani.kettlebelltracker.data.local - -import androidx.room.TypeConverter -import java.util.Date - -class Converters { - @TypeConverter - fun fromTimestamp(value: Long?): Date? { - return value?.let { Date(it) } - } - - @TypeConverter - fun dateToTimestamp(date: Date?): Long? { - return date?.time - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt deleted file mode 100644 index ec91096..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSession.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.patani.kettlebelltracker.data.local - -import androidx.room.Entity -import androidx.room.PrimaryKey -import java.util.Date - -@Entity(tableName = "training_session") -data class TrainingSession( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, - val date: Date, - val sets: Int, - val weightLeft: Double, - val weightRight: Double, - val repsPerSet: Int, - val duration: Long, // in seconds - val program: String, - val blockDay: Int -) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt b/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt deleted file mode 100644 index 1772939..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/local/TrainingSessionDao.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.patani.kettlebelltracker.data.local - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Update -import kotlinx.coroutines.flow.Flow - -@Dao -interface TrainingSessionDao { - @Query("SELECT * FROM training_session ORDER BY date DESC") - fun getAllSessions(): Flow> - - @Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 20") - fun getHistory(): Flow> - - @Query("SELECT * FROM training_session ORDER BY date DESC LIMIT 1") - fun getLastSession(): Flow - - @Query("SELECT COUNT(*) FROM training_session") - fun getTrainingCount(): Flow - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(session: TrainingSession) - - @Update - suspend fun update(session: TrainingSession) - - @Delete - suspend fun delete(session: TrainingSession) - - @Query("DELETE FROM training_session WHERE id = :sessionId") - suspend fun deleteById(sessionId: Long) -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt b/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt deleted file mode 100644 index ff141cc..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/remote/ApiService.kt +++ /dev/null @@ -1,38 +0,0 @@ -package de.patani.kettlebelltracker.data.remote - -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.POST - -interface ApiService { - @POST("trainings") - suspend fun sendTrainingData(@Body payload: TrainingPayload): Response - - @POST("trainings/recommend-rest") - suspend fun getRecommendedRest(@Body request: RestRecommendationRequest): Response> - -} - -data class RestRecommendationRequest( - val uuid: String, - val reps_per_set: Int, - val current_sets: Int -) - -data class RestRecommendationResponse( - val recommended_rest_seconds: Int -) - -data class ApiResponse( - val status: String, - val message: String?, - val data: T? -) - -data class RestRecommendationData( - val recommended_rest: Int, - val expected_sets: Int, - val last_training_rest: Int?, - val last_training_sets: Int?, - val reasoning: String? -) diff --git a/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt b/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt deleted file mode 100644 index 01ad4a1..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/data/remote/TrainingPayload.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.patani.kettlebelltracker.data.remote - -data class TrainingPayload( - val reps: Int, - val rest: Double, - val sets: Int, - val uuid: String -) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt b/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt deleted file mode 100644 index d6f4768..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/repositories/ApiRepository.kt +++ /dev/null @@ -1,54 +0,0 @@ -package de.patani.kettlebelltracker.repositories - -import android.util.Log -import de.patani.kettlebelltracker.data.remote.TrainingPayload -import de.patani.kettlebelltracker.data.remote.ApiService -import de.patani.kettlebelltracker.data.remote.RestRecommendationData -import de.patani.kettlebelltracker.data.remote.RestRecommendationRequest -import de.patani.kettlebelltracker.data.remote.RestRecommendationResponse - -class ApiRepository(private val apiService: ApiService) { - - suspend fun sendTrainingData(session: de.patani.kettlebelltracker.data.local.TrainingSession, uuid: String) { - try { - val rest = if (session.sets > 0) { - session.duration.toDouble() / session.sets.toDouble() - } else { - 0.0 - } - - val payload = TrainingPayload( - reps = session.repsPerSet, - rest = rest, - sets = session.sets, - uuid = uuid - ) - val response = apiService.sendTrainingData(payload) - if (response.isSuccessful) { - Log.i("ApiRepository", "Training successfully sent to backend.") - } else { - Log.e("ApiRepository", "API Error: Unexpected status code: ${response.code()}") - } - } catch (e: Exception) { - Log.e("ApiRepository", "API Error: Failed to send training data", e) - } - } - - suspend fun getRecommendedRest(uuid: String, repsPerSet: Int, currentSets: Int): RestRecommendationData? { - return try { - val request = RestRecommendationRequest(uuid, repsPerSet, currentSets) - val response = apiService.getRecommendedRest(request) - if (response.isSuccessful) { - Log.i("ApiRepository", "Got Rest Recommendation:") - val body = response.body()?.data - Log.i("ApiRepository", body.toString()) - response.body()?.data - } else { - null - } - } catch (e: Exception) { - null - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt deleted file mode 100644 index b79bd02..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/navigation/Screen.kt +++ /dev/null @@ -1,15 +0,0 @@ -package de.patani.kettlebelltracker.ui.navigation - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DateRange -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Settings -import androidx.compose.ui.graphics.vector.ImageVector - -sealed class Screen(val route: String, val title: String, val icon: ImageVector) { - object Home : Screen("home", "Home", Icons.Default.Home) - object Training : Screen("training", "Training", Icons.Default.PlayArrow) - object History : Screen("history", "Historie", Icons.Default.DateRange) - object Settings : Screen("settings", "Einstellungen", Icons.Default.Settings) -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt deleted file mode 100644 index ece7ac8..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HistoryScreen.kt +++ /dev/null @@ -1,229 +0,0 @@ -package de.patani.kettlebelltracker.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import de.patani.kettlebelltracker.viewmodels.HistoryViewModel -import de.patani.kettlebelltracker.util.formatDate -import de.patani.kettlebelltracker.util.formatDuration -import de.patani.kettlebelltracker.data.local.TrainingSession -import androidx.compose.ui.Alignment - -@Composable -fun HistoryScreen(viewModel: HistoryViewModel) { - val history by viewModel.history.collectAsState() - - if (history.isEmpty()) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Noch keine Trainingsdaten vorhanden.") - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - item { - Row(Modifier.fillMaxWidth()) { - Text("Datum", modifier = Modifier.weight(1f)) - Text("Sätze", modifier = Modifier.weight(0.5f)) - Text("Dauer", modifier = Modifier.weight(0.7f)) - Text("Reps", modifier = Modifier.weight(0.5f)) - } - Divider(modifier = Modifier.padding(vertical = 8.dp)) - } - items(history) { session -> - HistoryItem( - session = session, - onUpdate = viewModel::updateSession, - onDelete = viewModel::deleteSession - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun HistoryItem(session: TrainingSession, onUpdate: (TrainingSession) -> Unit, onDelete: (TrainingSession) -> Unit) { - var showDialog by remember { mutableStateOf(false) } - - Card(onClick = { showDialog = true }, modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.padding(16.dp)) { - Text(text = session.date.formatDate(), modifier = Modifier.weight(1f)) - Text(text = session.sets.toString(), modifier = Modifier.weight(0.5f)) - Text(text = formatDuration(session.duration), modifier = Modifier.weight(0.7f)) - Text(text = session.repsPerSet.toString(), modifier = Modifier.weight(0.5f)) - } - } - - if (showDialog) { - EditHistoryDialog( - session = session, - onDismiss = { showDialog = false }, - onSave = { updatedSession -> - onUpdate(updatedSession) - showDialog = false - }, - onDelete = { - onDelete(session) - showDialog = false - } - ) - } -} - -@Composable -fun EditHistoryDialog(session: TrainingSession, onDismiss: () -> Unit, onSave: (TrainingSession) -> Unit, onDelete: () -> Unit) { - var editedSets by remember { mutableStateOf(session.sets.toString()) } - var editedRepsPerSet by remember { mutableStateOf(session.repsPerSet.toString()) } - var editedWeightLeft by remember { mutableStateOf(session.weightLeft.toString()) } - var editedWeightRight by remember { mutableStateOf(session.weightRight.toString()) } - var editedDurationMinutes by remember { mutableStateOf((session.duration / 60).toString()) } - var editedDurationSeconds by remember { mutableStateOf((session.duration % 60).toString()) } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Eintrag bearbeiten") }, - text = { - Column { - Text("Datum: ${session.date.formatDate()}", style = MaterialTheme.typography.bodyMedium) - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = editedSets, - onValueChange = { newValue -> - if (newValue.all { it.isDigit() } || newValue.isEmpty()) { - editedSets = newValue - } - }, - label = { Text("Sätze") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - singleLine = true - ) - - OutlinedTextField( - value = editedRepsPerSet, - onValueChange = { newValue -> - if (newValue.all { it.isDigit() } || newValue.isEmpty()) { - editedRepsPerSet = newValue - } - }, - label = { Text("Wiederholungen pro Satz") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - singleLine = true - ) - - OutlinedTextField( - value = editedWeightLeft, - onValueChange = { newValue -> - if (newValue.matches(Regex("^\\d*\\.?\\d*\$"))) { - editedWeightLeft = newValue - } - }, - label = { Text("Gewicht Links (kg)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - singleLine = true - ) - - OutlinedTextField( - value = editedWeightRight, - onValueChange = { newValue -> - if (newValue.matches(Regex("^\\d*\\.?\\d*\$"))) { - editedWeightRight = newValue - } - }, - label = { Text("Gewicht Rechts (kg)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - singleLine = true - ) - - Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) { - OutlinedTextField( - value = editedDurationMinutes, - onValueChange = { newValue -> - if (newValue.all { it.isDigit() } || newValue.isEmpty()) { - editedDurationMinutes = newValue - } - }, - label = { Text("Dauer (Min)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f).padding(end = 4.dp), - singleLine = true - ) - OutlinedTextField( - value = editedDurationSeconds, - onValueChange = { newValue -> - if (newValue.all { it.isDigit() } || newValue.isEmpty()) { - editedDurationSeconds = newValue - } - }, - label = { Text("Dauer (Sek)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f).padding(start = 4.dp), - singleLine = true - ) - } - Text("Programm: ${session.program}", style = MaterialTheme.typography.bodySmall) - Text("Block Tag: ${session.blockDay}", style = MaterialTheme.typography.bodySmall) - } - }, - confirmButton = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onDelete, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - modifier = Modifier.weight(1f).padding(end = 4.dp) - ) { - Icon(Icons.Default.Delete, contentDescription = "Löschen") - Spacer(Modifier.width(4.dp)) - Text("Löschen") - } - Button( - onClick = { - val newSets = editedSets.toIntOrNull() ?: session.sets - val newRepsPerSet = editedRepsPerSet.toIntOrNull() ?: session.repsPerSet - val newWeightLeft = editedWeightLeft.toDoubleOrNull() ?: session.weightLeft - val newWeightRight = editedWeightRight.toDoubleOrNull() ?: session.weightRight - val newDurationMinutes = editedDurationMinutes.toLongOrNull() ?: 0L - val newDurationSeconds = editedDurationSeconds.toLongOrNull() ?: 0L - val newDuration = newDurationMinutes * 60 + newDurationSeconds - - val updatedSession = session.copy( - sets = newSets, - repsPerSet = newRepsPerSet, - weightLeft = newWeightLeft, - weightRight = newWeightRight, - duration = newDuration - ) - onSave(updatedSession) - }, - modifier = Modifier.weight(1f).padding(start = 4.dp) - ) { - Text("Speichern") - } - } - }, - dismissButton = { - Button(onClick = onDismiss) { - Text("Abbrechen") - } - } - ) -} diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt deleted file mode 100644 index 56f6c3a..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/HomeScreen.kt +++ /dev/null @@ -1,85 +0,0 @@ -package de.patani.kettlebelltracker.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import de.patani.kettlebelltracker.viewmodels.HomeViewModel -import de.patani.kettlebelltracker.util.formatDate -import de.patani.kettlebelltracker.util.formatDuration -import java.util.Calendar - -@Composable -fun HomeScreen( - viewModel: HomeViewModel, - onStartTrainingClicked: () -> Unit -) { - val state by viewModel.homeScreenState.collectAsState() - - val isTrainedToday = remember(state.lastTrainingSession) { - val lastDate = state.lastTrainingSession?.date - if (lastDate == null) false - else { - val cal1 = Calendar.getInstance().apply { time = lastDate } - val cal2 = Calendar.getInstance() - cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && - cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceAround - ) { - Text( - text = "Kettlebell Workout Tracker", - style = MaterialTheme.typography.headlineMedium, - textAlign = TextAlign.Center - ) - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text("Nächstes Training", style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.height(8.dp)) - Text("${state.nextTrainingProgram} - Tag ${state.nextTrainingBlockDay}") - Spacer(modifier = Modifier.height(8.dp)) - Text("Ziel: ${state.nextTrainingReps} Wiederholungen pro Satz") - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onStartTrainingClicked, enabled = !isTrainedToday) { - Text(if (isTrainedToday) "Heute bereits trainiert" else "Training starten") - } - } - } - - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - Text("Letzte Leistung", style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally)) - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround - ) { - StatItem("Datum", state.lastTrainingSession?.date?.formatDate() ?: "–") - StatItem("Sätze", state.lastTrainingSession?.sets?.toString() ?: "–") - StatItem("Dauer", formatDuration(state.lastTrainingSession?.duration ?: 0)) - StatItem("Gewicht", "${state.lastTrainingSession?.weightLeft ?: "–"}kg") - } - } - } - } -} - -@Composable -fun StatItem(label: String, value: String) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = label, style = MaterialTheme.typography.labelMedium) - Text(text = value, style = MaterialTheme.typography.bodyLarge, fontSize = 18.sp) - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt deleted file mode 100644 index d85f717..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/SettingsScreen.kt +++ /dev/null @@ -1,84 +0,0 @@ -package de.patani.kettlebelltracker.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import de.patani.kettlebelltracker.viewmodels.SettingsViewModel -import kotlinx.coroutines.launch - -@Composable -fun SettingsScreen(viewModel: SettingsViewModel) { - val settings by viewModel.settings.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() - - var time by remember(settings.trainingTimeMinutes) { mutableStateOf(settings.trainingTimeMinutes.toString()) } - var sets by remember(settings.goalSets) { mutableStateOf(settings.goalSets.toString()) } - var weightLeft by remember(settings.weightLeft) { mutableStateOf(settings.weightLeft.toString()) } - var weightRight by remember(settings.weightRight) { mutableStateOf(settings.weightRight.toString()) } - - Scaffold( - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text("Einstellungen", style = MaterialTheme.typography.headlineSmall) - - OutlinedTextField( - value = time, - onValueChange = { time = it }, - label = { Text("Trainingszeit (Minuten)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = sets, - onValueChange = { sets = it }, - label = { Text("Ziel-Sätze") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = weightLeft, - onValueChange = { weightLeft = it }, - label = { Text("Gewicht Links (kg)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = weightRight, - onValueChange = { weightRight = it }, - label = { Text("Gewicht Rechts (kg)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth() - ) - - Button( - onClick = { - val timeInt = time.toIntOrNull() ?: settings.trainingTimeMinutes - val setsInt = sets.toIntOrNull() ?: settings.goalSets - val weightL = weightLeft.toDoubleOrNull() ?: settings.weightLeft - val weightR = weightRight.toDoubleOrNull() ?: settings.weightRight - - viewModel.saveSettings(timeInt, setsInt, weightL, weightR) - - scope.launch { - snackbarHostState.showSnackbar("Einstellungen gespeichert!") - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Speichern") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt deleted file mode 100644 index 0a76673..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/screens/TrainingScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -package de.patani.kettlebelltracker.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import de.patani.kettlebelltracker.viewmodels.TrainingViewModel -import de.patani.kettlebelltracker.util.formatDuration - -@Composable -fun TrainingScreen(viewModel: TrainingViewModel) { - val trainingState by viewModel.trainingState.collectAsState() - val showFinishButton = trainingState.remainingSeconds <= 0 && trainingState.isTrainingRunning - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly - ) { - - if (trainingState.isRoundActive) { - RoundTimerCard( - timeRemaining = trainingState.currentRoundTime, - totalTime = trainingState.totalRoundTime, - ) - } - - if (trainingState.isRoundActive) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Verbleibende Zeit", style = MaterialTheme.typography.titleLarge) - Text( - text = formatDuration(trainingState.remainingSeconds.toLong()), - fontSize = 72.sp, - fontWeight = FontWeight.Bold, - color = if (trainingState.remainingSeconds <= 10) - MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.onSurface - ) - } - } - - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Sätze", style = MaterialTheme.typography.titleLarge) - Text( - text = "${trainingState.setsDone} / ${trainingState.goalSets}", - fontSize = 60.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.tertiary - ) - Text( - text = "${trainingState.repsPerSet} Wiederholungen", - fontSize = 24.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (trainingState.isRoundActive) { - Button( - onClick = viewModel::completeSet, - enabled = trainingState.isTrainingRunning, - modifier = Modifier - .fillMaxWidth() - .height(60.dp) - ) { - Text("Satz abschließen", fontSize = 18.sp) - } - } - - if (showFinishButton) { - Button( - onClick = viewModel::finishTraining, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ), - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - ) { - Text("Training beenden & Speichern", fontSize = 16.sp) - } - } - } - } -} - -@Composable -fun RoundTimerCard( - timeRemaining: Int, - totalTime: Int -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - colors = CardDefaults.cardColors( - containerColor = when { - timeRemaining <= 0 -> MaterialTheme.colorScheme.errorContainer - timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f) - else -> MaterialTheme.colorScheme.primaryContainer - } - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = if (timeRemaining <= 0) "Zeit abgelaufen!" else "Rundenzeit", - style = MaterialTheme.typography.titleLarge, - color = when { - timeRemaining <= 0 -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.primary - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Box( - modifier = Modifier.size(120.dp), - contentAlignment = Alignment.Center - ) { - val progress = if (totalTime > 0 && timeRemaining >= 0) { - (totalTime - timeRemaining).toFloat() / totalTime.toFloat() - } else if (timeRemaining < 0) 1f else 0f - - CircularProgressIndicator( - progress = progress, - modifier = Modifier.fillMaxSize(), - strokeWidth = 8.dp, - color = when { - timeRemaining <= 0 -> MaterialTheme.colorScheme.error - timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.primary - } - ) - - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = if (timeRemaining >= 0) { - formatDuration(timeRemaining.toLong()) - } else { - "+${formatDuration((-timeRemaining).toLong())}" - }, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = when { - timeRemaining <= 0 -> MaterialTheme.colorScheme.error - timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.primary - } - ) - Text( - text = if (timeRemaining <= 0) "überzogen" else "verbleibend", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt deleted file mode 100644 index dba3d63..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.patani.kettlebelltracker.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt deleted file mode 100644 index ad317fc..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Theme.kt +++ /dev/null @@ -1,30 +0,0 @@ -package de.patani.kettlebelltracker.ui.theme - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.runtime.Composable - -import androidx.compose.ui.graphics.Color - -private val DarkColorScheme = darkColorScheme( - primary = Color(0xFF61AFEF), // Blue - secondary = Color(0xFFC678DD), // Purple - tertiary = Color(0xFF98C379), // Green - background = Color(0xFF282C34), - surface = Color(0xFF2C313A), - onPrimary = Color.Black, - onSecondary = Color.Black, - onTertiary = Color.Black, - onBackground = Color(0xFFABB2BF), - onSurface = Color(0xFFABB2BF), - error = Color(0xFFE06C75), // Red -) - -@Composable -fun KettlebellTrackerTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = DarkColorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt b/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt deleted file mode 100644 index f6ed694..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/ui/theme/Type.kt +++ /dev/null @@ -1,17 +0,0 @@ -package de.patani.kettlebelltracker.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) -) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt b/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt deleted file mode 100644 index 5bd9038..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/util/formatDuration.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.patani.kettlebelltracker.util - -import android.annotation.SuppressLint -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -fun Date.formatDate(): String { - val formatter = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) - return formatter.format(this) -} - -@SuppressLint("DefaultLocale") -fun formatDuration(totalSeconds: Long): String { - if (totalSeconds < 0) return "00:00" - val minutes = totalSeconds / 60 - val seconds = totalSeconds % 60 - return String.format("%02d:%02d", minutes, seconds) -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt deleted file mode 100644 index 1827055..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HistoryViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package de.patani.kettlebelltracker.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import de.patani.kettlebelltracker.data.datastore.SettingsDataStore -import de.patani.kettlebelltracker.data.local.TrainingSessionDao -import de.patani.kettlebelltracker.data.local.TrainingSession -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import java.util.Date - -class HistoryViewModel(private val dao: TrainingSessionDao) : ViewModel() { - val history = dao.getHistory() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) - - fun updateSession(session: TrainingSession) = viewModelScope.launch { - dao.update(session) - } - - fun deleteSession(session: TrainingSession) = viewModelScope.launch { - dao.delete(session) - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt deleted file mode 100644 index c662d01..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/HomeViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -package de.patani.kettlebelltracker.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import de.patani.kettlebelltracker.data.local.TrainingSessionDao -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn - -class HomeViewModel( - dao: TrainingSessionDao, - trainingViewModel: TrainingViewModel -) : ViewModel() { - - val lastSession = dao.getLastSession() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - - val homeScreenState = combine(lastSession, trainingViewModel.trainingState) { last, trainingState -> - HomeScreenState( - lastTrainingSession = last, - nextTrainingProgram = trainingState.currentProgram, - nextTrainingBlockDay = trainingState.currentBlockDay, - nextTrainingReps = trainingState.currentReps - ) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomeScreenState()) -} - -data class HomeScreenState( - val lastTrainingSession: de.patani.kettlebelltracker.data.local.TrainingSession? = null, - val nextTrainingProgram: String = "", - val nextTrainingBlockDay: Int = 0, - val nextTrainingReps: Int = 0 -) \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt deleted file mode 100644 index 0c61f5c..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/SettingsViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package de.patani.kettlebelltracker.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import de.patani.kettlebelltracker.data.datastore.Settings -import de.patani.kettlebelltracker.data.datastore.SettingsDataStore -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -class SettingsViewModel(private val settingsDataStore: SettingsDataStore) : ViewModel() { - val settings = settingsDataStore.settingsFlow - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Settings(0,0.0,0.0,0,"")) - - fun saveSettings( - time: Int, - sets: Int, - weightLeft: Double, - weightRight: Double - ) = viewModelScope.launch { - val currentSettings = settings.value - settingsDataStore.saveSettings( - currentSettings.copy( - trainingTimeMinutes = time, - goalSets = sets, - weightLeft = weightLeft, - weightRight = weightRight - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt b/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt deleted file mode 100644 index 51aaf2e..0000000 --- a/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt +++ /dev/null @@ -1,256 +0,0 @@ -package de.patani.kettlebelltracker.viewmodels - -import android.media.AudioManager -import android.media.ToneGenerator -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import de.patani.kettlebelltracker.data.local.TrainingSessionDao -import de.patani.kettlebelltracker.data.datastore.SettingsDataStore -import de.patani.kettlebelltracker.repositories.ApiRepository -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.util.Date -import kotlin.math.min - -data class TrainingState( - val isTrainingRunning: Boolean = false, - val remainingSeconds: Int = 0, - val initialDurationSeconds: Int = 0, - val setsDone: Int = 0, - val goalSets: Int = 5, - val repsPerSet: Int = 5, - val progress: Float = 0.0f, - val currentProgram: String = "clean_1.0", - val currentBlockDay: Int = 1, - val currentReps: Int = 5, - val totalTrainingDays: Int = 0, - val isRoundActive: Boolean = false, - val currentRoundTime: Int = 0, - val totalRoundTime: Int = 0 -) - -class TrainingViewModel( - private val dao: TrainingSessionDao, - private val settingsDataStore: SettingsDataStore, - private val apiRepository: ApiRepository, - private val appUUID: String -) : ViewModel() { - - private val _trainingState = MutableStateFlow(TrainingState()) - val trainingState = _trainingState.asStateFlow() - - private var timerJob: Job? = null - private var roundTimerJob: Job? = null - - init { - viewModelScope.launch { - val trainingCount = dao.getTrainingCount().first() - val initialState = calculateStateByDayCount(trainingCount) - _trainingState.update { - it.copy( - totalTrainingDays = trainingCount, - currentProgram = initialState.program, - currentBlockDay = initialState.blockDay, - currentReps = initialState.reps, - repsPerSet = initialState.reps - ) - } - } - } - - private var recommendedRestSeconds: Int = 90 - - fun startTraining() { - if (_trainingState.value.isTrainingRunning) return - - viewModelScope.launch { - val settings = settingsDataStore.settingsFlow.first() - val durationSeconds = settings.trainingTimeMinutes * 60 - - _trainingState.update { - it.copy( - isTrainingRunning = true, - initialDurationSeconds = durationSeconds, - remainingSeconds = durationSeconds, - goalSets = settings.goalSets, - setsDone = 0, - progress = 0.0f - ) - } - val recommendation = apiRepository.getRecommendedRest( - uuid = appUUID, - repsPerSet = _trainingState.value.repsPerSet, - currentSets = 0 // ggf. 0 oder deinen Startwert - ) - recommendedRestSeconds = recommendation?.recommended_rest ?: 90 - Log.d("Training", "using rest time ${recommendedRestSeconds.toString()}") - startTimer() - startFirstRound() - } - } - - private fun startTimer() { - timerJob?.cancel() - timerJob = viewModelScope.launch { - while (_trainingState.value.remainingSeconds > 0 && _trainingState.value.isTrainingRunning) { - delay(1000) - _trainingState.update { it.copy(remainingSeconds = it.remainingSeconds - 1) } - } - if (_trainingState.value.isTrainingRunning) { - finishTraining() - } - } - } - - private fun startFirstRound() { - startRoundTimer(recommendedRestSeconds) - } - - fun completeSet() { - if (!_trainingState.value.isTrainingRunning) return - - val currentState = _trainingState.value - val newSetsDone = currentState.setsDone + 1 - val newProgress = if (currentState.goalSets > 0) { - min(newSetsDone.toFloat() / currentState.goalSets.toFloat(), 1.0f) - } else 0.0f - - _trainingState.update { - it.copy( - setsDone = newSetsDone, - progress = newProgress - ) - } - - stopRoundTimer() - - startNextRound() - } - - private fun startNextRound() { - startRoundTimer(recommendedRestSeconds) - } - - private fun startRoundTimer(seconds: Int) { - roundTimerJob?.cancel() - - _trainingState.update { - it.copy( - isRoundActive = true, - currentRoundTime = seconds, - totalRoundTime = seconds - ) - } - - roundTimerJob = viewModelScope.launch { - while (_trainingState.value.currentRoundTime > 0 && _trainingState.value.isRoundActive) { - delay(1000) - _trainingState.update { - it.copy(currentRoundTime = it.currentRoundTime - 1) - } - } - - if (_trainingState.value.isRoundActive) { - playRoundCompleteSound() - } - } - } - - private fun stopRoundTimer() { - roundTimerJob?.cancel() - _trainingState.update { - it.copy( - isRoundActive = false, - currentRoundTime = 0, - totalRoundTime = 0 - ) - } - } - - private fun playRoundCompleteSound() { - viewModelScope.launch { - try { - val toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100) - toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 1000) - } catch (e: Exception) { - Log.e("TrainingViewModel", "Could not play sound", e) - } - } - } - - fun finishTraining() { - timerJob?.cancel() - roundTimerJob?.cancel() - if (!_trainingState.value.isTrainingRunning) return - - viewModelScope.launch { - val state = _trainingState.value - val settings = settingsDataStore.settingsFlow.first() - - val session = de.patani.kettlebelltracker.data.local.TrainingSession( - date = Date(), - sets = state.setsDone, - weightLeft = settings.weightLeft, - weightRight = settings.weightRight, - repsPerSet = state.repsPerSet, - duration = (state.initialDurationSeconds - state.remainingSeconds).toLong(), - program = state.currentProgram, - blockDay = state.currentBlockDay - ) - - dao.insert(session) - Log.d("Training", "Sending Trainingsession to backend: $appUUID") - apiRepository.sendTrainingData(session, appUUID) - resetTraining() - } - } - - private suspend fun resetTraining() { - val trainingCount = dao.getTrainingCount().first() - val nextState = calculateStateByDayCount(trainingCount) - _trainingState.value = TrainingState( - totalTrainingDays = trainingCount, - currentProgram = nextState.program, - currentBlockDay = nextState.blockDay, - currentReps = nextState.reps, - repsPerSet = nextState.reps - ) - } - - private fun calculateStateByDayCount(totalDays: Int): ProgramState { - if (totalDays <= 0) { - return ProgramState("clean_1.0", 1, 5) - } - - val cycleIndex = (totalDays / 12) % 6 - val programs = listOf("clean_1.0", "snatch_1.0", "clean_1.1", "snatch_1.1", "clean_1.2", "snatch_1.2") - val program = programs[cycleIndex] - val blockDay = (totalDays % 3) + 1 - - val repsMap = mapOf( - "clean_1.0" to listOf(5, 6, 4), - "clean_1.1" to listOf(6, 8, 7), - "clean_1.2" to listOf(7, 9, 8), - "snatch_1.0" to listOf(5, 6, 4), - "snatch_1.1" to listOf(6, 8, 7), - "snatch_1.2" to listOf(7, 9, 8) - ) - - val reps = repsMap[program]?.getOrNull(blockDay - 1) ?: 5 - return ProgramState(program, blockDay, reps) - } - - override fun onCleared() { - super.onCleared() - timerJob?.cancel() - roundTimerJob?.cancel() - } - - data class ProgramState(val program: String, val blockDay: Int, val reps: Int) -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index d1aa070..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - kettlebelltracker - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 9a13f97..0000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - -