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) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-09-17 09:21:02 -04:00
co-authored by Claude Opus 5
parent c5526f3575
commit ec8a5fa573
2 changed files with 63 additions and 40 deletions
@@ -56,6 +56,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.fitness.DetectedWorkout 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.fitness.WorkoutStats
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -284,34 +285,50 @@ private fun ThisWeekCard(
miles: Boolean, miles: Boolean,
) { ) {
SectionCard(stringRes(Res.string.my_fitness_this_week)) { SectionCard(stringRes(Res.string.my_fitness_this_week)) {
StatGrid( // Workouts and time are always worth a cell. Distance and calories are not: a week of
listOf( // strength work has neither, and a column of zeroes reads as missing data rather than
StatCell( // as "this activity doesn't have that metric". Last week still counts, so a drop to
stringRes(Res.string.my_fitness_workouts), // zero keeps its cell and shows the fall.
report.thisWeek.workoutCount.toString(), val cells =
null, buildList {
WorkoutStats.percentChange(report.thisWeek.workoutCount.toDouble(), report.previousWeek.workoutCount.toDouble()), add(
), StatCell(
StatCell( stringRes(Res.string.my_fitness_workouts),
stringRes(Res.string.my_fitness_time), report.thisWeek.workoutCount.toString(),
formatDuration(report.thisWeek.durationSeconds), null,
null, WorkoutStats.percentChange(report.thisWeek.workoutCount.toDouble(), report.previousWeek.workoutCount.toDouble()),
WorkoutStats.percentChange(report.thisWeek.durationSeconds.toDouble(), report.previousWeek.durationSeconds.toDouble()), ),
), )
StatCell( add(
stringRes(Res.string.my_fitness_distance), StatCell(
formatDistanceValue(report.thisWeek.distanceMeters, miles), stringRes(Res.string.my_fitness_time),
distanceUnit(miles), formatDuration(report.thisWeek.durationSeconds),
WorkoutStats.percentChange(report.thisWeek.distanceMeters, report.previousWeek.distanceMeters), null,
), WorkoutStats.percentChange(report.thisWeek.durationSeconds.toDouble(), report.previousWeek.durationSeconds.toDouble()),
StatCell( ),
stringRes(Res.string.my_fitness_calories), )
report.thisWeek.calories.toString(), if (report.thisWeek.distanceMeters > 0 || report.previousWeek.distanceMeters > 0) {
stringRes(Res.string.my_fitness_unit_kcal), add(
WorkoutStats.percentChange(report.thisWeek.calories.toDouble(), report.previousWeek.calories.toDouble()), 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 = val cells =
buildList { buildList {
add(StatCell(stringRes(Res.string.my_fitness_time), formatDuration(report.weeklyAverage.durationSeconds), null, null)) add(StatCell(stringRes(Res.string.my_fitness_time), formatDuration(report.weeklyAverage.durationSeconds), null, null))
add( if (report.windowTotals.distanceMeters > 0) {
StatCell( add(
stringRes(Res.string.my_fitness_distance), StatCell(
formatDistanceValue(report.weeklyAverage.distanceMeters, miles), stringRes(Res.string.my_fitness_distance),
distanceUnit(miles), formatDistanceValue(report.weeklyAverage.distanceMeters, miles),
null, distanceUnit(miles),
), null,
) ),
)
}
if (report.windowTotals.steps > 0) { if (report.windowTotals.steps > 0) {
add(StatCell(stringRes(Res.string.my_fitness_steps), report.weeklyAverage.steps.toString(), null, null)) add(StatCell(stringRes(Res.string.my_fitness_steps), report.weeklyAverage.steps.toString(), null, null))
} }
@@ -492,8 +511,13 @@ private fun RecentWorkouts(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton(onClick = { onShare(workout, label) }) { // Only what is still unpublished can be shared. A workout that came
Text(stringRes(Res.string.my_fitness_share), style = MaterialTheme.typography.labelMedium) // 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( Text(
@@ -685,7 +685,7 @@
<string name="health_connect_rationale_privacy_policy">Read the full privacy policy</string> <string name="health_connect_rationale_privacy_policy">Read the full privacy policy</string>
<string name="my_fitness_title">My Fitness</string> <string name="my_fitness_title">My Fitness</string>
<string name="my_fitness_window">Last 4 weeks</string> <string name="my_fitness_window">Last 4 weeks</string>
<string name="my_fitness_window_note">Health Connect serves the last 30 days, so Amethyst summarises four whole weeks. Nothing here leaves your phone.</string> <string name="my_fitness_window_note">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.</string>
<string name="my_fitness_this_week">This week</string> <string name="my_fitness_this_week">This week</string>
<string name="my_fitness_vs_last_week">vs last week</string> <string name="my_fitness_vs_last_week">vs last week</string>
<string name="my_fitness_weekly_average">Weekly average</string> <string name="my_fitness_weekly_average">Weekly average</string>
@@ -708,7 +708,6 @@
<string name="my_fitness_best_highest_heart_rate">Highest heart rate</string> <string name="my_fitness_best_highest_heart_rate">Highest heart rate</string>
<string name="my_fitness_recent">Recent workouts</string> <string name="my_fitness_recent">Recent workouts</string>
<string name="my_fitness_empty">Nothing logged in the last 4 weeks. Workouts you post, and any your watch records, show up here.</string> <string name="my_fitness_empty">Nothing logged in the last 4 weeks. Workouts you post, and any your watch records, show up here.</string>
<string name="my_fitness_unavailable">Health Connect is not available on this device, so Amethyst has no workouts to summarise.</string>
<string name="my_fitness_connect_title">Track your training</string> <string name="my_fitness_connect_title">Track your training</string>
<string name="my_fitness_connect_message">Connect Health Connect to see your weekly totals, week-over-week trend, best efforts and training streak. Everything stays on your phone.</string> <string name="my_fitness_connect_message">Connect Health Connect to see your weekly totals, week-over-week trend, best efforts and training streak. Everything stays on your phone.</string>
<string name="my_fitness_connect_banner">Connect Health Connect to add the workouts your watch records, with heart rate, steps and climb.</string> <string name="my_fitness_connect_banner">Connect Health Connect to add the workouts your watch records, with heart rate, steps and climb.</string>