Timer is persistent with Local Storage. Drag&Drop of planned sets fixed on mobile.
This commit is contained in:
@@ -10,14 +10,91 @@ interface UseRestTimerProps {
|
||||
}
|
||||
|
||||
export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
const [timeLeft, setTimeLeft] = useState(defaultTime);
|
||||
const [status, setStatus] = useState<TimerStatus>('IDLE');
|
||||
const [duration, setDuration] = useState(defaultTime); // The set duration to reset to
|
||||
// Initial state function to restore from localStorage if available
|
||||
const getInitialState = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('gymflow_rest_timer');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
// Validate parsed data structure lightly
|
||||
if (parsed && typeof parsed.timeLeft === 'number') {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse saved timer", e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const endTimeRef = useRef<number | null>(null);
|
||||
const savedState = getInitialState();
|
||||
|
||||
// If we have a saved running timer, we need to recalculate time left
|
||||
let initialTimeLeft = defaultTime;
|
||||
let initialStatus: TimerStatus = 'IDLE';
|
||||
let initialDuration = defaultTime;
|
||||
|
||||
if (savedState) {
|
||||
initialDuration = savedState.duration || defaultTime;
|
||||
initialStatus = savedState.status;
|
||||
initialTimeLeft = savedState.timeLeft;
|
||||
|
||||
if (initialStatus === 'RUNNING' && savedState.endTime) {
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((savedState.endTime - now) / 1000));
|
||||
if (remaining > 0) {
|
||||
initialTimeLeft = remaining;
|
||||
} else {
|
||||
initialStatus = 'FINISHED'; // It finished while we were away
|
||||
initialTimeLeft = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(initialTimeLeft);
|
||||
const [status, setStatus] = useState<TimerStatus>(initialStatus);
|
||||
const [duration, setDuration] = useState(initialDuration);
|
||||
|
||||
const endTimeRef = useRef<number | null>(savedState?.endTime || null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const prevDefaultTimeRef = useRef(defaultTime);
|
||||
|
||||
// Tick function - defined before effects
|
||||
const tick = useCallback(() => {
|
||||
if (!endTimeRef.current) return;
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((endTimeRef.current - now) / 1000));
|
||||
|
||||
setTimeLeft(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
setStatus('FINISHED');
|
||||
playTimeUpSignal();
|
||||
if (onFinish) onFinish();
|
||||
|
||||
endTimeRef.current = null; // Clear end time
|
||||
|
||||
// Auto-reset visuals after 3 seconds of "FINISHED" state?
|
||||
setTimeout(() => {
|
||||
setStatus(prev => prev === 'FINISHED' ? 'IDLE' : prev);
|
||||
setTimeLeft(prev => prev === 0 ? duration : prev);
|
||||
}, 3000);
|
||||
} else {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
}, [duration, onFinish]);
|
||||
|
||||
// Save to localStorage whenever relevant state changes
|
||||
useEffect(() => {
|
||||
const stateToSave = {
|
||||
status,
|
||||
timeLeft,
|
||||
duration,
|
||||
endTime: endTimeRef.current
|
||||
};
|
||||
localStorage.setItem('gymflow_rest_timer', JSON.stringify(stateToSave));
|
||||
}, [status, timeLeft, duration]);
|
||||
|
||||
// Update internal duration when defaultTime changes
|
||||
useEffect(() => {
|
||||
if (prevDefaultTimeRef.current !== defaultTime) {
|
||||
@@ -30,34 +107,27 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
}
|
||||
}, [defaultTime, status]);
|
||||
|
||||
// Manage RAF based on status
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tick = useCallback(() => {
|
||||
if (!endTimeRef.current) return;
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((endTimeRef.current - now) / 1000));
|
||||
|
||||
setTimeLeft(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
setStatus('FINISHED');
|
||||
playTimeUpSignal();
|
||||
if (onFinish) onFinish();
|
||||
|
||||
// Auto-reset visuals after 3 seconds of "FINISHED" state?
|
||||
// Requirement says: "The FAB must first change color to red for 3 seconds, and then return to the idle state"
|
||||
// So the hook stays in FINISHED.
|
||||
setTimeout(() => {
|
||||
reset();
|
||||
}, 3000);
|
||||
if (status === 'RUNNING') {
|
||||
if (!rafRef.current) {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
} else {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [onFinish]);
|
||||
|
||||
return () => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [status, tick]);
|
||||
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (status === 'RUNNING') return;
|
||||
@@ -67,24 +137,23 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
endTimeRef.current = Date.now() + targetSeconds * 1000;
|
||||
|
||||
setStatus('RUNNING');
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}, [status, timeLeft, duration, tick]);
|
||||
// Effect will trigger tick
|
||||
}, [status, timeLeft, duration]);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
if (status !== 'RUNNING') return;
|
||||
setStatus('PAUSED');
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
// Effect calls cancelAnimationFrame
|
||||
endTimeRef.current = null;
|
||||
}, [status]);
|
||||
|
||||
const reset = useCallback((newDuration?: number) => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
|
||||
const nextDuration = newDuration !== undefined ? newDuration : duration;
|
||||
setDuration(nextDuration);
|
||||
setTimeLeft(nextDuration);
|
||||
setStatus('IDLE');
|
||||
endTimeRef.current = null;
|
||||
// Effect calls cancelAnimationFrame (since status becomes IDLE)
|
||||
}, [duration]);
|
||||
|
||||
const addTime = useCallback((seconds: number) => {
|
||||
|
||||
Reference in New Issue
Block a user