소스 검색

Add temporary workout saving and ability to resume.

Lee Morgan 2 주 전
부모
커밋
e443c32247

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

@@ -8,12 +8,16 @@ 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.model.InProgressDistanceLog
+import com.example.torus.model.InProgressSession
+import com.example.torus.model.InProgressWeightSet
 import com.example.torus.model.SessionExerciseInput
 import com.example.torus.model.SessionSetInput
 import java.util.UUID
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
@@ -227,6 +231,67 @@ class WorkoutRepositoryTest {
         assertEquals("155", bench.sets[1].weight)
         assertEquals("5", bench.sets[1].reps)
     }
+
+    @Test
+    fun inProgressSession_roundTripsAndClearsOnComplete() = runBlocking {
+        val benchId = UuidV7.generateString()
+        val runId = UuidV7.generateString()
+        val setId = UuidV7.generateString()
+        val workoutId = repository.saveWorkout(
+            name = "Push day",
+            exercises = listOf(
+                DraftExercise(id = benchId, type = ExerciseType.Weights, name = "Bench"),
+                DraftExercise(id = runId, type = ExerciseType.Distance, name = "Easy run")
+            )
+        )
+        val draft = InProgressSession(
+            workoutId = workoutId,
+            startedAt = 99L,
+            currentExerciseIndex = 1,
+            weightSets = listOf(
+                InProgressWeightSet(
+                    id = setId,
+                    workoutExerciseId = benchId,
+                    sortOrder = 0,
+                    weight = "135",
+                    reps = "8"
+                )
+            ),
+            distanceLogs = listOf(
+                InProgressDistanceLog(
+                    workoutExerciseId = runId,
+                    amount = "5",
+                    unit = "Kilometers",
+                    hours = "0",
+                    minutes = "32",
+                    seconds = "10"
+                )
+            )
+        )
+        repository.saveInProgressSession(draft)
+        val loaded = repository.getInProgressSession(workoutId)!!
+        assertEquals(99L, loaded.startedAt)
+        assertEquals(1, loaded.currentExerciseIndex)
+        assertEquals("135", loaded.weightSets.single().weight)
+        assertEquals("8", loaded.weightSets.single().reps)
+        assertEquals("5", loaded.distanceLogs.single().amount)
+        assertEquals("Kilometers", loaded.distanceLogs.single().unit)
+
+        repository.completeSession(
+            workoutId = workoutId,
+            startedAt = 99L,
+            finishedAt = 100L,
+            exercises = listOf(
+                SessionExerciseInput(
+                    workoutExerciseId = benchId,
+                    name = "Bench",
+                    type = ExerciseType.Weights,
+                    sets = listOf(SessionSetInput(weight = "135", reps = "8"))
+                )
+            )
+        )
+        assertNull(repository.getInProgressSession(workoutId))
+    }
 }
 
 private fun assertUuidV7(value: String) {

+ 36 - 0
app/src/main/java/com/example/torus/data/DraftSessionDao.kt

@@ -0,0 +1,36 @@
+package com.example.torus.data
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+@Dao
+interface DraftSessionDao {
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertSession(session: DraftSessionEntity)
+
+    @Insert
+    suspend fun insertWeightSets(sets: List<DraftWeightSetEntity>)
+
+    @Insert
+    suspend fun insertDistanceLogs(logs: List<DraftDistanceEntity>)
+
+    @Query("DELETE FROM draft_session WHERE workout_id = :workoutId")
+    suspend fun deleteSession(workoutId: String)
+
+    @Query("SELECT * FROM draft_session WHERE workout_id = :workoutId")
+    suspend fun getSession(workoutId: String): DraftSessionEntity?
+
+    @Query(
+        """
+        SELECT * FROM draft_weight_set
+        WHERE workout_id = :workoutId
+        ORDER BY workout_exercise_id ASC, sort_order ASC
+        """
+    )
+    suspend fun getWeightSets(workoutId: String): List<DraftWeightSetEntity>
+
+    @Query("SELECT * FROM draft_distance WHERE workout_id = :workoutId")
+    suspend fun getDistanceLogs(workoutId: String): List<DraftDistanceEntity>
+}

+ 68 - 0
app/src/main/java/com/example/torus/data/DraftSessionEntity.kt

@@ -0,0 +1,68 @@
+package com.example.torus.data
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+@Entity(
+    tableName = "draft_session",
+    foreignKeys = [
+        ForeignKey(
+            entity = WorkoutEntity::class,
+            parentColumns = ["id"],
+            childColumns = ["workout_id"],
+            onDelete = ForeignKey.CASCADE
+        )
+    ]
+)
+data class DraftSessionEntity(
+    @PrimaryKey @ColumnInfo(name = "workout_id") val workoutId: String,
+    @ColumnInfo(name = "started_at") val startedAt: Long,
+    @ColumnInfo(name = "current_exercise_index") val currentExerciseIndex: Int,
+)
+
+@Entity(
+    tableName = "draft_weight_set",
+    foreignKeys = [
+        ForeignKey(
+            entity = DraftSessionEntity::class,
+            parentColumns = ["workout_id"],
+            childColumns = ["workout_id"],
+            onDelete = ForeignKey.CASCADE
+        )
+    ],
+    indices = [Index("workout_id")]
+)
+data class DraftWeightSetEntity(
+    @PrimaryKey val id: String,
+    @ColumnInfo(name = "workout_id") val workoutId: String,
+    @ColumnInfo(name = "workout_exercise_id") val workoutExerciseId: String,
+    @ColumnInfo(name = "sort_order") val sortOrder: Int,
+    val weight: String,
+    val reps: String,
+)
+
+@Entity(
+    tableName = "draft_distance",
+    primaryKeys = ["workout_id", "workout_exercise_id"],
+    foreignKeys = [
+        ForeignKey(
+            entity = DraftSessionEntity::class,
+            parentColumns = ["workout_id"],
+            childColumns = ["workout_id"],
+            onDelete = ForeignKey.CASCADE
+        )
+    ],
+    indices = [Index("workout_id")]
+)
+data class DraftDistanceEntity(
+    @ColumnInfo(name = "workout_id") val workoutId: String,
+    @ColumnInfo(name = "workout_exercise_id") val workoutExerciseId: String,
+    val amount: String,
+    val unit: String,
+    val hours: String,
+    val minutes: String,
+    val seconds: String,
+)

+ 57 - 3
app/src/main/java/com/example/torus/data/TorusDatabase.kt

@@ -13,14 +13,18 @@ import androidx.sqlite.db.SupportSQLiteDatabase
         WorkoutExerciseEntity::class,
         SessionEntity::class,
         SessionExerciseEntity::class,
-        SessionSetEntity::class
+        SessionSetEntity::class,
+        DraftSessionEntity::class,
+        DraftWeightSetEntity::class,
+        DraftDistanceEntity::class
     ],
-    version = 7,
+    version = 8,
     exportSchema = false
 )
 abstract class TorusDatabase : RoomDatabase() {
     abstract fun workoutDao(): WorkoutDao
     abstract fun sessionDao(): SessionDao
+    abstract fun draftSessionDao(): DraftSessionDao
 
     companion object {
         private val MIGRATION_2_3 = object : Migration(2, 3) {
@@ -132,6 +136,55 @@ abstract class TorusDatabase : RoomDatabase() {
             }
         }
 
+        private val MIGRATION_7_8 = object : Migration(7, 8) {
+            override fun migrate(db: SupportSQLiteDatabase) {
+                db.execSQL(
+                    """
+                    CREATE TABLE IF NOT EXISTS draft_session (
+                        workout_id TEXT NOT NULL PRIMARY KEY,
+                        started_at INTEGER NOT NULL,
+                        current_exercise_index INTEGER NOT NULL,
+                        FOREIGN KEY(workout_id) REFERENCES workout(id) ON DELETE CASCADE
+                    )
+                    """.trimIndent()
+                )
+                db.execSQL(
+                    """
+                    CREATE TABLE IF NOT EXISTS draft_weight_set (
+                        id TEXT NOT NULL PRIMARY KEY,
+                        workout_id TEXT NOT NULL,
+                        workout_exercise_id TEXT NOT NULL,
+                        sort_order INTEGER NOT NULL,
+                        weight TEXT NOT NULL,
+                        reps TEXT NOT NULL,
+                        FOREIGN KEY(workout_id) REFERENCES draft_session(workout_id) ON DELETE CASCADE
+                    )
+                    """.trimIndent()
+                )
+                db.execSQL(
+                    "CREATE INDEX IF NOT EXISTS index_draft_weight_set_workout_id ON draft_weight_set(workout_id)"
+                )
+                db.execSQL(
+                    """
+                    CREATE TABLE IF NOT EXISTS draft_distance (
+                        workout_id TEXT NOT NULL,
+                        workout_exercise_id TEXT NOT NULL,
+                        amount TEXT NOT NULL,
+                        unit TEXT NOT NULL,
+                        hours TEXT NOT NULL,
+                        minutes TEXT NOT NULL,
+                        seconds TEXT NOT NULL,
+                        PRIMARY KEY(workout_id, workout_exercise_id),
+                        FOREIGN KEY(workout_id) REFERENCES draft_session(workout_id) ON DELETE CASCADE
+                    )
+                    """.trimIndent()
+                )
+                db.execSQL(
+                    "CREATE INDEX IF NOT EXISTS index_draft_distance_workout_id ON draft_distance(workout_id)"
+                )
+            }
+        }
+
         fun create(context: Context): TorusDatabase {
             return Room.databaseBuilder(
                 context.applicationContext,
@@ -142,7 +195,8 @@ abstract class TorusDatabase : RoomDatabase() {
                 MIGRATION_3_4,
                 MIGRATION_4_5,
                 MIGRATION_5_6,
-                MIGRATION_6_7
+                MIGRATION_6_7,
+                MIGRATION_7_8
             ).build()
         }
     }

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

@@ -4,6 +4,9 @@ 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.InProgressDistanceLog
+import com.example.torus.model.InProgressSession
+import com.example.torus.model.InProgressWeightSet
 import com.example.torus.model.LastExerciseLog
 import com.example.torus.model.LastWeightSet
 import com.example.torus.model.SavedExercise
@@ -12,10 +15,15 @@ import com.example.torus.model.WorkoutDetail
 import com.example.torus.model.WorkoutSummary
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 
 class WorkoutRepository(private val database: TorusDatabase) {
     private val workoutDao = database.workoutDao()
     private val sessionDao = database.sessionDao()
+    private val draftSessionDao = database.draftSessionDao()
+    private val draftMutex = Mutex()
+    private val discardedDrafts = mutableSetOf<String>()
 
     fun observeWorkouts(): Flow<List<WorkoutSummary>> {
         return workoutDao.observeWorkouts().map { workouts ->
@@ -60,6 +68,102 @@ class WorkoutRepository(private val database: TorusDatabase) {
         }
     }
 
+    suspend fun allowInProgressSaves(workoutId: String) {
+        draftMutex.withLock {
+            discardedDrafts.remove(workoutId)
+        }
+    }
+
+    suspend fun hasInProgressSession(workoutId: String): Boolean {
+        return draftMutex.withLock {
+            draftSessionDao.getSession(workoutId) != null
+        }
+    }
+
+    suspend fun discardInProgressSession(workoutId: String) {
+        draftMutex.withLock {
+            discardedDrafts += workoutId
+            draftSessionDao.deleteSession(workoutId)
+        }
+    }
+
+    suspend fun getInProgressSession(workoutId: String): InProgressSession? {
+        return draftMutex.withLock {
+            val session = draftSessionDao.getSession(workoutId) ?: return@withLock null
+            InProgressSession(
+                workoutId = session.workoutId,
+                startedAt = session.startedAt,
+                currentExerciseIndex = session.currentExerciseIndex,
+                weightSets = draftSessionDao.getWeightSets(workoutId).map { set ->
+                    InProgressWeightSet(
+                        id = set.id,
+                        workoutExerciseId = set.workoutExerciseId,
+                        sortOrder = set.sortOrder,
+                        weight = set.weight,
+                        reps = set.reps
+                    )
+                },
+                distanceLogs = draftSessionDao.getDistanceLogs(workoutId).map { log ->
+                    InProgressDistanceLog(
+                        workoutExerciseId = log.workoutExerciseId,
+                        amount = log.amount,
+                        unit = log.unit,
+                        hours = log.hours,
+                        minutes = log.minutes,
+                        seconds = log.seconds
+                    )
+                }
+            )
+        }
+    }
+
+    suspend fun saveInProgressSession(session: InProgressSession) {
+        draftMutex.withLock {
+            if (session.workoutId in discardedDrafts) {
+                return
+            }
+            database.withTransaction {
+                draftSessionDao.deleteSession(session.workoutId)
+                draftSessionDao.insertSession(
+                    DraftSessionEntity(
+                        workoutId = session.workoutId,
+                        startedAt = session.startedAt,
+                        currentExerciseIndex = session.currentExerciseIndex
+                    )
+                )
+                if (session.weightSets.isNotEmpty()) {
+                    draftSessionDao.insertWeightSets(
+                        session.weightSets.map { set ->
+                            DraftWeightSetEntity(
+                                id = set.id,
+                                workoutId = session.workoutId,
+                                workoutExerciseId = set.workoutExerciseId,
+                                sortOrder = set.sortOrder,
+                                weight = set.weight,
+                                reps = set.reps
+                            )
+                        }
+                    )
+                }
+                if (session.distanceLogs.isNotEmpty()) {
+                    draftSessionDao.insertDistanceLogs(
+                        session.distanceLogs.map { log ->
+                            DraftDistanceEntity(
+                                workoutId = session.workoutId,
+                                workoutExerciseId = log.workoutExerciseId,
+                                amount = log.amount,
+                                unit = log.unit,
+                                hours = log.hours,
+                                minutes = log.minutes,
+                                seconds = log.seconds
+                            )
+                        }
+                    )
+                }
+            }
+        }
+    }
+
     suspend fun saveWorkout(name: String, exercises: List<DraftExercise>): String {
         return database.withTransaction {
             val workoutId = UuidV7.generateString()
@@ -120,7 +224,10 @@ class WorkoutRepository(private val database: TorusDatabase) {
         finishedAt: Long,
         exercises: List<SessionExerciseInput>
     ): String {
-        return database.withTransaction {
+        return draftMutex.withLock {
+            discardedDrafts += workoutId
+            database.withTransaction {
+            draftSessionDao.deleteSession(workoutId)
             val sessionId = UuidV7.generateString()
             sessionDao.insertSession(
                 SessionEntity(
@@ -160,6 +267,7 @@ class WorkoutRepository(private val database: TorusDatabase) {
                 sessionDao.insertSessionSets(sessionSets)
             }
             sessionId
+            }
         }
     }
 }

+ 26 - 0
app/src/main/java/com/example/torus/model/InProgressSession.kt

@@ -0,0 +1,26 @@
+package com.example.torus.model
+
+data class InProgressSession(
+    val workoutId: String,
+    val startedAt: Long,
+    val currentExerciseIndex: Int,
+    val weightSets: List<InProgressWeightSet>,
+    val distanceLogs: List<InProgressDistanceLog>,
+)
+
+data class InProgressWeightSet(
+    val id: String,
+    val workoutExerciseId: String,
+    val sortOrder: Int,
+    val weight: String,
+    val reps: String,
+)
+
+data class InProgressDistanceLog(
+    val workoutExerciseId: String,
+    val amount: String,
+    val unit: String,
+    val hours: String,
+    val minutes: String,
+    val seconds: String,
+)

+ 125 - 8
app/src/main/java/com/example/torus/ui/StartWorkoutScreen.kt

@@ -33,11 +33,13 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateMapOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -50,6 +52,9 @@ import com.example.torus.data.WorkoutRepository
 import com.example.torus.id.UuidV7
 import com.example.torus.model.DistanceUnit
 import com.example.torus.model.ExerciseType
+import com.example.torus.model.InProgressDistanceLog
+import com.example.torus.model.InProgressSession
+import com.example.torus.model.InProgressWeightSet
 import com.example.torus.model.LastExerciseLog
 import com.example.torus.model.durationHours
 import com.example.torus.model.durationMinutes
@@ -93,8 +98,9 @@ fun StartWorkoutScreen(
     var detail by remember(workoutId) { mutableStateOf<WorkoutDetail?>(null) }
     var exerciseIndex by remember(workoutId) { mutableIntStateOf(0) }
     var isCompleting by remember { mutableStateOf(false) }
+    var loaded by remember(workoutId) { mutableStateOf(false) }
     var lastLogs by remember(workoutId) { mutableStateOf<Map<String, LastExerciseLog>>(emptyMap()) }
-    val startedAt = remember(workoutId) { System.currentTimeMillis() }
+    var startedAt by remember(workoutId) { mutableLongStateOf(0L) }
     val setsByExercise = remember(workoutId) {
         mutableStateMapOf<String, SnapshotStateList<WeightSetDraft>>()
     }
@@ -103,21 +109,46 @@ fun StartWorkoutScreen(
     }
 
     LaunchedEffect(workoutId) {
+        loaded = false
+        workoutRepository.allowInProgressSaves(workoutId)
         val workout = workoutRepository.getWorkoutDetail(workoutId)
+        val draft = workoutRepository.getInProgressSession(workoutId)
         detail = workout
-        exerciseIndex = 0
         setsByExercise.clear()
         distanceByExercise.clear()
-        workout?.exercises?.forEach { exercise ->
-            when (exercise.type) {
-                ExerciseType.Weights -> setsByExercise[exercise.id] = defaultWeightSets()
-                ExerciseType.Distance -> distanceByExercise[exercise.id] = DistanceDraft()
-            }
-        }
+        restoreInProgress(
+            workout = workout,
+            draft = draft,
+            setsByExercise = setsByExercise,
+            distanceByExercise = distanceByExercise,
+            onIndex = { exerciseIndex = it },
+            onStartedAt = { startedAt = it }
+        )
         lastLogs = workoutRepository.getLastExerciseLogs(workoutId)
+        loaded = true
+    }
+
+    LaunchedEffect(workoutId, loaded, isCompleting) {
+        if (!loaded || isCompleting) {
+            return@LaunchedEffect
+        }
+        snapshotFlow {
+            captureInProgress(
+                workoutId = workoutId,
+                startedAt = startedAt,
+                exerciseIndex = exerciseIndex,
+                setsByExercise = setsByExercise,
+                distanceByExercise = distanceByExercise
+            )
+        }.collect { session ->
+            workoutRepository.saveInProgressSession(session)
+        }
     }
 
     val workout = detail ?: return
+    if (!loaded) {
+        return
+    }
     StartWorkoutContent(
         workout = workout,
         exerciseIndex = exerciseIndex,
@@ -717,6 +748,92 @@ private fun SavedExercise.isComplete(
     }
 }
 
+private fun restoreInProgress(
+    workout: WorkoutDetail?,
+    draft: InProgressSession?,
+    setsByExercise: MutableMap<String, SnapshotStateList<WeightSetDraft>>,
+    distanceByExercise: MutableMap<String, DistanceDraft>,
+    onIndex: (Int) -> Unit,
+    onStartedAt: (Long) -> Unit
+) {
+    val exercises = workout?.exercises.orEmpty()
+    val setsByDraftId = draft?.weightSets.orEmpty().groupBy { it.workoutExerciseId }
+    val distanceByDraftId = draft?.distanceLogs.orEmpty().associateBy { it.workoutExerciseId }
+    exercises.forEach { exercise ->
+        when (exercise.type) {
+            ExerciseType.Weights -> {
+                val saved = setsByDraftId[exercise.id].orEmpty().sortedBy { it.sortOrder }
+                setsByExercise[exercise.id] = if (saved.isEmpty()) {
+                    defaultWeightSets()
+                } else {
+                    mutableStateListOf<WeightSetDraft>().also { list ->
+                        saved.forEach { set ->
+                            list += WeightSetDraft(
+                                id = set.id,
+                                weight = set.weight,
+                                reps = set.reps
+                            )
+                        }
+                    }
+                }
+            }
+            ExerciseType.Distance -> {
+                val saved = distanceByDraftId[exercise.id]
+                distanceByExercise[exercise.id] = if (saved == null) {
+                    DistanceDraft()
+                } else {
+                    DistanceDraft(
+                        amount = saved.amount,
+                        unit = DistanceUnit.entries.find { it.name == saved.unit }
+                            ?: DistanceUnit.Kilometers,
+                        hours = saved.hours,
+                        minutes = saved.minutes,
+                        seconds = saved.seconds
+                    )
+                }
+            }
+        }
+    }
+    val lastIndex = (exercises.size - 1).coerceAtLeast(0)
+    onIndex((draft?.currentExerciseIndex ?: 0).coerceIn(0, lastIndex))
+    onStartedAt(draft?.startedAt ?: System.currentTimeMillis())
+}
+
+private fun captureInProgress(
+    workoutId: String,
+    startedAt: Long,
+    exerciseIndex: Int,
+    setsByExercise: Map<String, List<WeightSetDraft>>,
+    distanceByExercise: Map<String, DistanceDraft>
+): InProgressSession {
+    return InProgressSession(
+        workoutId = workoutId,
+        startedAt = startedAt,
+        currentExerciseIndex = exerciseIndex,
+        weightSets = setsByExercise.flatMap { (exerciseId, sets) ->
+            sets.mapIndexed { index, set ->
+                InProgressWeightSet(
+                    id = set.id,
+                    workoutExerciseId = exerciseId,
+                    sortOrder = index,
+                    weight = set.weight,
+                    reps = set.reps
+                )
+            }
+        },
+        distanceLogs = distanceByExercise.map { (exerciseId, draft) ->
+            InProgressDistanceLog(
+                workoutExerciseId = exerciseId,
+                amount = draft.amount,
+                unit = draft.unit.name,
+                hours = draft.hours,
+                minutes = draft.minutes,
+                seconds = draft.seconds
+            )
+        }
+    )
+}
+
 private fun defaultWeightSets(): SnapshotStateList<WeightSetDraft> {
     return mutableStateListOf(
         WeightSetDraft(id = UuidV7.generateString()),

+ 7 - 0
app/src/main/java/com/example/torus/ui/TorusApp.kt

@@ -104,6 +104,13 @@ fun TorusApp(workoutRepository: WorkoutRepository) {
                     workoutRepository = workoutRepository,
                     modifier = contentModifier,
                     onStartWorkout = { navigateTo(Routes.start(workoutId)) },
+                    onResumeWorkout = { navigateTo(Routes.start(workoutId)) },
+                    onStartNewWorkout = {
+                        coroutineScope.launch {
+                            workoutRepository.discardInProgressSession(workoutId)
+                            navigateTo(Routes.start(workoutId))
+                        }
+                    },
                     onEditWorkout = { navigateTo(Routes.edit(workoutId)) },
                     onHome = { goHome() }
                 )

+ 25 - 4
app/src/main/java/com/example/torus/ui/WorkoutDetailScreen.kt

@@ -33,20 +33,27 @@ fun WorkoutDetailScreen(
     workoutRepository: WorkoutRepository,
     modifier: Modifier = Modifier,
     onStartWorkout: () -> Unit = {},
+    onResumeWorkout: () -> Unit = {},
+    onStartNewWorkout: () -> Unit = {},
     onEditWorkout: () -> Unit = {},
     onHome: () -> Unit = {}
 ) {
     var detail by remember(workoutId) { mutableStateOf<WorkoutDetail?>(null) }
+    var hasInProgressSession by remember(workoutId) { mutableStateOf(false) }
 
     LaunchedEffect(workoutId) {
         detail = workoutRepository.getWorkoutDetail(workoutId)
+        hasInProgressSession = workoutRepository.hasInProgressSession(workoutId)
     }
 
     detail?.let { workout ->
         WorkoutDetailContent(
             workout = workout,
+            hasInProgressSession = hasInProgressSession,
             modifier = modifier,
             onStartWorkout = onStartWorkout,
+            onResumeWorkout = onResumeWorkout,
+            onStartNewWorkout = onStartNewWorkout,
             onEditWorkout = onEditWorkout,
             onHome = onHome
         )
@@ -56,8 +63,11 @@ fun WorkoutDetailScreen(
 @Composable
 private fun WorkoutDetailContent(
     workout: WorkoutDetail,
+    hasInProgressSession: Boolean = false,
     modifier: Modifier = Modifier,
     onStartWorkout: () -> Unit = {},
+    onResumeWorkout: () -> Unit = {},
+    onStartNewWorkout: () -> Unit = {},
     onEditWorkout: () -> Unit = {},
     onHome: () -> Unit = {}
 ) {
@@ -72,10 +82,21 @@ private fun WorkoutDetailContent(
             color = Sand,
             style = MaterialTheme.typography.headlineMedium
         )
-        EmberButton(
-            text = stringResource(R.string.start_workout),
-            onClick = onStartWorkout
-        )
+        if (hasInProgressSession) {
+            EmberButton(
+                text = stringResource(R.string.resume_workout),
+                onClick = onResumeWorkout
+            )
+            GhostButton(
+                text = stringResource(R.string.start_new_workout),
+                onClick = onStartNewWorkout
+            )
+        } else {
+            EmberButton(
+                text = stringResource(R.string.start_workout),
+                onClick = onStartWorkout
+            )
+        }
         GhostButton(
             text = stringResource(R.string.edit_workout),
             onClick = onEditWorkout

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

@@ -13,6 +13,8 @@
     <string name="save_workout">Save Workout</string>
     <string name="untitled_workout">Untitled workout</string>
     <string name="start_workout">Start Workout</string>
+    <string name="resume_workout">Resume Workout</string>
+    <string name="start_new_workout">Start New Workout</string>
     <string name="edit_workout">Edit Workout</string>
     <string name="home">Home</string>
     <string name="move_up">Up</string>