Sfoglia il codice sorgente

Add ability to edit a workout.

Lee Morgan 2 settimane fa
parent
commit
c9bbf601e8

+ 36 - 0
app/src/androidTest/java/com/example/torus/WorkoutRepositoryTest.kt

@@ -64,15 +64,51 @@ class WorkoutRepositoryTest {
         assertEquals(2, exercises.size)
         assertEquals("Bench", exercises[0].name)
         assertEquals("weights", exercises[0].type)
+        assertEquals(0, exercises[0].sortOrder)
         assertTrue(exercises[0].isActive)
         assertEquals("", exercises[0].notes)
         assertEquals("Easy run", exercises[1].name)
         assertEquals("run", exercises[1].type)
+        assertEquals(1, exercises[1].sortOrder)
         assertTrue(exercises[1].isActive)
         assertEquals("", exercises[1].notes)
         exercises.forEach { assertUuidV7(it.id) }
         assertUuidV7(exercises[0].workoutId)
     }
+
+    @Test
+    fun updateWorkout_renamesReordersAndReplacesExercises() = runBlocking {
+        val firstId = UuidV7.generateString()
+        val secondId = UuidV7.generateString()
+        val workoutId = repository.saveWorkout(
+            name = "Push day",
+            exercises = listOf(
+                DraftExercise(id = firstId, type = ExerciseType.Weights, name = "Bench"),
+                DraftExercise(id = secondId, type = ExerciseType.Run, name = "Easy run")
+            )
+        )
+        val thirdId = UuidV7.generateString()
+
+        repository.updateWorkout(
+            id = workoutId,
+            name = " Pull day ",
+            exercises = listOf(
+                DraftExercise(id = secondId, type = ExerciseType.Run, name = "Intervals"),
+                DraftExercise(id = thirdId, type = ExerciseType.Weights, name = "Row")
+            )
+        )
+
+        val workout = database.workoutDao().getWorkout(workoutId)!!
+        assertEquals("Pull day", workout.name)
+        val exercises = database.workoutDao().getExercisesForWorkout(workoutId)
+        assertEquals(2, exercises.size)
+        assertEquals(secondId, exercises[0].id)
+        assertEquals("Intervals", exercises[0].name)
+        assertEquals(0, exercises[0].sortOrder)
+        assertEquals(thirdId, exercises[1].id)
+        assertEquals("Row", exercises[1].name)
+        assertEquals(1, exercises[1].sortOrder)
+    }
 }
 
 private fun assertUuidV7(value: String) {

+ 12 - 2
app/src/main/java/com/example/torus/data/TorusDatabase.kt

@@ -4,22 +4,32 @@ import android.content.Context
 import androidx.room.Database
 import androidx.room.Room
 import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
 
 @Database(
     entities = [WorkoutEntity::class, WorkoutExerciseEntity::class],
-    version = 2,
+    version = 3,
     exportSchema = false
 )
 abstract class TorusDatabase : RoomDatabase() {
     abstract fun workoutDao(): WorkoutDao
 
     companion object {
+        private val MIGRATION_2_3 = object : Migration(2, 3) {
+            override fun migrate(db: SupportSQLiteDatabase) {
+                db.execSQL(
+                    "ALTER TABLE workout_exercise ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
+                )
+            }
+        }
+
         fun create(context: Context): TorusDatabase {
             return Room.databaseBuilder(
                 context.applicationContext,
                 TorusDatabase::class.java,
                 "torus.db"
-            ).fallbackToDestructiveMigration(dropAllTables = true).build()
+            ).addMigrations(MIGRATION_2_3).build()
         }
     }
 }

+ 7 - 1
app/src/main/java/com/example/torus/data/WorkoutDao.kt

@@ -13,12 +13,18 @@ interface WorkoutDao {
     @Insert
     suspend fun insertExercises(exercises: List<WorkoutExerciseEntity>)
 
+    @Query("UPDATE workout SET name = :name WHERE id = :id")
+    suspend fun updateWorkoutName(id: String, name: String)
+
+    @Query("DELETE FROM workout_exercise WHERE workout_id = :workoutId")
+    suspend fun deleteExercisesForWorkout(workoutId: String)
+
     @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?
 
-    @Query("SELECT * FROM workout_exercise WHERE workout_id = :workoutId ORDER BY id ASC")
+    @Query("SELECT * FROM workout_exercise WHERE workout_id = :workoutId ORDER BY sort_order ASC")
     suspend fun getExercisesForWorkout(workoutId: String): List<WorkoutExerciseEntity>
 }

+ 1 - 0
app/src/main/java/com/example/torus/data/WorkoutExerciseEntity.kt

@@ -23,6 +23,7 @@ data class WorkoutExerciseEntity(
     @ColumnInfo(name = "workout_id") val workoutId: String,
     val name: String,
     val type: String,
+    @ColumnInfo(name = "sort_order") val sortOrder: Int,
     @ColumnInfo(name = "is_active") val isActive: Boolean,
     val notes: String,
 )

+ 29 - 1
app/src/main/java/com/example/torus/data/WorkoutRepository.kt

@@ -28,6 +28,7 @@ class WorkoutRepository(private val database: TorusDatabase) {
                 id = exercise.id,
                 name = exercise.name,
                 type = ExerciseType.fromStorage(exercise.type),
+                sortOrder = exercise.sortOrder,
                 isActive = exercise.isActive,
                 notes = exercise.notes
             )
@@ -50,12 +51,13 @@ class WorkoutRepository(private val database: TorusDatabase) {
             )
             if (exercises.isNotEmpty()) {
                 workoutDao.insertExercises(
-                    exercises.map { exercise ->
+                    exercises.mapIndexed { index, exercise ->
                         WorkoutExerciseEntity(
                             id = exercise.id,
                             workoutId = workoutId,
                             name = exercise.name.trim(),
                             type = exercise.type.storageValue,
+                            sortOrder = index,
                             isActive = exercise.isActive,
                             notes = ""
                         )
@@ -65,4 +67,30 @@ class WorkoutRepository(private val database: TorusDatabase) {
             workoutId
         }
     }
+
+    suspend fun updateWorkout(
+        id: String,
+        name: String,
+        exercises: List<DraftExercise>
+    ) {
+        database.withTransaction {
+            workoutDao.updateWorkoutName(id, name.trim())
+            workoutDao.deleteExercisesForWorkout(id)
+            if (exercises.isNotEmpty()) {
+                workoutDao.insertExercises(
+                    exercises.mapIndexed { index, exercise ->
+                        WorkoutExerciseEntity(
+                            id = exercise.id,
+                            workoutId = id,
+                            name = exercise.name.trim(),
+                            type = exercise.type.storageValue,
+                            sortOrder = index,
+                            isActive = exercise.isActive,
+                            notes = exercise.notes
+                        )
+                    }
+                )
+            }
+        }
+    }
 }

+ 13 - 1
app/src/main/java/com/example/torus/model/Workout.kt

@@ -1,5 +1,6 @@
 package com.example.torus.model
 
+
 data class WorkoutSummary(
     val id: String,
     val name: String,
@@ -15,6 +16,17 @@ data class SavedExercise(
     val id: String,
     val name: String,
     val type: ExerciseType,
+    val sortOrder: Int,
     val isActive: Boolean,
     val notes: String,
-)
+) {
+    fun toDraft(): DraftExercise {
+        return DraftExercise(
+            id = id,
+            type = type,
+            name = name,
+            notes = notes,
+            isActive = isActive
+        )
+    }
+}

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

@@ -1,11 +1,7 @@
 package com.example.torus.ui
 
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-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
@@ -15,11 +11,7 @@ import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
 import androidx.compose.material3.Text
-import androidx.compose.material3.TextFieldColors
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
@@ -30,7 +22,6 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.withFrameNanos
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
@@ -137,137 +128,6 @@ fun CreateWorkoutScreen(
     }
 }
 
-private fun updateExercise(
-    exercises: MutableList<DraftExercise>,
-    id: String,
-    transform: (DraftExercise) -> DraftExercise
-) {
-    val index = exercises.indexOfFirst { it.id == id }
-    if (index >= 0) {
-        exercises[index] = transform(exercises[index])
-    }
-}
-
-@Composable
-private fun NamedExerciseEditor(
-    number: Int,
-    typeLabel: String,
-    name: String,
-    onNameChange: (String) -> Unit
-) {
-    val shape = RoundedCornerShape(14.dp)
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .border(1.dp, MaterialTheme.colorScheme.outline, shape)
-            .padding(16.dp),
-        verticalArrangement = Arrangement.spacedBy(12.dp)
-    ) {
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = Arrangement.SpaceBetween,
-            verticalAlignment = Alignment.CenterVertically
-        ) {
-            Text(
-                text = typeLabel,
-                color = MaterialTheme.colorScheme.onSurfaceVariant,
-                style = MaterialTheme.typography.labelLarge
-            )
-            Text(
-                text = number.toString(),
-                color = MaterialTheme.colorScheme.onSurfaceVariant,
-                style = MaterialTheme.typography.labelSmall
-            )
-        }
-        TorusTextField(
-            value = name,
-            onValueChange = onNameChange,
-            label = stringResource(R.string.exercise_name)
-        )
-    }
-}
-
-@Composable
-private fun ExerciseTypePicker(
-    onWeightsClick: () -> Unit,
-    onRunClick: () -> Unit = {}
-) {
-    val shape = RoundedCornerShape(14.dp)
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .background(MaterialTheme.colorScheme.surfaceVariant, shape)
-            .border(1.dp, MaterialTheme.colorScheme.primary, shape)
-            .padding(16.dp),
-        verticalArrangement = Arrangement.spacedBy(12.dp)
-    ) {
-        Text(
-            text = stringResource(R.string.choose_exercise_type),
-            color = MaterialTheme.colorScheme.onSurfaceVariant,
-            style = MaterialTheme.typography.titleSmall
-        )
-        ExerciseTypeOption(
-            label = stringResource(R.string.exercise_type_weights),
-            onClick = onWeightsClick
-        )
-        ExerciseTypeOption(
-            label = stringResource(R.string.exercise_type_run),
-            onClick = onRunClick
-        )
-    }
-}
-
-@Composable
-private fun ExerciseTypeOption(
-    label: String,
-    onClick: () -> Unit = {}
-) {
-    OutlinedButton(
-        onClick = onClick,
-        modifier = Modifier.fillMaxWidth(),
-        shape = RoundedCornerShape(50),
-        colors = ButtonDefaults.outlinedButtonColors(
-            contentColor = MaterialTheme.colorScheme.onBackground
-        ),
-        border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
-    ) {
-        Text(text = label)
-    }
-}
-
-@Composable
-private fun TorusTextField(
-    value: String,
-    onValueChange: (String) -> Unit,
-    label: String,
-    modifier: Modifier = Modifier
-) {
-    OutlinedTextField(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier.fillMaxWidth(),
-        label = { Text(text = label) },
-        singleLine = true,
-        shape = RoundedCornerShape(14.dp),
-        colors = torusTextFieldColors()
-    )
-}
-
-@Composable
-private fun torusTextFieldColors(): TextFieldColors {
-    return OutlinedTextFieldDefaults.colors(
-        focusedTextColor = MaterialTheme.colorScheme.onBackground,
-        unfocusedTextColor = MaterialTheme.colorScheme.onBackground,
-        focusedBorderColor = MaterialTheme.colorScheme.primary,
-        unfocusedBorderColor = MaterialTheme.colorScheme.outline,
-        focusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
-        unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
-        cursorColor = MaterialTheme.colorScheme.primary,
-        focusedContainerColor = MaterialTheme.colorScheme.surface,
-        unfocusedContainerColor = MaterialTheme.colorScheme.surface
-    )
-}
-
 @Preview(showBackground = true, backgroundColor = 0xFF0C0F14)
 @Composable
 private fun CreateWorkoutScreenPreview() {

+ 156 - 0
app/src/main/java/com/example/torus/ui/EditWorkoutScreen.kt

@@ -0,0 +1,156 @@
+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.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.example.torus.R
+import com.example.torus.data.WorkoutRepository
+import com.example.torus.id.UuidV7
+import com.example.torus.model.DraftExercise
+import com.example.torus.model.ExerciseType
+import com.example.torus.ui.theme.Sand
+import kotlinx.coroutines.launch
+
+@Composable
+fun EditWorkoutScreen(
+    workoutId: String,
+    workoutRepository: WorkoutRepository,
+    modifier: Modifier = Modifier,
+    onSaveWorkout: (workoutName: String, exercises: List<DraftExercise>) -> Unit = { _, _ -> }
+) {
+    var workoutName by remember { mutableStateOf("") }
+    var showExerciseTypes by remember { mutableStateOf(false) }
+    var isSaving by remember { mutableStateOf(false) }
+    var loaded by remember { mutableStateOf(false) }
+    val exercises = remember { mutableStateListOf<DraftExercise>() }
+    val scrollState = rememberScrollState()
+    val coroutineScope = rememberCoroutineScope()
+
+    LaunchedEffect(workoutId) {
+        val detail = workoutRepository.getWorkoutDetail(workoutId) ?: return@LaunchedEffect
+        workoutName = detail.name
+        exercises.clear()
+        exercises.addAll(detail.exercises.map { it.toDraft() })
+        loaded = true
+    }
+
+    fun addExercise(type: ExerciseType) {
+        exercises += DraftExercise(id = UuidV7.generateString(), type = type)
+        showExerciseTypes = false
+    }
+
+    if (!loaded) {
+        return
+    }
+
+    Column(modifier = modifier.fillMaxSize()) {
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .fillMaxWidth()
+                .verticalScroll(scrollState)
+                .padding(horizontal = 16.dp, vertical = 8.dp),
+            verticalArrangement = Arrangement.spacedBy(16.dp)
+        ) {
+            Text(
+                text = stringResource(R.string.edit_workout),
+                color = Sand,
+                style = MaterialTheme.typography.headlineMedium
+            )
+            TorusTextField(
+                value = workoutName,
+                onValueChange = { workoutName = it },
+                label = stringResource(R.string.workout_name)
+            )
+            exercises.forEachIndexed { index, exercise ->
+                key(exercise.id) {
+                    NamedExerciseEditor(
+                        number = index + 1,
+                        typeLabel = stringResource(exercise.type.labelRes),
+                        name = exercise.name,
+                        onNameChange = { name ->
+                            updateExercise(exercises, exercise.id) { it.copy(name = name) }
+                        },
+                        onMoveUp = if (index > 0) {
+                            { moveExercise(exercises, exercise.id, -1) }
+                        } else {
+                            null
+                        },
+                        onMoveDown = if (index < exercises.lastIndex) {
+                            { moveExercise(exercises, exercise.id, 1) }
+                        } else {
+                            null
+                        },
+                        onRemove = {
+                            exercises.removeAll { it.id == exercise.id }
+                        }
+                    )
+                }
+            }
+            Button(
+                onClick = {
+                    showExerciseTypes = true
+                    coroutineScope.launch {
+                        withFrameNanos { }
+                        withFrameNanos { }
+                        scrollState.animateScrollTo(scrollState.maxValue)
+                    }
+                },
+                shape = RoundedCornerShape(50),
+                colors = ButtonDefaults.buttonColors(
+                    containerColor = MaterialTheme.colorScheme.primary,
+                    contentColor = MaterialTheme.colorScheme.onPrimary
+                )
+            ) {
+                Text(text = stringResource(R.string.add_exercise))
+            }
+            if (showExerciseTypes) {
+                ExerciseTypePicker(
+                    onWeightsClick = { addExercise(ExerciseType.Weights) },
+                    onRunClick = { addExercise(ExerciseType.Run) }
+                )
+            }
+        }
+        Button(
+            onClick = {
+                if (!isSaving) {
+                    isSaving = true
+                    onSaveWorkout(workoutName, exercises.toList())
+                }
+            },
+            enabled = !isSaving,
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 12.dp),
+            shape = RoundedCornerShape(50),
+            colors = ButtonDefaults.buttonColors(
+                containerColor = MaterialTheme.colorScheme.primary,
+                contentColor = MaterialTheme.colorScheme.onPrimary
+            )
+        ) {
+            Text(text = stringResource(R.string.save_workout))
+        }
+    }
+}

+ 30 - 6
app/src/main/java/com/example/torus/ui/TorusApp.kt

@@ -21,9 +21,15 @@ private object Routes {
 
     fun workout(id: String) = "workout/$id"
 
+    fun edit(id: String) = "edit/$id"
+
     fun isWorkout(route: String) = route.startsWith("workout/")
 
+    fun isEdit(route: String) = route.startsWith("edit/")
+
     fun workoutId(route: String) = route.removePrefix("workout/")
+
+    fun editId(route: String) = route.removePrefix("edit/")
 }
 
 @Composable
@@ -72,12 +78,30 @@ fun TorusApp(workoutRepository: WorkoutRepository) {
                     }
                 }
             )
-            Routes.isWorkout(currentRoute) -> WorkoutDetailScreen(
-                workoutId = Routes.workoutId(currentRoute),
-                workoutRepository = workoutRepository,
-                modifier = contentModifier,
-                onHome = { goHome() }
-            )
+            Routes.isWorkout(currentRoute) -> {
+                val workoutId = Routes.workoutId(currentRoute)
+                WorkoutDetailScreen(
+                    workoutId = workoutId,
+                    workoutRepository = workoutRepository,
+                    modifier = contentModifier,
+                    onEditWorkout = { navigateTo(Routes.edit(workoutId)) },
+                    onHome = { goHome() }
+                )
+            }
+            Routes.isEdit(currentRoute) -> {
+                val workoutId = Routes.editId(currentRoute)
+                EditWorkoutScreen(
+                    workoutId = workoutId,
+                    workoutRepository = workoutRepository,
+                    modifier = contentModifier,
+                    onSaveWorkout = { name, exercises ->
+                        coroutineScope.launch {
+                            workoutRepository.updateWorkout(workoutId, name, exercises)
+                            popBack()
+                        }
+                    }
+                )
+            }
         }
     }
 }

+ 197 - 0
app/src/main/java/com/example/torus/ui/WorkoutEditorComponents.kt

@@ -0,0 +1,197 @@
+package com.example.torus.ui
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextFieldColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.example.torus.R
+import com.example.torus.model.DraftExercise
+
+fun updateExercise(
+    exercises: MutableList<DraftExercise>,
+    id: String,
+    transform: (DraftExercise) -> DraftExercise
+) {
+    val index = exercises.indexOfFirst { it.id == id }
+    if (index >= 0) {
+        exercises[index] = transform(exercises[index])
+    }
+}
+
+fun moveExercise(exercises: MutableList<DraftExercise>, id: String, offset: Int) {
+    val index = exercises.indexOfFirst { it.id == id }
+    val target = index + offset
+    if (index < 0 || target !in exercises.indices) {
+        return
+    }
+    val exercise = exercises.removeAt(index)
+    exercises.add(target, exercise)
+}
+
+@Composable
+fun NamedExerciseEditor(
+    number: Int,
+    typeLabel: String,
+    name: String,
+    onNameChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    onMoveUp: (() -> Unit)? = null,
+    onMoveDown: (() -> Unit)? = null,
+    onRemove: (() -> Unit)? = null
+) {
+    val shape = RoundedCornerShape(14.dp)
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .border(1.dp, MaterialTheme.colorScheme.outline, shape)
+            .padding(16.dp),
+        verticalArrangement = Arrangement.spacedBy(12.dp)
+    ) {
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.SpaceBetween,
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(
+                text = typeLabel,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.labelLarge
+            )
+            Text(
+                text = number.toString(),
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.labelSmall
+            )
+        }
+        TorusTextField(
+            value = name,
+            onValueChange = onNameChange,
+            label = stringResource(R.string.exercise_name)
+        )
+        if (onMoveUp != null || onMoveDown != null || onRemove != null) {
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.spacedBy(8.dp),
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                if (onMoveUp != null) {
+                    TextButton(onClick = onMoveUp) {
+                        Text(text = stringResource(R.string.move_up))
+                    }
+                }
+                if (onMoveDown != null) {
+                    TextButton(onClick = onMoveDown) {
+                        Text(text = stringResource(R.string.move_down))
+                    }
+                }
+                if (onRemove != null) {
+                    TextButton(onClick = onRemove) {
+                        Text(
+                            text = stringResource(R.string.remove_exercise),
+                            color = MaterialTheme.colorScheme.error
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun ExerciseTypePicker(
+    onWeightsClick: () -> Unit,
+    onRunClick: () -> Unit = {}
+) {
+    val shape = RoundedCornerShape(14.dp)
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(MaterialTheme.colorScheme.surfaceVariant, shape)
+            .border(1.dp, MaterialTheme.colorScheme.primary, shape)
+            .padding(16.dp),
+        verticalArrangement = Arrangement.spacedBy(12.dp)
+    ) {
+        Text(
+            text = stringResource(R.string.choose_exercise_type),
+            color = MaterialTheme.colorScheme.onSurfaceVariant,
+            style = MaterialTheme.typography.titleSmall
+        )
+        ExerciseTypeOption(
+            label = stringResource(R.string.exercise_type_weights),
+            onClick = onWeightsClick
+        )
+        ExerciseTypeOption(
+            label = stringResource(R.string.exercise_type_run),
+            onClick = onRunClick
+        )
+    }
+}
+
+@Composable
+private fun ExerciseTypeOption(
+    label: String,
+    onClick: () -> Unit = {}
+) {
+    OutlinedButton(
+        onClick = onClick,
+        modifier = Modifier.fillMaxWidth(),
+        shape = RoundedCornerShape(50),
+        colors = ButtonDefaults.outlinedButtonColors(
+            contentColor = MaterialTheme.colorScheme.onBackground
+        ),
+        border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
+    ) {
+        Text(text = label)
+    }
+}
+
+@Composable
+fun TorusTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    label: String,
+    modifier: Modifier = Modifier
+) {
+    OutlinedTextField(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier.fillMaxWidth(),
+        label = { Text(text = label) },
+        singleLine = true,
+        shape = RoundedCornerShape(14.dp),
+        colors = torusTextFieldColors()
+    )
+}
+
+@Composable
+private fun torusTextFieldColors(): TextFieldColors {
+    return OutlinedTextFieldDefaults.colors(
+        focusedTextColor = MaterialTheme.colorScheme.onBackground,
+        unfocusedTextColor = MaterialTheme.colorScheme.onBackground,
+        focusedBorderColor = MaterialTheme.colorScheme.primary,
+        unfocusedBorderColor = MaterialTheme.colorScheme.outline,
+        focusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        cursorColor = MaterialTheme.colorScheme.primary,
+        focusedContainerColor = MaterialTheme.colorScheme.surface,
+        unfocusedContainerColor = MaterialTheme.colorScheme.surface
+    )
+}

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

@@ -13,4 +13,7 @@
     <string name="start_workout">Start Workout</string>
     <string name="edit_workout">Edit Workout</string>
     <string name="home">Home</string>
+    <string name="move_up">Up</string>
+    <string name="move_down">Down</string>
+    <string name="remove_exercise">Remove</string>
 </resources>