From ec8a5fa573a3abf5bc83dcf6d7882b2201eea71d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 17 Sep 2026 09:19:50 -0400 Subject: [PATCH] fix: don't offer to re-share an already-published workout My Fitness builds its log from both Health Connect and the user's own kind 1301 events, but every row in Recent workouts carried a "Share this workout" button. On a relay-sourced entry that opened the composer prefilled and one tap from publishing a second kind 1301 for the same effort. WorkoutOrigin already recorded where each workout came from and nothing read it; the button is now gated on it. Also tidies the dashboard's copy and its empty cells: - This week showed "0 kcal" (and would show "0.0 mi" for a strength-only week) where the weekly-average card already omits metrics the window has none of. Distance and calories now appear only when this week or last week has one, so a drop to zero still renders its trend. - The footer note explained the four-week window as a Health Connect limit and claimed nothing leaves the phone. Neither holds once the log is fed by published events, which by definition already left. - Drops my_fitness_unavailable, unused since the log stopped depending on Health Connect, so it isn't sent out for translation. Co-Authored-By: Claude Opus 5 (1M context) --- .../workouts/fitness/MyFitnessScreen.kt | 100 +++++++++++------- .../composeResources/values/strings.xml | 3 +- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessScreen.kt index 125770000a..bf1886b371 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessScreen.kt @@ -56,6 +56,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.fitness.DetectedWorkout +import com.vitorpamplona.amethyst.commons.fitness.WorkoutOrigin import com.vitorpamplona.amethyst.commons.fitness.WorkoutStats import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -284,34 +285,50 @@ private fun ThisWeekCard( miles: Boolean, ) { SectionCard(stringRes(Res.string.my_fitness_this_week)) { - StatGrid( - listOf( - StatCell( - stringRes(Res.string.my_fitness_workouts), - report.thisWeek.workoutCount.toString(), - null, - WorkoutStats.percentChange(report.thisWeek.workoutCount.toDouble(), report.previousWeek.workoutCount.toDouble()), - ), - StatCell( - stringRes(Res.string.my_fitness_time), - formatDuration(report.thisWeek.durationSeconds), - null, - WorkoutStats.percentChange(report.thisWeek.durationSeconds.toDouble(), report.previousWeek.durationSeconds.toDouble()), - ), - StatCell( - stringRes(Res.string.my_fitness_distance), - formatDistanceValue(report.thisWeek.distanceMeters, miles), - distanceUnit(miles), - WorkoutStats.percentChange(report.thisWeek.distanceMeters, report.previousWeek.distanceMeters), - ), - StatCell( - stringRes(Res.string.my_fitness_calories), - report.thisWeek.calories.toString(), - stringRes(Res.string.my_fitness_unit_kcal), - WorkoutStats.percentChange(report.thisWeek.calories.toDouble(), report.previousWeek.calories.toDouble()), - ), - ), - ) + // Workouts and time are always worth a cell. Distance and calories are not: a week of + // strength work has neither, and a column of zeroes reads as missing data rather than + // as "this activity doesn't have that metric". Last week still counts, so a drop to + // zero keeps its cell and shows the fall. + val cells = + buildList { + add( + StatCell( + stringRes(Res.string.my_fitness_workouts), + report.thisWeek.workoutCount.toString(), + null, + WorkoutStats.percentChange(report.thisWeek.workoutCount.toDouble(), report.previousWeek.workoutCount.toDouble()), + ), + ) + add( + StatCell( + stringRes(Res.string.my_fitness_time), + formatDuration(report.thisWeek.durationSeconds), + null, + WorkoutStats.percentChange(report.thisWeek.durationSeconds.toDouble(), report.previousWeek.durationSeconds.toDouble()), + ), + ) + if (report.thisWeek.distanceMeters > 0 || report.previousWeek.distanceMeters > 0) { + add( + StatCell( + stringRes(Res.string.my_fitness_distance), + formatDistanceValue(report.thisWeek.distanceMeters, miles), + distanceUnit(miles), + WorkoutStats.percentChange(report.thisWeek.distanceMeters, report.previousWeek.distanceMeters), + ), + ) + } + if (report.thisWeek.calories > 0 || report.previousWeek.calories > 0) { + add( + StatCell( + stringRes(Res.string.my_fitness_calories), + report.thisWeek.calories.toString(), + stringRes(Res.string.my_fitness_unit_kcal), + WorkoutStats.percentChange(report.thisWeek.calories.toDouble(), report.previousWeek.calories.toDouble()), + ), + ) + } + } + StatGrid(cells) } } @@ -333,14 +350,16 @@ private fun WindowTotalsCard( val cells = buildList { add(StatCell(stringRes(Res.string.my_fitness_time), formatDuration(report.weeklyAverage.durationSeconds), null, null)) - add( - StatCell( - stringRes(Res.string.my_fitness_distance), - formatDistanceValue(report.weeklyAverage.distanceMeters, miles), - distanceUnit(miles), - null, - ), - ) + if (report.windowTotals.distanceMeters > 0) { + add( + StatCell( + stringRes(Res.string.my_fitness_distance), + formatDistanceValue(report.weeklyAverage.distanceMeters, miles), + distanceUnit(miles), + null, + ), + ) + } if (report.windowTotals.steps > 0) { add(StatCell(stringRes(Res.string.my_fitness_steps), report.weeklyAverage.steps.toString(), null, null)) } @@ -492,8 +511,13 @@ private fun RecentWorkouts( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) - TextButton(onClick = { onShare(workout, label) }) { - Text(stringRes(Res.string.my_fitness_share), style = MaterialTheme.typography.labelMedium) + // Only what is still unpublished can be shared. A workout that came + // back from the relays is already posted: offering to share it again + // would publish a second kind 1301 for the same effort. + if (workout.origin != WorkoutOrigin.PUBLISHED) { + TextButton(onClick = { onShare(workout, label) }) { + Text(stringRes(Res.string.my_fitness_share), style = MaterialTheme.typography.labelMedium) + } } } Text( diff --git a/commons/src/commonMain/composeResources/values/strings.xml b/commons/src/commonMain/composeResources/values/strings.xml index 7f46225d00..4550aca749 100644 --- a/commons/src/commonMain/composeResources/values/strings.xml +++ b/commons/src/commonMain/composeResources/values/strings.xml @@ -685,7 +685,7 @@ Read the full privacy policy My Fitness Last 4 weeks - Health Connect serves the last 30 days, so Amethyst summarises four whole weeks. Nothing here leaves your phone. + The last four whole weeks of your training: the workouts you have published, plus anything Health Connect records on this phone. Amethyst works the summary out on the device and publishes none of it. This week vs last week Weekly average @@ -708,7 +708,6 @@ Highest heart rate Recent workouts Nothing logged in the last 4 weeks. Workouts you post, and any your watch records, show up here. - Health Connect is not available on this device, so Amethyst has no workouts to summarise. Track your training Connect Health Connect to see your weekly totals, week-over-week trend, best efforts and training streak. Everything stays on your phone. Connect Health Connect to add the workouts your watch records, with heart rate, steps and climb.