diff --git a/src/components/SwipeableRow.tsx b/src/components/SwipeableRow.tsx index 1c8bee9..7efa41b 100644 --- a/src/components/SwipeableRow.tsx +++ b/src/components/SwipeableRow.tsx @@ -19,7 +19,8 @@ export function SwipeableRow({ children, onDelete, deleteLabel = "Delete" }: Swi const onHandlerStateChange = (event: any) => { if (event.oldState === State.ACTIVE) { const { translationX } = event.nativeEvent; - const newOffset = lastOffset.current + translationX; + const clampedTranslation = Math.min(0, translationX); + const newOffset = lastOffset.current + clampedTranslation; if (newOffset < -80) { Animated.spring(translateX, { diff --git a/src/features/active-workout/components/WorkoutSummary.tsx b/src/features/active-workout/components/WorkoutSummary.tsx index abe7725..17e7dcf 100644 --- a/src/features/active-workout/components/WorkoutSummary.tsx +++ b/src/features/active-workout/components/WorkoutSummary.tsx @@ -1,14 +1,23 @@ -import { View, Text, ScrollView, Pressable, type ReactNode } from "react-native"; +import { View, Text, ScrollView, Pressable } from "react-native"; import { useTranslation } from "react-i18next"; import { formatTime, formatPace } from "@utils/formatTime"; import { formatDistance } from "@utils/formatDistance"; import { STROKE_COLORS } from "@constants/strokes"; +function formatPoolPace(seconds: number, poolLength: number): string { + if (poolLength === 0) return "--:--"; + const pace = seconds / poolLength; + const mins = Math.floor(pace / 60); + const secs = Math.floor(pace % 60); + return `${mins}:${String(secs).padStart(2, "0")}`; +} + type Set = { stroke: string; distancePerRep: number; repCount: number; position: number; + poolPace?: number | null; }; type Lap = { @@ -25,14 +34,15 @@ type Props = { date?: string; totalDistance: number; totalDuration: number; + totalLengths?: number | null; calories?: number | null; avgHeartRate?: number | null; + avgPace?: number | null; poolLength: number; sets: Set[]; laps: Lap[]; avgSwolf: number | null; onDone?: () => void; - rightAction?: ReactNode; }; export function WorkoutSummary({ @@ -40,14 +50,15 @@ export function WorkoutSummary({ date, totalDistance, totalDuration, + totalLengths, calories, avgHeartRate, + avgPace, poolLength, sets, laps, avgSwolf, onDone, - rightAction, }: Props) { const { t } = useTranslation("active-workout"); const { t: tc } = useTranslation("common"); @@ -65,7 +76,6 @@ export function WorkoutSummary({ {date || tc("pool", { length: poolLength })} - {rightAction} {/* Stats grid */} @@ -99,8 +109,25 @@ export function WorkoutSummary({ {t("avgHeartRate")} )} + {totalLengths != null && totalLengths > 0 && ( + + {totalLengths} + {t("lengths")} + + )} + {avgPace != null && avgPace > 0 && ( + + + + {formatPace(totalDistance, totalDuration)} + + {t("avgPace")} + + + )} + {/* SWOLF */} {avgSwolf != null && avgSwolf > 0 && ( @@ -129,9 +156,16 @@ export function WorkoutSummary({ {tc(`stroke.${set.stroke.toLowerCase()}`)} - - {set.repCount}脳{set.distancePerRep}m - + + + {set.repCount}脳{set.distancePerRep}m + + {set.poolPace != null && set.poolPace > 0 && ( + + {formatPoolPace(set.poolPace, poolLength)}/pool + + )} + ))} diff --git a/src/features/active-workout/screens/PostSwimScreen.tsx b/src/features/active-workout/screens/PostSwimScreen.tsx index 364abcd..3bd06c9 100644 --- a/src/features/active-workout/screens/PostSwimScreen.tsx +++ b/src/features/active-workout/screens/PostSwimScreen.tsx @@ -56,6 +56,24 @@ export function PostSwimScreen() { ? Math.round(laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / laps.length) : null; + const totalLengths = + workout.totalDistance && workout.poolLength + ? Math.round(workout.totalDistance / workout.poolLength) + : null; + + const avgPace = + workout.totalDistance && totalDuration + ? workout.totalDistance / totalDuration + : null; + + const setsWithPace = (workout.sets ?? []).map((set: any) => { + const setLaps = laps.filter((l: any) => l.setPosition === set.position); + const setDuration = setLaps.reduce((acc: number, l: any) => acc + l.duration, 0); + const setDistance = setLaps.reduce((acc: number, l: any) => acc + l.distance, 0); + const poolPace = setDistance > 0 ? setDuration / setDistance : null; + return { ...set, poolPace }; + }); + setData({ name: workout.name, poolLength: workout.poolLength, @@ -63,9 +81,11 @@ export function PostSwimScreen() { totalDuration, calories: workout.calories, avgHeartRate: workout.avgHeartRate, - sets: workout.sets ?? [], + sets: setsWithPace, laps, avgSwolf, + totalLengths, + avgPace, }); }; @@ -82,8 +102,10 @@ export function PostSwimScreen() { name={data.name} totalDistance={data.totalDistance} totalDuration={data.totalDuration} + totalLengths={data.totalLengths} calories={data.calories} avgHeartRate={data.avgHeartRate} + avgPace={data.avgPace} poolLength={data.poolLength} sets={data.sets} laps={data.laps} diff --git a/src/features/history/screens/SessionDetailScreen.tsx b/src/features/history/screens/SessionDetailScreen.tsx index c525bec..389a643 100644 --- a/src/features/history/screens/SessionDetailScreen.tsx +++ b/src/features/history/screens/SessionDetailScreen.tsx @@ -60,6 +60,18 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail" ]); }; + useEffect(() => { + if (workout) { + navigation.setOptions({ + headerRight: () => ( + + 馃棏 + + ), + }); + } + }, [workout]); + if (!workout) { return ( @@ -75,23 +87,38 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail" ) : null; + const totalLengths = + workout.totalDistance && workout.poolLength + ? Math.round(workout.totalDistance / workout.poolLength) + : null; + + const avgPace = + workout.totalDistance && workout.totalDuration + ? workout.totalDistance / workout.totalDuration + : null; + + const setsWithPace = workout.sets.map((set) => { + const setLaps = workout.laps.filter((l) => l.setPosition === set.position); + const setDuration = setLaps.reduce((acc, l) => acc + l.duration, 0); + const setDistance = setLaps.reduce((acc, l) => acc + l.distance, 0); + const poolPace = setDistance > 0 ? setDuration / setDistance : null; + return { ...set, poolPace }; + }); + return ( - 馃棏 - - } /> ); } diff --git a/src/i18n/de/active-workout.json b/src/i18n/de/active-workout.json index e59abc1..16b2f64 100644 --- a/src/i18n/de/active-workout.json +++ b/src/i18n/de/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Durchschn. Herzfrequenz", "calories": "Kalorien", "pace": "Tempo", + "lengths": "Bahnen", + "avgPace": "脴 Tempo", "setRep": "Set {{set}} 路 Wiederholung {{rep}}", "watchStatus": { "ready": "Watch bereit", diff --git a/src/i18n/dk/active-workout.json b/src/i18n/dk/active-workout.json index b3342fd..27c1504 100644 --- a/src/i18n/dk/active-workout.json +++ b/src/i18n/dk/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Gns. puls", "calories": "Kalorier", "pace": "Tempo", + "lengths": "Baner", + "avgPace": "Snit-tempo", "setRep": "S忙t {{set}} 路 Gentagelse {{rep}}", "watchStatus": { "ready": "Watch klar", diff --git a/src/i18n/en/active-workout.json b/src/i18n/en/active-workout.json index 824085a..5343fcc 100644 --- a/src/i18n/en/active-workout.json +++ b/src/i18n/en/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Avg Heart Rate", "calories": "Calories", "pace": "Pace", + "lengths": "Lengths", + "avgPace": "Avg pace", "setRep": "Set {{set}} 路 Rep {{rep}}", "watchStatus": { "ready": "Watch ready", diff --git a/src/i18n/es/active-workout.json b/src/i18n/es/active-workout.json index 279ebc5..a520b8c 100644 --- a/src/i18n/es/active-workout.json +++ b/src/i18n/es/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Freq. card. media", "calories": "Calor铆as", "pace": "Ritmo", + "lengths": "Tiradas", + "avgPace": "Ritmo medio", "setRep": "Serie {{set}} 路 Repetici贸n {{rep}}", "watchStatus": { "ready": "Watch lista", diff --git a/src/i18n/fr/active-workout.json b/src/i18n/fr/active-workout.json index 4b2539e..ecdbbbc 100644 --- a/src/i18n/fr/active-workout.json +++ b/src/i18n/fr/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Fr茅q. cardiaque moy.", "calories": "Calories", "pace": "Rythme", + "lengths": "Longueurs", + "avgPace": "Rythme moy.", "setRep": "S茅rie {{set}} 路 R茅p茅tition {{rep}}", "watchStatus": { "ready": "Watch pr锚te", diff --git a/src/i18n/it/active-workout.json b/src/i18n/it/active-workout.json index 742f721..25f6ec4 100644 --- a/src/i18n/it/active-workout.json +++ b/src/i18n/it/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Freq. card. media", "calories": "Calorie", "pace": "Ritmo", + "lengths": "Vasche", + "avgPace": "Passo medio", "setRep": "Serie {{set}} 路 Ripetizione {{rep}}", "watchStatus": { "ready": "Watch pronta", diff --git a/src/i18n/nl/active-workout.json b/src/i18n/nl/active-workout.json index c9f985e..ad11d54 100644 --- a/src/i18n/nl/active-workout.json +++ b/src/i18n/nl/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Gem. hartslag", "calories": "Calorie毛n", "pace": "Tempo", + "lengths": "Banen", + "avgPace": "Gem. tempo", "setRep": "Set {{set}} 路 Herhaling {{rep}}", "watchStatus": { "ready": "Watch gereed", diff --git a/src/i18n/pl/active-workout.json b/src/i18n/pl/active-workout.json index dc04fdc..6e28584 100644 --- a/src/i18n/pl/active-workout.json +++ b/src/i18n/pl/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "艢r. t臋tno", "calories": "Kalorie", "pace": "Tempo", + "lengths": "D艂ugo艣ci", + "avgPace": "艢r. tempo", "setRep": "Serie {{set}} 路 Powt贸rzenie {{rep}}", "watchStatus": { "ready": "Watch gotowa", diff --git a/src/i18n/pt/active-workout.json b/src/i18n/pt/active-workout.json index 1ad9af1..68e80f2 100644 --- a/src/i18n/pt/active-workout.json +++ b/src/i18n/pt/active-workout.json @@ -17,6 +17,8 @@ "avgHeartRate": "Freq. card. m茅dia", "calories": "Calorias", "pace": "Ritmo", + "lengths": "Comprimentos", + "avgPace": "Ritmo m茅dio", "setRep": "S茅rie {{set}} 路 Repeti莽茫o {{rep}}", "watchStatus": { "ready": "Watch pronta",