Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ data class WorkoutHistoryRow(
val recordCount: Int,
)

/** A completed set other than a warm-up, from the last finished workout its exercise was done in. */
data class LastSessionSet(val exerciseId: String, val startedAt: Long, val weightKg: Double, val reps: Int)

/** The name of one exercise in a workout, for the history list. */
data class WorkoutExerciseName(val workoutId: String, val name: String)

/** A completed set of an exercise, with the workout it was done in, for the exercise's history. */
data class ExerciseHistoryRow(
val workoutId: String,
Expand Down Expand Up @@ -74,6 +80,49 @@ interface WorkoutDao {
)
fun observeHistory(): Flow<List<WorkoutHistoryRow>>

/**
* For every exercise, its completed sets other than warm-ups from the last finished workout that had
* any, for the exercise list's "last top set".
*/
@Query(
"""
SELECT we.exerciseId, w.startedAt, s.weightKg, s.reps FROM workout_sets s
JOIN workout_exercises we ON we.id = s.workoutExerciseId JOIN workouts w ON w.id = we.workoutId
WHERE s.completedAt IS NOT NULL AND s.type != 'WARMUP' AND w.endedAt IS NOT NULL
AND w.startedAt = (
SELECT MAX(w2.startedAt) FROM workout_sets s2
JOIN workout_exercises we2 ON we2.id = s2.workoutExerciseId JOIN workouts w2 ON w2.id = we2.workoutId
WHERE we2.exerciseId = we.exerciseId AND s2.completedAt IS NOT NULL AND s2.type != 'WARMUP'
AND w2.endedAt IS NOT NULL
)
"""
)
fun observeLastSessionSets(): Flow<List<LastSessionSet>>

/**
* The exercises of the last [workoutCount] finished workouts, most recent workout first and each in
* order. An exercise can appear more than once.
*/
@Query(
"""
SELECT we.exerciseId FROM workout_exercises we JOIN workouts w ON w.id = we.workoutId
WHERE w.id IN (SELECT id FROM workouts WHERE endedAt IS NOT NULL ORDER BY startedAt DESC LIMIT :workoutCount)
ORDER BY w.startedAt DESC, we.position
"""
)
fun observeRecentExerciseIds(workoutCount: Int): Flow<List<String>>

/** Exercise names of every finished workout, each workout's in order. */
@Query(
"""
SELECT we.workoutId, e.name FROM workout_exercises we
JOIN workouts w ON w.id = we.workoutId JOIN exercises e ON e.id = we.exerciseId
WHERE w.endedAt IS NOT NULL
ORDER BY we.workoutId, we.position
"""
)
fun observeHistoryExerciseNames(): Flow<List<WorkoutExerciseName>>

/**
* Every completed set of the exercise, the active workout's included: newest workout first, then in
* the order the logger shows them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import io.github.prithvimk.gymtracker.data.db.entity.PersonalRecord
import io.github.prithvimk.gymtracker.data.db.entity.Workout
import io.github.prithvimk.gymtracker.data.db.entity.WorkoutExercise
import io.github.prithvimk.gymtracker.data.db.entity.WorkoutSet
import io.github.prithvimk.gymtracker.domain.LastTopSet
import io.github.prithvimk.gymtracker.domain.SessionComparison
import io.github.prithvimk.gymtracker.domain.SessionExercise
import io.github.prithvimk.gymtracker.domain.TopSet
import io.github.prithvimk.gymtracker.domain.WorkoutSummary
import io.github.prithvimk.gymtracker.domain.bestOf
import io.github.prithvimk.gymtracker.domain.compareSessions
import io.github.prithvimk.gymtracker.domain.model.SetType
import io.github.prithvimk.gymtracker.domain.summarizeWorkout
import java.util.UUID
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map

/** What [WorkoutRepository.endIdleWorkout] did with the active workout. */
sealed interface IdleWorkoutResult {
Expand Down Expand Up @@ -190,6 +194,24 @@ class WorkoutRepository(
/** Finished workouts for the history list, newest first. */
fun observeHistory(): Flow<List<WorkoutHistoryRow>> = workoutDao.observeHistory()

/** Each trained exercise's top set from the last finished workout it was done in, keyed by exercise id. */
fun observeLastTopSets(): Flow<Map<String, LastTopSet>> = workoutDao.observeLastSessionSets().map { rows ->
rows.groupBy { it.exerciseId }.mapValues { (_, sets) ->
// Two workouts can't start at the same moment in practice, but the latest wins if they do.
val startedAt = sets.maxOf { it.startedAt }
val top = bestOf(sets.filter { it.startedAt == startedAt }.map { TopSet(it.weightKg, it.reps) })
LastTopSet(checkNotNull(top), startedAt)
}
}

/** Exercises from the last few finished workouts, most recently done first, each once. */
fun observeRecentExerciseIds(): Flow<List<String>> =
workoutDao.observeRecentExerciseIds(RECENT_WORKOUTS).map { it.distinct() }

/** The exercise names of each finished workout, in order, keyed by workout id. */
fun observeHistoryExerciseNames(): Flow<Map<String, List<String>>> =
workoutDao.observeHistoryExerciseNames().map { rows -> rows.groupBy({ it.workoutId }, { it.name }) }

/** Every completed set of the exercise with its workout, newest workout first. */
fun observeExerciseHistory(exerciseId: String): Flow<List<ExerciseHistoryRow>> =
workoutDao.observeExerciseHistory(exerciseId)
Expand Down Expand Up @@ -270,5 +292,8 @@ class WorkoutRepository(
companion object {
/** How long an active workout can go untouched before [endIdleWorkout] ends it: 3 hours. */
const val IDLE_LIMIT_MILLIS = 3 * 60 * 60 * 1_000L

/** How many finished workouts the add-exercises sheet takes its Recent exercises from. */
const val RECENT_WORKOUTS = 5
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ data class SessionComparison(
* The top set among the completed sets that aren't warm-ups: the heaviest, and of the heaviest the
* one with the most reps. Null if there is no such set.
*/
fun topSet(sets: List<WorkoutSet>): TopSet? = sets
.filter { it.completedAt != null && it.type != SetType.WARMUP }
.maxWithOrNull(compareBy<WorkoutSet> { it.weightKg }.thenBy { it.reps })
?.let { TopSet(it.weightKg, it.reps) }
fun topSet(sets: List<WorkoutSet>): TopSet? = bestOf(
sets.filter { it.completedAt != null && it.type != SetType.WARMUP }.map { TopSet(it.weightKg, it.reps) },
)

/** The heaviest of [sets], and of the heaviest the one with the most reps. */
fun bestOf(sets: List<TopSet>): TopSet? = sets.maxWithOrNull(compareBy<TopSet> { it.weightKg }.thenBy { it.reps })

/** An exercise's top set from the last finished workout it was done in, and when that workout started. */
data class LastTopSet(val topSet: TopSet, val startedAt: Long)

/**
* Compares a session with the previous one. Exercises are matched by library exercise, an exercise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,17 @@ fun GymTrackerRoot() {
WORKOUT_DETAIL_ROUTE,
arguments = listOf(navArgument(WORKOUT_ID) { type = NavType.StringType }),
) {
WorkoutDetailRoute(onBack = { navController.popBackStack() })
WorkoutDetailRoute(
onBack = { navController.popBackStack() },
onExerciseClick = { navController.navigate(historyExerciseRoute(it)) },
)
}
// The same exercise detail as under Exercises, kept under History so that tab stays selected.
composable(
HISTORY_EXERCISE_ROUTE,
arguments = listOf(navArgument(EXERCISE_ID) { type = NavType.StringType }),
) {
ExerciseDetailRoute(onBack = { navController.popBackStack() })
}
composable(TopLevelDestination.EXERCISES.route) {
ExercisesRoute(onExerciseClick = { navController.navigate(exerciseDetailRoute(it.id)) })
Expand All @@ -125,6 +135,10 @@ private const val EXERCISE_DETAIL_ROUTE = "exercises/{$EXERCISE_ID}"

private fun exerciseDetailRoute(id: String): String = "exercises/$id"

private const val HISTORY_EXERCISE_ROUTE = "history/exercise/{$EXERCISE_ID}"

private fun historyExerciseRoute(id: String): String = "history/exercise/$id"

private const val WORKOUT_ID = WorkoutDetailViewModel.WORKOUT_ID_ARG
private const val WORKOUT_DETAIL_ROUTE = "history/{$WORKOUT_ID}"

Expand Down
Loading
Loading