diff --git a/app/(app)/(tabs)/(stats)/edit-history.tsx b/app/(app)/(tabs)/(stats)/edit-history.tsx
index 62c38308..3faa1e75 100644
--- a/app/(app)/(tabs)/(stats)/edit-history.tsx
+++ b/app/(app)/(tabs)/(stats)/edit-history.tsx
@@ -10,6 +10,8 @@ import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery";
import { useEditCompletedWorkoutMutation } from "@/hooks/useEditCompletedWorkoutMutation";
import { ActivityIndicator } from "react-native-paper";
import { useCompletedWorkoutByIdQuery } from "@/hooks/useCompletedWorkoutByIdQuery";
+import { formatFromTotalSeconds, convertToTotalSeconds } from "@/utils/utility";
+import { TimeInput } from "@/components/TimeInput";
import Bugsnag from "@bugsnag/expo";
export default function EditCompletedWorkoutScreen() {
@@ -36,8 +38,11 @@ export default function EditCompletedWorkoutScreen() {
error: workoutError,
} = useCompletedWorkoutByIdQuery(Number(id), weightUnit, distanceUnit);
- const editWorkout = useEditCompletedWorkoutMutation(Number(id), weightUnit, distanceUnit);
-
+ const editWorkout = useEditCompletedWorkoutMutation(
+ Number(id),
+ weightUnit,
+ distanceUnit,
+ );
// Update exercises when workout data is available
useEffect(() => {
if (workoutData) {
@@ -58,10 +63,23 @@ export default function EditCompletedWorkoutScreen() {
}, [exercises]);
const handleSave = () => {
- // Blur each input to ensure all onBlur events are fired
Object.values(weightInputRefs.current).forEach((input) => input?.blur());
- editWorkout.mutate(exercises, {
+ // Merge weightInputs into exercises synchronously — onBlur state updates
+ // are batched by React and won't be applied before mutate runs.
+ const finalExercises = exercises.map((exercise, exerciseIndex) => ({
+ ...exercise,
+ sets: exercise.sets.map((set, setIndex) => {
+ const key = `${exerciseIndex}-${setIndex}`;
+ if (weightInputs[key] === undefined) return set;
+ const raw = weightInputs[key];
+ if (raw == null || raw.trim() === "") return { ...set, weight: null };
+ const parsedWeight = parseFloat(raw);
+ return { ...set, weight: isNaN(parsedWeight) ? null : parsedWeight };
+ }),
+ }));
+
+ editWorkout.mutate(finalExercises, {
onSuccess: () => {
router.back();
},
@@ -90,13 +108,21 @@ export default function EditCompletedWorkoutScreen() {
options={{
headerRight: () => (
-
+ {editWorkout.isPending ? (
+
+ ) : (
+
+ )}
),
}}
@@ -185,22 +211,18 @@ export default function EditCompletedWorkoutScreen() {
) : exercise.exercise_tracking_type === "time" ? (
- Time (Seconds)
- {
+ Time (Min:Sec)
+ {
setExercises((prev) => {
const updated = [...prev];
updated[exerciseIndex].sets[setIndex].time =
- Number(value);
+ convertToTotalSeconds(value);
return updated;
});
}}
+ style={styles.timeInput}
/>
) : exercise.exercise_tracking_type === "reps" ? (
@@ -282,4 +304,14 @@ const styles = StyleSheet.create({
fontSize: 18,
textAlign: "right",
},
+ timeInput: {
+ width: 96,
+ padding: 10,
+ borderColor: Colors.dark.subText,
+ borderWidth: 1,
+ borderRadius: 8,
+ color: Colors.dark.text,
+ fontSize: 18,
+ textAlign: "center",
+ },
});
diff --git a/app/(app)/(tabs)/(stats)/history-details.tsx b/app/(app)/(tabs)/(stats)/history-details.tsx
index cde2b505..daae0741 100644
--- a/app/(app)/(tabs)/(stats)/history-details.tsx
+++ b/app/(app)/(tabs)/(stats)/history-details.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useMemo, useCallback } from "react";
+import { useState, useMemo, useCallback, useEffect } from "react";
import { View, StyleSheet, ScrollView, Alert } from "react-native";
import { Image } from "expo-image";
import { ThemedView } from "@/components/ThemedView";
@@ -11,14 +11,14 @@ import {
useLocalSearchParams,
useFocusEffect,
} from "expo-router";
-import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery";
-import { fetchExerciseImagesByIds } from "@/utils/database";
import { byteArrayToBase64 } from "@/utils/utility";
import { parseISO, format } from "date-fns";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useSettingsQuery } from "@/hooks/useSettingsQuery";
-import { useCompletedWorkoutByIdQuery } from "@/hooks/useCompletedWorkoutByIdQuery";
+import { fetchCompletedWorkoutById } from "@/utils/database";
+import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery";
import { useDeleteCompletedWorkoutMutation } from "@/hooks/useDeleteCompletedWorkoutMutation";
+import { formatFromTotalSeconds } from "@/utils/utility";
import Bugsnag from "@bugsnag/expo";
const fallbackImage = require("@/assets/images/placeholder.webp");
@@ -26,6 +26,8 @@ const fallbackImage = require("@/assets/images/placeholder.webp");
export default function HistoryDetailsScreen() {
const { id } = useLocalSearchParams();
const [workout, setWorkout] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
const {
data: settings,
@@ -40,55 +42,50 @@ export default function HistoryDetailsScreen() {
const countUnilateralDouble = settings?.countUnilateralDouble === "true";
const doubleWeightForPaired = settings?.doubleWeightForPaired === "true";
- const deleteMutation = useDeleteCompletedWorkoutMutation();
-
- const {
- data: workoutData,
- isLoading: isWorkoutLoading,
- error: workoutError,
- refetch,
- } = useCompletedWorkoutByIdQuery(Number(id), weightUnit, distanceUnit);
-
useEffect(() => {
- if (workoutData) {
- // Collect unique exercise IDs
- const exerciseIds = workoutData.exercises.map(
- (exercise) => exercise.exercise_id,
- );
+ if (settingsError instanceof Error) {
+ Bugsnag.notify(settingsError);
+ }
+ }, [settingsError]);
- // Fetch images for these exercise IDs and attach them to the exercises
- const fetchImages = async () => {
- try {
- const imagesMap = await fetchExerciseImagesByIds(exerciseIds);
+ const deleteMutation = useDeleteCompletedWorkoutMutation();
- // Attach images to exercises
- const exercisesWithImages = workoutData.exercises.map((exercise) => ({
- ...exercise,
- exercise_image: imagesMap[exercise.exercise_id],
- }));
+ useFocusEffect(
+ useCallback(() => {
+ const numId = Number(Array.isArray(id) ? id[0] : id);
+ if (!numId) {
+ setIsLoading(false);
+ setWorkout(null);
+ return;
+ }
- // Update workout data with exercises including images
- setWorkout({
- ...workoutData,
- exercises: exercisesWithImages,
- });
- } catch (error: any) {
- console.error("Error fetching exercise images:", error);
- Bugsnag.notify(error);
- }
- };
+ let cancelled = false;
+ setIsLoading(true);
+ setError(null);
- fetchImages();
- }
- }, [workoutData]);
+ fetchCompletedWorkoutById(numId, weightUnit, distanceUnit)
+ .then((data) => {
+ if (!cancelled) {
+ setWorkout(data);
+ setError(null);
+ setIsLoading(false);
+ }
+ })
+ .catch((err) => {
+ if (!cancelled) {
+ setError(err instanceof Error ? err : new Error(String(err)));
+ setWorkout(null);
+ setIsLoading(false);
+ Bugsnag.notify(err);
+ }
+ });
- useFocusEffect(
- useCallback(() => {
- refetch();
- }, [refetch]),
+ return () => {
+ cancelled = true;
+ };
+ }, [id, weightUnit, distanceUnit]),
);
- // Calculate total volume
const totalVolume = useMemo(() => {
if (!workout) return 0;
@@ -106,9 +103,15 @@ export default function HistoryDetailsScreen() {
return parseFloat((exerciseAcc + exerciseVolume).toFixed(1));
}, 0);
- }, [workout, bodyWeight, excludeWarmup, countUnilateralDouble, doubleWeightForPaired]);
+ }, [
+ workout,
+ bodyWeight,
+ excludeWarmup,
+ countUnilateralDouble,
+ doubleWeightForPaired,
+ ]);
- if (isWorkoutLoading || !workout || settingsLoading) {
+ if (isLoading || settingsLoading) {
return (
@@ -116,12 +119,16 @@ export default function HistoryDetailsScreen() {
);
}
- if (settingsError || workoutError) {
- const error = settingsError || workoutError;
- if (error instanceof Error) {
- Bugsnag.notify(error);
- return Error: {error.message};
- }
+ if (settingsError instanceof Error) {
+ return Error: {settingsError.message};
+ }
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ if (!workout) {
+ return null;
}
const isoDateString = workout.date_completed.replace(" ", "T");
@@ -219,10 +226,8 @@ export default function HistoryDetailsScreen() {
{/* Exercise List */}
{workout.exercises.map((exercise) => {
- // Check if the image exists
let imageUri = "";
if (exercise.exercise_image) {
- // Convert the image blob to Base64
const base64Image = byteArrayToBase64(exercise.exercise_image);
imageUri = `data:image/webp;base64,${base64Image}`;
}
@@ -253,7 +258,9 @@ export default function HistoryDetailsScreen() {
{exercise.exercise_tracking_type === "time" ? (
- {set.time} Seconds
+ {set.time != null
+ ? formatFromTotalSeconds(set.time)
+ : "—"}
) : exercise.exercise_tracking_type === "reps" ? (
diff --git a/app/(app)/(workout)/index.tsx b/app/(app)/(workout)/index.tsx
index 06e11433..8a5c325d 100644
--- a/app/(app)/(workout)/index.tsx
+++ b/app/(app)/(workout)/index.tsx
@@ -33,6 +33,7 @@ import {
linkCompletedWorkoutToWorkout,
} from "@/utils/database";
import { cancelRestNotifications } from "@/utils/restNotification";
+import { convertTimeStrToSeconds } from "@/utils/utility";
import { useQueryClient } from "@tanstack/react-query";
import { UserExercise, Workout } from "@/store/workoutStore";
@@ -518,7 +519,7 @@ export default function WorkoutOverviewScreen() {
set_number: parseInt(setIndex) + 1,
weight: set.weight ? parseFloat(set.weight) : null,
reps: set.reps ? parseInt(set.reps) : null,
- time: set.time ? parseInt(set.time) : null,
+ time: set.time ? convertTimeStrToSeconds(set.time) : null,
distance:
set.distance !== "" && set.distance != null
? parseFloat(set.distance)
diff --git a/app/(app)/(workout)/workout-session.tsx b/app/(app)/(workout)/workout-session.tsx
index d7058d7e..c38f22fd 100644
--- a/app/(app)/(workout)/workout-session.tsx
+++ b/app/(app)/(workout)/workout-session.tsx
@@ -632,6 +632,17 @@ export default function WorkoutSessionScreen() {
style: "destructive",
onPress: () => {
removeSet(index);
+ const st = useActiveWorkoutStore.getState();
+ const newExerciseIndex = st.currentExerciseIndex;
+ const newSetIndex = st.currentSetIndices[newExerciseIndex] ?? 0;
+ setSlots((prev) => {
+ const u = [...prev] as [SlotData, SlotData, SlotData];
+ u[currentSlotIndex] = {
+ exerciseIndex: newExerciseIndex,
+ setIndex: newSetIndex,
+ };
+ return u;
+ });
},
},
]);
diff --git a/components/SessionSetInfo.tsx b/components/SessionSetInfo.tsx
index 0369377e..1e33f384 100644
--- a/components/SessionSetInfo.tsx
+++ b/components/SessionSetInfo.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useCallback } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import {
IconButton,
@@ -107,7 +107,6 @@ export default function SessionSetInfo({
onToggleSetType,
}: SessionSetInfoProps) {
const [menuVisible, setMenuVisible] = useState(false);
- const [timeInput, setTimeInput] = useState(time);
const [timerModalVisible, setTimerModalVisible] = useState(false);
const weightMinusPress = useContinuousPress(
@@ -135,13 +134,7 @@ export default function SessionSetInfo({
useCallback(() => handleDistanceChange(1), [handleDistanceChange]),
);
- // Update timeInput when time prop changes
- useEffect(() => {
- setTimeInput(time);
- }, [time]);
-
- // Format display value - now always formatted
- const displayValue = formatTimeInput(timeInput);
+ const displayValue = formatTimeInput(time);
const openMenu = () => setMenuVisible(true);
const closeMenu = () => setMenuVisible(false);
@@ -403,6 +396,7 @@ export default function SessionSetInfo({