Skip to content
Merged
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
@@ -0,0 +1,19 @@
package com.jdluu.flexinsight.data.mapper

import com.jdluu.flexinsight.data.model.ExerciseTemplate
import com.jdluu.flexinsight.data.model.ExerciseTemplateResponse

/**
* Pure mapper for exercise template API responses to domain models.
* Mapping must stay identical to the previous inline implementations.
*/
object ExerciseMapper {

fun toExerciseTemplate(response: ExerciseTemplateResponse): ExerciseTemplate {
return ExerciseTemplate(
id = response.id,
name = response.title, // Map title to name for internal model
muscleGroup = response.muscleGroup
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.jdluu.flexinsight.data.mapper

import com.jdluu.flexinsight.data.model.Routine
import com.jdluu.flexinsight.data.model.RoutineExercise
import com.jdluu.flexinsight.data.model.RoutineExerciseResponse
import com.jdluu.flexinsight.data.model.RoutineFolder
import com.jdluu.flexinsight.data.model.RoutineFolderResponse
import com.jdluu.flexinsight.data.model.RoutineResponse

/**
* Pure mapper for routine-related API responses to domain models.
* Mapping must stay identical to the previous inline implementations.
*/
object RoutineMapper {

fun toRoutine(
response: RoutineResponse,
exerciseTemplateMapping: Map<String, String> = emptyMap()
): Routine {
val routineExercises = response.exercises?.map { exerciseResponse ->
val exerciseName = exerciseTemplateMapping[exerciseResponse.templateId]
toRoutineExercise(exerciseResponse, exerciseName)
}

return Routine(
id = response.id,
name = response.name,
exerciseCount = response.exerciseCount,
exercises = routineExercises
)
}

fun toRoutineExercise(
response: RoutineExerciseResponse,
exerciseName: String? = null
): RoutineExercise {
return RoutineExercise(
templateId = response.templateId,
name = response.title ?: exerciseName // Prefer API title, fallback to mapping
)
}

fun toRoutineFolder(response: RoutineFolderResponse): RoutineFolder {
return RoutineFolder(
id = response.id,
title = response.title,
index = response.index
)
}
}
101 changes: 101 additions & 0 deletions app/src/main/java/com/jdluu/flexinsight/data/mapper/WorkoutMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.jdluu.flexinsight.data.mapper

import com.jdluu.flexinsight.data.model.Exercise
import com.jdluu.flexinsight.data.model.ExerciseResponse
import com.jdluu.flexinsight.data.model.Set
import com.jdluu.flexinsight.data.model.SetResponse
import com.jdluu.flexinsight.data.model.Workout
import com.jdluu.flexinsight.data.model.WorkoutResponse

/**
* Pure mapper for workout-related API responses to domain models.
* Mapping must stay identical to the previous inline implementations.
*/
object WorkoutMapper {

fun toWorkout(response: WorkoutResponse): Workout {
val startTimestamp = parseTimestamp(response.startTime)
val endTimestamp = response.endTime?.let { parseTimestamp(it) }

return Workout(
id = response.id,
name = response.title, // API uses "title", we store as "name"
startTime = startTimestamp,
endTime = endTimestamp,
notes = response.description, // API uses "description", we store as "notes"
routineId = response.routineId,
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}

fun toExercise(response: ExerciseResponse, workoutId: String): Exercise {
// Generate ID from workout ID and index, or use a hash if index is null
val exerciseId = if (response.index != null) {
"${workoutId}_exercise_${response.index}"
} else {
"${workoutId}_exercise_${response.title.hashCode()}"
}

return Exercise(
id = exerciseId,
workoutId = workoutId,
exerciseTemplateId = response.exerciseTemplateId,
name = response.title, // API uses "title", we store as "name"
notes = response.notes,
restDuration = response.restSeconds, // API uses "rest_seconds", we store as "restDuration"
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}

fun toSet(response: SetResponse, exerciseId: String): Set {
// Generate ID from exercise ID and index
val setId = "${exerciseId}_set_${response.index}"

return Set(
id = setId,
exerciseId = exerciseId,
number = response.index + 1, // Convert 0-based index to 1-based number
weight = response.weightKg, // API uses "weight_kg", we store as "weight"
reps = response.reps,
rpe = response.rpe,
distance = response.distanceMeters, // API uses "distance_meters", we store as "distance"
duration = response.durationSeconds, // API uses "duration_seconds", we store as "duration"
restDuration = null, // Not provided in API response
notes = response.type, // Store set type as notes for now
isPersonalRecord = response.personalRecord ?: false,
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}

private fun parseTimestamp(isoString: String): Long {
return try {
// Parse ISO 8601 format (e.g., "2025-12-12T18:27:13+00:00" or "2024-01-15T10:30:00Z")
// Check if string has timezone offset (contains "+" or has "-" after the date part)
val hasTimezoneOffset = isoString.contains("+") ||
(isoString.length > 19 && isoString.substring(19).contains("-"))

if (hasTimezoneOffset) {
// Format: "2025-12-12T18:27:13+00:00" - replace timezone with Z
val cleanString = isoString.replace(Regex("[+-]\\d{2}:\\d{2}$"), "Z")
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
dateFormat.parse(cleanString)?.time ?: System.currentTimeMillis()
} else {
// Format: "2024-01-15T10:30:00Z"
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
dateFormat.parse(isoString)?.time ?: System.currentTimeMillis()
}
} catch (e: Exception) {
// Fallback: try parsing with java.time if available (Android API 26+)
try {
java.time.Instant.parse(isoString).toEpochMilli()
} catch (e2: Exception) {
System.currentTimeMillis()
}
}
}
}
32 changes: 2 additions & 30 deletions app/src/main/java/com/jdluu/flexinsight/data/model/Exercise.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,7 @@ data class ExerciseResponse(
val restSeconds: Int?,
@SerializedName("sets")
val sets: List<SetResponse>?
) {
fun toExercise(workoutId: String): Exercise {
// Generate ID from workout ID and index, or use a hash if index is null
val exerciseId = if (index != null) {
"${workoutId}_exercise_$index"
} else {
"${workoutId}_exercise_${title.hashCode()}"
}

return Exercise(
id = exerciseId,
workoutId = workoutId,
exerciseTemplateId = exerciseTemplateId,
name = title, // API uses "title", we store as "name"
notes = notes,
restDuration = restSeconds, // API uses "rest_seconds", we store as "restDuration"
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}
}
)

/**
* Exercise template from Hevy API
Expand All @@ -92,15 +72,7 @@ data class ExerciseTemplateResponse(
val title: String,
@SerializedName("muscle_group")
val muscleGroup: String?
) {
fun toExerciseTemplate(): ExerciseTemplate {
return ExerciseTemplate(
id = id,
name = title, // Map title to name for internal model
muscleGroup = muscleGroup
)
}
}
)

/**
* Paginated response wrapper for exercise templates
Expand Down
25 changes: 2 additions & 23 deletions app/src/main/java/com/jdluu/flexinsight/data/model/Routine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,7 @@ data class RoutineExerciseResponse(
val templateId: String,
@SerializedName("title")
val title: String?
) {
fun toRoutineExercise(exerciseName: String? = null): RoutineExercise {
return RoutineExercise(
templateId = templateId,
name = title ?: exerciseName // Prefer API title, fallback to mapping
)
}
}
)

/**
* Routine - Local representation
Expand All @@ -51,21 +44,7 @@ data class RoutineResponse(
val exerciseCount: Int,
@SerializedName("exercises")
val exercises: List<RoutineExerciseResponse>?
) {
fun toRoutine(exerciseTemplateMapping: Map<String, String> = emptyMap()): Routine {
val routineExercises = exercises?.map { exerciseResponse ->
val exerciseName = exerciseTemplateMapping[exerciseResponse.templateId]
exerciseResponse.toRoutineExercise(exerciseName)
}

return Routine(
id = id,
name = name,
exerciseCount = exerciseCount,
exercises = routineExercises
)
}
}
)

/**
* Paginated response wrapper for routines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,3 @@ data class RoutineFolder(
val title: String,
val index: Int
)

fun RoutineFolderResponse.toRoutineFolder(): RoutineFolder {
return RoutineFolder(
id = id,
title = title,
index = index
)
}
23 changes: 1 addition & 22 deletions app/src/main/java/com/jdluu/flexinsight/data/model/Set.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,5 @@ data class SetResponse(
val customMetric: Double?,
@SerializedName("personal_record")
val personalRecord: Boolean? = false
) {
fun toSet(exerciseId: String): Set {
// Generate ID from exercise ID and index
val setId = "${exerciseId}_set_$index"

return Set(
id = setId,
exerciseId = exerciseId,
number = index + 1, // Convert 0-based index to 1-based number
weight = weightKg, // API uses "weight_kg", we store as "weight"
reps = reps,
rpe = rpe,
distance = distanceMeters, // API uses "distance_meters", we store as "distance"
duration = durationSeconds, // API uses "duration_seconds", we store as "duration"
restDuration = null, // Not provided in API response
notes = type, // Store set type as notes for now
isPersonalRecord = personalRecord ?: false,
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}
}
)

47 changes: 1 addition & 46 deletions app/src/main/java/com/jdluu/flexinsight/data/model/Workout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,52 +47,7 @@ data class WorkoutResponse(
val routineId: String?,
@SerializedName("exercises")
val exercises: List<ExerciseResponse>?
) {
fun toWorkout(): Workout {
val startTimestamp = parseTimestamp(startTime)
val endTimestamp = endTime?.let { parseTimestamp(it) }

return Workout(
id = id,
name = title, // API uses "title", we store as "name"
startTime = startTimestamp,
endTime = endTimestamp,
notes = description, // API uses "description", we store as "notes"
routineId = routineId,
lastSynced = System.currentTimeMillis(),
needsSync = false
)
}

private fun parseTimestamp(isoString: String): Long {
return try {
// Parse ISO 8601 format (e.g., "2025-12-12T18:27:13+00:00" or "2024-01-15T10:30:00Z")
// Check if string has timezone offset (contains "+" or has "-" after the date part)
val hasTimezoneOffset = isoString.contains("+") ||
(isoString.length > 19 && isoString.substring(19).contains("-"))

if (hasTimezoneOffset) {
// Format: "2025-12-12T18:27:13+00:00" - replace timezone with Z
val cleanString = isoString.replace(Regex("[+-]\\d{2}:\\d{2}$"), "Z")
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
dateFormat.parse(cleanString)?.time ?: System.currentTimeMillis()
} else {
// Format: "2024-01-15T10:30:00Z"
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
dateFormat.parse(isoString)?.time ?: System.currentTimeMillis()
}
} catch (e: Exception) {
// Fallback: try parsing with java.time if available (Android API 26+)
try {
java.time.Instant.parse(isoString).toEpochMilli()
} catch (e2: Exception) {
System.currentTimeMillis()
}
}
}
}
)

/**
* Paginated response wrapper for workouts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.jdluu.flexinsight.data.cache.CacheKeys
import com.jdluu.flexinsight.data.cache.CacheManager
import com.jdluu.flexinsight.data.cache.CacheTTL
import com.jdluu.flexinsight.data.local.dao.ExerciseDao
import com.jdluu.flexinsight.data.mapper.ExerciseMapper
import com.jdluu.flexinsight.data.model.Exercise
import com.jdluu.flexinsight.data.model.ExerciseTemplate
import com.jdluu.flexinsight.data.model.ExerciseTemplateResponse
Expand Down Expand Up @@ -165,7 +166,7 @@ class ExerciseRepositoryImpl(
}
is Result.Success -> {
val mapping = templatesResult.data.mapNotNull { templateResponse ->
val template = templateResponse.toExerciseTemplate()
val template = ExerciseMapper.toExerciseTemplate(templateResponse)
template.muscleGroup?.let { template.id to it }
}.toMap()
cacheManager.put(CacheKeys.EXERCISE_TEMPLATES, mapping)
Expand Down
Loading
Loading