From 5de377f0c336f7366d71826c9a0b3022dd827f53 Mon Sep 17 00:00:00 2001 From: Torpenn Date: Tue, 8 Apr 2025 22:53:40 +0200 Subject: [PATCH] feat: add prepation time to timers --- app/(tabs)/index.tsx | 45 ++++++++++++++-- app/Timer.tsx | 53 +++++++++++++++---- app/i18n/translations/en.ts | 2 + app/i18n/translations/fr.ts | 4 ++ app/store/TimerContext.tsx | 17 +++++- app/store/TimerStore.ts | 36 ++++++++++--- app/store/__tests__/TimerStore.test.ts | 9 ++-- components/useCases/timer/business/type.ts | 2 +- .../useCases/timer/view/TimerContent.tsx | 44 +++++++++++---- 9 files changed, 176 insertions(+), 36 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 165fdf1..1c298f6 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -21,22 +21,26 @@ const t = i18n.scoped('dashboard'); export default function Dashboard() { const navigation = useNavigation(); - const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime } = useTimerContext(); + const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime, updatePreparationTime } = useTimerContext(); const [showWorkTimePicker, setShowWorkTimePicker] = useState(false); + const [showPreparationTimePicker, setShowPreparationTimePicker] = useState(false); const [showRestTimePicker, setShowRestTimePicker] = useState(false); // Local variables for UI changes + const [localPreparationTime, setLocalPreparationTime] = useState(timerState.preparationTime); const [localReps, setLocalReps] = useState(timerState.reps); const [localWorkTime, setLocalWorkTime] = useState(timerState.workTime); const [localRestTime, setLocalRestTime] = useState(timerState.restTime); // Update local states when store data is loaded useEffect(() => { + console.log('timerState', timerState); if (!isLoading) { setLocalReps(timerState.reps); setLocalWorkTime(timerState.workTime); setLocalRestTime(timerState.restTime); + setLocalPreparationTime(timerState.preparationTime); } }, [isLoading, timerState]); @@ -64,13 +68,35 @@ export default function Dashboard() { - + setShowPreparationTimePicker(true)}> - {t('repetition')} - + {t('preparation')} + {formatTime(localPreparationTime)} + { + const newPreparationTime = pickedDuration.minutes * 60 + pickedDuration.seconds; + setLocalPreparationTime(newPreparationTime); + updatePreparationTime(newPreparationTime); + setShowPreparationTimePicker(false); + }} + modalTitle={t('setPreparationTime')} + onCancel={() => setShowPreparationTimePicker(false)} + closeOnOverlayPress + styles={{ + theme: "dark", + }} + initialValue={{ minutes: Math.floor(timerState.preparationTime / 60), seconds: timerState.preparationTime % 60 }} + modalProps={{ + overlayOpacity: 0.2, + }} + /> + setShowWorkTimePicker(true)}> {t('fight')} @@ -128,6 +154,13 @@ export default function Dashboard() { overlayOpacity: 0.2, }} /> + + + + {t('repetition')} + + + @@ -179,3 +212,7 @@ const CustomText = styled(Text)(({ theme }) => ({ textAlign: 'center', color: theme.colors.fixed.white })) + +const BlackCustomText = styled(CustomText)(({ theme }) => ({ + color: theme.colors.fixed.black +})) diff --git a/app/Timer.tsx b/app/Timer.tsx index 159bc57..34ba7f2 100644 --- a/app/Timer.tsx +++ b/app/Timer.tsx @@ -29,6 +29,7 @@ export default function Timer() { const [isWorkPhase, setIsWorkPhase] = useState(true); const [isRunning, setIsRunning] = useState(false); const [isFinish, setIsFinish] = useState(false); + const [isPreparationPhase, setIsPreparationPhase] = useState(false); const [soundEnabled, setSoundEnabled] = useState(true); const { playSound } = useAudio( require("../assets/audios/boxingBell.mp3"), @@ -75,13 +76,25 @@ export default function Timer() { timerId = BackgroundTimer.setInterval(() => { const newTime = timeLeft - 1; setTimeLeft(newTime); + + let phaseText = "Repos"; + if (isPreparationPhase) { + phaseText = "Préparation"; + } else if (isWorkPhase) { + phaseText = "Travail"; + } + updateNotification( "Timer en cours", - `Phase: ${isWorkPhase ? "Travail" : "Repos"}, Temps restant: ${newTime}s`, + `Phase: ${phaseText}, Temps restant: ${newTime}s`, ); }, 1000); } else if (isRunning && timeLeft === 0) { - nextRep(); + if (isPreparationPhase) { + startFirstRep(); + } else { + nextRep(); + } } return () => { @@ -89,23 +102,43 @@ export default function Timer() { BackgroundTimer.clearInterval(timerId); } }; - }, [isRunning, timeLeft]); + }, [isRunning, timeLeft, isPreparationPhase]); const handleStart = () => { + // Démarrer avec la phase de préparation si elle est configurée + if (timerState.preparationTime > 0) { + setIsPreparationPhase(true); + setCurrentRep(0); + setTimeLeft(timerState.preparationTime); + } else { + setIsPreparationPhase(false); + setCurrentRep(1); + setIsWorkPhase(true); + setTimeLeft(timerState.workTime); + } + + setIsRunning(true); + setIsFinish(false); + + const phaseText = timerState.preparationTime > 0 ? "Préparation" : "Travail"; + updateNotification( + "Timer en cours", + `Phase: ${phaseText}, Temps restant: ${timeLeft}s`, + ); + }; + + const startFirstRep = () => { + playSound(); + setIsPreparationPhase(false); setCurrentRep(1); setIsWorkPhase(true); setTimeLeft(timerState.workTime); - setIsRunning(true); - setIsFinish(false); - updateNotification( - "Timer en cours", - `Phase: ${isWorkPhase ? "Travail" : "Repos"}, Temps restant: ${timeLeft}s`, - ); }; const handleReset = () => { setCurrentRep(0); setIsWorkPhase(true); + setIsPreparationPhase(false); setTimeLeft(timerState.workTime); setIsRunning(false); setIsFinish(false); @@ -155,6 +188,7 @@ export default function Timer() { const renderBgColor: () => TimerBgColor = () => { if (isFinish) return "black"; + if (isPreparationPhase) return "grey"; if (isWorkPhase) return "red"; return "green"; @@ -167,6 +201,7 @@ export default function Timer() { {!isFinish && ( Promise; updateWorkTime: (workTime: number) => Promise; updateRestTime: (restTime: number) => Promise; + updatePreparationTime: (preparationTime: number) => Promise; updateState: (state: Partial) => Promise; } @@ -16,12 +17,14 @@ const defaultContextValue: TimerContextType = { timerState: { reps: 1, workTime: 0, - restTime: 0 + restTime: 0, + preparationTime: 3 }, isLoading: true, updateReps: async () => {}, updateWorkTime: async () => {}, updateRestTime: async () => {}, + updatePreparationTime: async () => {}, updateState: async () => {} }; @@ -41,7 +44,8 @@ export const TimerProvider: React.FC = ({ children }) => { const [timerState, setTimerState] = useState({ reps: 1, workTime: 0, - restTime: 0 + restTime: 0, + preparationTime: 3 }); const [isLoading, setIsLoading] = useState(true); @@ -71,6 +75,14 @@ export const TimerProvider: React.FC = ({ children }) => { } }; + const updatePreparationTime = async (preparationTime: number) => { + try { + await timerStore.savePreparationTime(preparationTime); + setTimerState(prev => ({ ...prev, preparationTime })); + } catch (error) { + console.error('Error updating preparation time:', error); + } + } // Update work time const updateWorkTime = async (workTime: number) => { try { @@ -108,6 +120,7 @@ export const TimerProvider: React.FC = ({ children }) => { updateReps, updateWorkTime, updateRestTime, + updatePreparationTime, updateState }; diff --git a/app/store/TimerStore.ts b/app/store/TimerStore.ts index 62875a9..0b5663f 100644 --- a/app/store/TimerStore.ts +++ b/app/store/TimerStore.ts @@ -4,7 +4,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; const TIMER_KEYS = { REPS: 'timer_reps', WORK_TIME: 'timer_work_time', - REST_TIME: 'timer_rest_time' + REST_TIME: 'timer_rest_time', + PREPARATION_TIME: 'timer_preparation_time' }; // Store interface @@ -12,13 +13,15 @@ export interface TimerState { reps: number; workTime: number; restTime: number; + preparationTime: number; } // Default values const DEFAULT_STATE: TimerState = { reps: 1, workTime: 0, - restTime: 0 + restTime: 0, + preparationTime: 3 }; // Store management class @@ -32,6 +35,14 @@ class TimerStore { } } + async savePreparationTime(preparationTime: number): Promise { + try { + await AsyncStorage.setItem(TIMER_KEYS.PREPARATION_TIME, preparationTime.toString()); + } catch (error) { + console.error('Error saving preparation time:', error); + } + } + // Method to save work time async saveWorkTime(workTime: number): Promise { try { @@ -56,7 +67,8 @@ class TimerStore { await Promise.all([ this.saveReps(state.reps), this.saveWorkTime(state.workTime), - this.saveRestTime(state.restTime) + this.saveRestTime(state.restTime), + this.savePreparationTime(state.preparationTime) ]); } catch (error) { console.error('Error saving state:', error); @@ -74,6 +86,16 @@ class TimerStore { } } + async getPreparationTime(): Promise { + try { + const value = await AsyncStorage.getItem(TIMER_KEYS.PREPARATION_TIME); + return value !== null ? parseInt(value, 10) : DEFAULT_STATE.preparationTime; + } catch (error) { + console.error('Error retrieving preparation time:', error); + return DEFAULT_STATE.preparationTime; + } + } + // Method to retrieve work time async getWorkTime(): Promise { try { @@ -99,16 +121,18 @@ class TimerStore { // Method to retrieve complete state async getState(): Promise { try { - const [reps, workTime, restTime] = await Promise.all([ + const [reps, workTime, restTime, preparationTime] = await Promise.all([ this.getReps(), this.getWorkTime(), - this.getRestTime() + this.getRestTime(), + this.getPreparationTime() ]); return { reps, workTime, - restTime + restTime, + preparationTime }; } catch (error) { console.error('Error retrieving state:', error); diff --git a/app/store/__tests__/TimerStore.test.ts b/app/store/__tests__/TimerStore.test.ts index cba8a94..a7868e0 100644 --- a/app/store/__tests__/TimerStore.test.ts +++ b/app/store/__tests__/TimerStore.test.ts @@ -102,7 +102,8 @@ describe('TimerStore', () => { const state: TimerState = { reps: 3, workTime: 40, - restTime: 20 + restTime: 20, + preparationTime: 3 }; await timerStore.saveState(state); @@ -233,7 +234,8 @@ describe('TimerStore', () => { expect(result).toEqual({ reps: 3, workTime: 40, - restTime: 20 + restTime: 20, + preparationTime: 3 }); expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps'); @@ -250,7 +252,8 @@ describe('TimerStore', () => { expect(result).toEqual({ reps: 1, workTime: 0, - restTime: 0 + restTime: 0, + preparationTime: 3 }); // Default values expect(consoleSpy).toHaveBeenCalledWith( diff --git a/components/useCases/timer/business/type.ts b/components/useCases/timer/business/type.ts index f395f20..2dd9d53 100644 --- a/components/useCases/timer/business/type.ts +++ b/components/useCases/timer/business/type.ts @@ -1 +1 @@ -export type TimerBgColor = 'red' | 'green' | 'black'; +export type TimerBgColor = 'red' | 'green' | 'black' | 'grey'; diff --git a/components/useCases/timer/view/TimerContent.tsx b/components/useCases/timer/view/TimerContent.tsx index a5e3eec..0ac8cfb 100644 --- a/components/useCases/timer/view/TimerContent.tsx +++ b/components/useCases/timer/view/TimerContent.tsx @@ -10,6 +10,7 @@ import { i18n } from '@/app/i18n/i18n'; interface TimerContentProps { isWorkPhase: boolean; + isPreparationPhase?: boolean; timeLeft: number; reps: number; currentRep: number; @@ -26,6 +27,7 @@ const t = i18n.scoped('timer.timerContent'); export default function TimerContent({ isWorkPhase, + isPreparationPhase = false, timeLeft, reps, currentRep, @@ -44,24 +46,40 @@ export default function TimerContent({ handleReset(); }; + const getTitle = () => { + if (isPreparationPhase) return t('preparation'); + if (isWorkPhase) return t('fight'); + return t('rest'); + }; + + // Handler for disabled buttons during preparation phase + const handleNoop = () => { + // Ne fait rien si on est en phase de préparation + }; + return ( <> - {isWorkPhase ? t('fight') : t('rest')} + {getTitle()} -