Selaa lähdekoodia

Display all workouts on the home page

Lee Morgan 3 viikkoa sitten
vanhempi
sitoutus
20d38cb090

+ 4 - 0
app/src/main/java/com/example/torus/data/WorkoutDao.kt

@@ -3,6 +3,7 @@ package com.example.torus.data
 import androidx.room.Dao
 import androidx.room.Insert
 import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
 
 @Dao
 interface WorkoutDao {
@@ -12,6 +13,9 @@ interface WorkoutDao {
     @Insert
     suspend fun insertExercises(exercises: List<WorkoutExerciseEntity>)
 
+    @Query("SELECT * FROM workout ORDER BY id DESC")
+    fun observeWorkouts(): Flow<List<WorkoutEntity>>
+
     @Query("SELECT * FROM workout WHERE id = :id")
     suspend fun getWorkout(id: String): WorkoutEntity?
 

+ 32 - 0
app/src/main/java/com/example/torus/data/WorkoutRepository.kt

@@ -3,10 +3,42 @@ package com.example.torus.data
 import androidx.room.withTransaction
 import com.example.torus.id.UuidV7
 import com.example.torus.model.DraftExercise
+import com.example.torus.model.ExerciseType
+import com.example.torus.model.SavedExercise
+import com.example.torus.model.WorkoutDetail
+import com.example.torus.model.WorkoutSummary
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 class WorkoutRepository(private val database: TorusDatabase) {
     private val workoutDao = database.workoutDao()
 
+    fun observeWorkouts(): Flow<List<WorkoutSummary>> {
+        return workoutDao.observeWorkouts().map { workouts ->
+            workouts.map { workout ->
+                WorkoutSummary(id = workout.id, name = workout.name)
+            }
+        }
+    }
+
+    suspend fun getWorkoutDetail(id: String): WorkoutDetail? {
+        val workout = workoutDao.getWorkout(id) ?: return null
+        val exercises = workoutDao.getExercisesForWorkout(id).map { exercise ->
+            SavedExercise(
+                id = exercise.id,
+                name = exercise.name,
+                type = ExerciseType.fromStorage(exercise.type),
+                isActive = exercise.isActive,
+                notes = exercise.notes
+            )
+        }
+        return WorkoutDetail(
+            id = workout.id,
+            name = workout.name,
+            exercises = exercises
+        )
+    }
+
     suspend fun saveWorkout(name: String, exercises: List<DraftExercise>): String {
         return database.withTransaction {
             val workoutId = UuidV7.generateString()

+ 20 - 0
app/src/main/java/com/example/torus/model/Workout.kt

@@ -0,0 +1,20 @@
+package com.example.torus.model
+
+data class WorkoutSummary(
+    val id: String,
+    val name: String,
+)
+
+data class WorkoutDetail(
+    val id: String,
+    val name: String,
+    val exercises: List<SavedExercise>,
+)
+
+data class SavedExercise(
+    val id: String,
+    val name: String,
+    val type: ExerciseType,
+    val isActive: Boolean,
+    val notes: String,
+)

+ 0 - 6
app/src/main/java/com/example/torus/ui/CreateWorkoutScreen.kt

@@ -137,12 +137,6 @@ fun CreateWorkoutScreen(
     }
 }
 
-private val ExerciseType.labelRes: Int
-    get() = when (this) {
-        ExerciseType.Weights -> R.string.exercise_type_weights
-        ExerciseType.Run -> R.string.exercise_type_run
-    }
-
 private fun updateExercise(
     exercises: MutableList<DraftExercise>,
     id: String,

+ 10 - 0
app/src/main/java/com/example/torus/ui/ExerciseTypeLabels.kt

@@ -0,0 +1,10 @@
+package com.example.torus.ui
+
+import com.example.torus.R
+import com.example.torus.model.ExerciseType
+
+val ExerciseType.labelRes: Int
+    get() = when (this) {
+        ExerciseType.Weights -> R.string.exercise_type_weights
+        ExerciseType.Run -> R.string.exercise_type_run
+    }

+ 40 - 2
app/src/main/java/com/example/torus/ui/HomeScreen.kt

@@ -1,12 +1,17 @@
 package com.example.torus.ui
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -14,12 +19,15 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import com.example.torus.R
+import com.example.torus.model.WorkoutSummary
 import com.example.torus.ui.theme.TorusTheme
 
 @Composable
 fun HomeScreen(
+    workouts: List<WorkoutSummary>,
     modifier: Modifier = Modifier,
-    onAddExercise: () -> Unit = {}
+    onAddExercise: () -> Unit = {},
+    onWorkoutClick: (String) -> Unit = {}
 ) {
     Column(modifier = modifier.fillMaxSize()) {
         Button(
@@ -33,16 +41,46 @@ fun HomeScreen(
         ) {
             Text(text = stringResource(R.string.add_exercise))
         }
+        LazyColumn(
+            modifier = Modifier
+                .weight(1f)
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp),
+            verticalArrangement = Arrangement.spacedBy(12.dp)
+        ) {
+            items(workouts, key = { it.id }) { workout ->
+                OutlinedButton(
+                    onClick = { onWorkoutClick(workout.id) },
+                    modifier = Modifier.fillMaxWidth(),
+                    shape = RoundedCornerShape(50),
+                    colors = ButtonDefaults.outlinedButtonColors(
+                        contentColor = MaterialTheme.colorScheme.onBackground
+                    )
+                ) {
+                    Text(text = workout.displayName())
+                }
+            }
+        }
     }
 }
 
+@Composable
+fun WorkoutSummary.displayName(): String {
+    return name.ifBlank { stringResource(R.string.untitled_workout) }
+}
+
 @Preview(showBackground = true, backgroundColor = 0xFF0C0F14)
 @Composable
 private fun HomeScreenPreview() {
     TorusTheme {
         Column {
             AppHeader()
-            HomeScreen()
+            HomeScreen(
+                workouts = listOf(
+                    WorkoutSummary(id = "1", name = "Push day"),
+                    WorkoutSummary(id = "2", name = "Easy run")
+                )
+            )
         }
     }
 }

+ 31 - 2
app/src/main/java/com/example/torus/ui/TorusApp.kt

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
@@ -17,26 +18,40 @@ import kotlinx.coroutines.launch
 private enum class Screen {
     Home,
     CreateWorkout,
+    WorkoutDetail,
 }
 
 @Composable
 fun TorusApp(workoutRepository: WorkoutRepository) {
     var screen by rememberSaveable { mutableStateOf(Screen.Home) }
+    var selectedWorkoutId by rememberSaveable { mutableStateOf<String?>(null) }
     val coroutineScope = rememberCoroutineScope()
+    val workouts by workoutRepository.observeWorkouts().collectAsState(initial = emptyList())
 
     BackHandler(enabled = screen != Screen.Home) {
         screen = Screen.Home
+        selectedWorkoutId = null
     }
 
     Scaffold(
         modifier = Modifier.fillMaxSize(),
-        topBar = { AppHeader(onClick = { screen = Screen.Home }) }
+        topBar = {
+            AppHeader(onClick = {
+                screen = Screen.Home
+                selectedWorkoutId = null
+            })
+        }
     ) { innerPadding ->
         val contentModifier = Modifier.padding(innerPadding)
         when (screen) {
             Screen.Home -> HomeScreen(
+                workouts = workouts,
                 modifier = contentModifier,
-                onAddExercise = { screen = Screen.CreateWorkout }
+                onAddExercise = { screen = Screen.CreateWorkout },
+                onWorkoutClick = { workoutId ->
+                    selectedWorkoutId = workoutId
+                    screen = Screen.WorkoutDetail
+                }
             )
             Screen.CreateWorkout -> CreateWorkoutScreen(
                 modifier = contentModifier,
@@ -47,6 +62,20 @@ fun TorusApp(workoutRepository: WorkoutRepository) {
                     }
                 }
             )
+            Screen.WorkoutDetail -> {
+                val workoutId = selectedWorkoutId
+                if (workoutId != null) {
+                    WorkoutDetailScreen(
+                        workoutId = workoutId,
+                        workoutRepository = workoutRepository,
+                        modifier = contentModifier,
+                        onBackToHome = {
+                            screen = Screen.Home
+                            selectedWorkoutId = null
+                        }
+                    )
+                }
+            }
         }
     }
 }

+ 164 - 0
app/src/main/java/com/example/torus/ui/WorkoutDetailScreen.kt

@@ -0,0 +1,164 @@
+package com.example.torus.ui
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.torus.R
+import com.example.torus.data.WorkoutRepository
+import com.example.torus.model.ExerciseType
+import com.example.torus.model.SavedExercise
+import com.example.torus.model.WorkoutDetail
+import com.example.torus.ui.theme.Sand
+import com.example.torus.ui.theme.TorusTheme
+
+@Composable
+fun WorkoutDetailScreen(
+    workoutId: String,
+    workoutRepository: WorkoutRepository,
+    modifier: Modifier = Modifier,
+    onBackToHome: () -> Unit = {}
+) {
+    var detail by remember(workoutId) { mutableStateOf<WorkoutDetail?>(null) }
+
+    LaunchedEffect(workoutId) {
+        detail = workoutRepository.getWorkoutDetail(workoutId)
+    }
+
+    detail?.let { workout ->
+        WorkoutDetailContent(
+            workout = workout,
+            modifier = modifier,
+            onBackToHome = onBackToHome
+        )
+    }
+}
+
+@Composable
+private fun WorkoutDetailContent(
+    workout: WorkoutDetail,
+    modifier: Modifier = Modifier,
+    onBackToHome: () -> Unit = {}
+) {
+    Column(modifier = modifier.fillMaxSize()) {
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .fillMaxWidth()
+                .verticalScroll(rememberScrollState())
+                .padding(horizontal = 16.dp, vertical = 8.dp),
+            verticalArrangement = Arrangement.spacedBy(16.dp)
+        ) {
+            Text(
+                text = workout.name.ifBlank { stringResource(R.string.untitled_workout) },
+                color = Sand,
+                style = MaterialTheme.typography.headlineMedium
+            )
+            workout.exercises.forEachIndexed { index, exercise ->
+                ExerciseRow(number = index + 1, exercise = exercise)
+            }
+        }
+        OutlinedButton(
+            onClick = onBackToHome,
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 12.dp),
+            shape = RoundedCornerShape(50),
+            colors = ButtonDefaults.outlinedButtonColors(
+                contentColor = MaterialTheme.colorScheme.onBackground
+            )
+        ) {
+            Text(text = stringResource(R.string.back_to_home))
+        }
+    }
+}
+
+@Composable
+private fun ExerciseRow(
+    number: Int,
+    exercise: SavedExercise
+) {
+    val shape = RoundedCornerShape(14.dp)
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .border(1.dp, MaterialTheme.colorScheme.outline, shape)
+            .padding(16.dp),
+        horizontalArrangement = Arrangement.SpaceBetween,
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        Column(
+            modifier = Modifier.weight(1f),
+            verticalArrangement = Arrangement.spacedBy(4.dp)
+        ) {
+            Text(
+                text = stringResource(exercise.type.labelRes),
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.labelLarge
+            )
+            Text(
+                text = exercise.name.ifBlank { stringResource(R.string.untitled_exercise) },
+                color = MaterialTheme.colorScheme.onBackground,
+                style = MaterialTheme.typography.bodyLarge
+            )
+        }
+        Text(
+            text = number.toString(),
+            color = MaterialTheme.colorScheme.onSurfaceVariant,
+            style = MaterialTheme.typography.labelSmall
+        )
+    }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFF0C0F14)
+@Composable
+private fun WorkoutDetailContentPreview() {
+    TorusTheme {
+        Column {
+            AppHeader()
+            WorkoutDetailContent(
+                workout = WorkoutDetail(
+                    id = "1",
+                    name = "Push day",
+                    exercises = listOf(
+                        SavedExercise(
+                            id = "a",
+                            name = "Bench",
+                            type = ExerciseType.Weights,
+                            isActive = true,
+                            notes = ""
+                        ),
+                        SavedExercise(
+                            id = "b",
+                            name = "Easy run",
+                            type = ExerciseType.Run,
+                            isActive = true,
+                            notes = ""
+                        )
+                    )
+                )
+            )
+        }
+    }
+}

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -9,4 +9,7 @@
     <string name="exercise_type_run">Run</string>
     <string name="exercise_name">Exercise name</string>
     <string name="save_workout">Save Workout</string>
+    <string name="untitled_workout">Untitled workout</string>
+    <string name="untitled_exercise">Untitled exercise</string>
+    <string name="back_to_home">Back to home</string>
 </resources>