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
74 changes: 53 additions & 21 deletions app/(app)/(tabs)/(stats)/edit-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand All @@ -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 };
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}));

editWorkout.mutate(finalExercises, {
onSuccess: () => {
router.back();
},
Expand Down Expand Up @@ -90,13 +108,21 @@ export default function EditCompletedWorkoutScreen() {
options={{
headerRight: () => (
<View style={styles.headerRight}>
<IconButton
icon="content-save-outline"
size={35}
style={{ marginRight: 0 }}
iconColor={Colors.dark.tint}
onPressIn={handleSave}
/>
{editWorkout.isPending ? (
<ActivityIndicator
size={24}
color={Colors.dark.tint}
style={{ marginRight: 12 }}
/>
) : (
<IconButton
icon="content-save-outline"
size={35}
style={{ marginRight: 0 }}
iconColor={Colors.dark.tint}
onPressIn={handleSave}
/>
)}
</View>
),
}}
Expand Down Expand Up @@ -185,22 +211,18 @@ export default function EditCompletedWorkoutScreen() {
</View>
) : exercise.exercise_tracking_type === "time" ? (
<View style={styles.inputContainer}>
<ThemedText style={styles.label}>Time (Seconds)</ThemedText>
<TextInput
style={styles.input}
placeholder="Time"
value={String(set.time || "")}
placeholderTextColor={Colors.dark.subText}
selectTextOnFocus={true}
keyboardType="numeric"
onChangeText={(value: string) => {
<ThemedText style={styles.label}>Time (Min:Sec)</ThemedText>
<TimeInput
value={formatFromTotalSeconds(set.time || 0)}
onChange={(value: string) => {
setExercises((prev) => {
const updated = [...prev];
updated[exerciseIndex].sets[setIndex].time =
Number(value);
convertToTotalSeconds(value);
return updated;
});
}}
style={styles.timeInput}
/>
</View>
) : exercise.exercise_tracking_type === "reps" ? (
Expand Down Expand Up @@ -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",
},
});
119 changes: 63 additions & 56 deletions app/(app)/(tabs)/(stats)/history-details.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,21 +11,23 @@ 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");

export default function HistoryDetailsScreen() {
const { id } = useLocalSearchParams();
const [workout, setWorkout] = useState<CompletedWorkout | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

const {
data: settings,
Expand All @@ -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) => {
Comment thread
isotronic marked this conversation as resolved.
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]),
Comment thread
isotronic marked this conversation as resolved.
);

// Calculate total volume
const totalVolume = useMemo(() => {
if (!workout) return 0;

Expand All @@ -106,22 +103,32 @@ 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 (
<ThemedView style={styles.container}>
<ActivityIndicator size="large" color={Colors.dark.text} />
</ThemedView>
);
}

if (settingsError || workoutError) {
const error = settingsError || workoutError;
if (error instanceof Error) {
Bugsnag.notify(error);
return <ThemedText>Error: {error.message}</ThemedText>;
}
if (settingsError instanceof Error) {
return <ThemedText>Error: {settingsError.message}</ThemedText>;
}

if (error) {
return <ThemedText>Error: {error.message}</ThemedText>;
}

if (!workout) {
return null;
}

const isoDateString = workout.date_completed.replace(" ", "T");
Expand Down Expand Up @@ -219,10 +226,8 @@ export default function HistoryDetailsScreen() {
{/* Exercise List */}
<View style={styles.exerciseList}>
{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}`;
}
Expand Down Expand Up @@ -253,7 +258,9 @@ export default function HistoryDetailsScreen() {
</ThemedText>
{exercise.exercise_tracking_type === "time" ? (
<ThemedText style={styles.setText}>
{set.time} Seconds
{set.time != null
? formatFromTotalSeconds(set.time)
: "—"}
</ThemedText>
) : exercise.exercise_tracking_type === "reps" ? (
<ThemedText style={styles.setText}>
Expand Down
3 changes: 2 additions & 1 deletion app/(app)/(workout)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions app/(app)/(workout)/workout-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
},
},
]);
Expand Down
12 changes: 3 additions & 9 deletions components/SessionSetInfo.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -403,6 +396,7 @@ export default function SessionSetInfo({
</View>
<View style={styles.inputContainer}>
<TimeInput
key={`${exercise_id}-${currentSetIndex}`}
value={displayValue}
onChange={handleTimeInputChange}
style={styles.input}
Expand Down
Loading