feat: add prepation time to timers

main
Torpenn 2025-04-08 22:53:40 +02:00
parent 2e44837381
commit 5de377f0c3
No known key found for this signature in database
GPG Key ID: 56C8A89C974E3ED2
9 changed files with 176 additions and 36 deletions

View File

@ -21,22 +21,26 @@ const t = i18n.scoped('dashboard');
export default function Dashboard() { export default function Dashboard() {
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime } = useTimerContext(); const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime, updatePreparationTime } = useTimerContext();
const [showWorkTimePicker, setShowWorkTimePicker] = useState<boolean>(false); const [showWorkTimePicker, setShowWorkTimePicker] = useState<boolean>(false);
const [showPreparationTimePicker, setShowPreparationTimePicker] = useState<boolean>(false);
const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false); const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false);
// Local variables for UI changes // Local variables for UI changes
const [localPreparationTime, setLocalPreparationTime] = useState<number>(timerState.preparationTime);
const [localReps, setLocalReps] = useState<number>(timerState.reps); const [localReps, setLocalReps] = useState<number>(timerState.reps);
const [localWorkTime, setLocalWorkTime] = useState<number>(timerState.workTime); const [localWorkTime, setLocalWorkTime] = useState<number>(timerState.workTime);
const [localRestTime, setLocalRestTime] = useState<number>(timerState.restTime); const [localRestTime, setLocalRestTime] = useState<number>(timerState.restTime);
// Update local states when store data is loaded // Update local states when store data is loaded
useEffect(() => { useEffect(() => {
console.log('timerState', timerState);
if (!isLoading) { if (!isLoading) {
setLocalReps(timerState.reps); setLocalReps(timerState.reps);
setLocalWorkTime(timerState.workTime); setLocalWorkTime(timerState.workTime);
setLocalRestTime(timerState.restTime); setLocalRestTime(timerState.restTime);
setLocalPreparationTime(timerState.preparationTime);
} }
}, [isLoading, timerState]); }, [isLoading, timerState]);
@ -64,13 +68,35 @@ export default function Dashboard() {
<VerticalSpacer heightUnits={8} /> <VerticalSpacer heightUnits={8} />
<CardContainer> <CardContainer>
<Card backgroundColor="black"> <Card backgroundColor="grey" onPress={() => setShowPreparationTimePicker(true)}>
<CardTextContainer> <CardTextContainer>
<CustomText>{t('repetition')}</CustomText> <BlackCustomText>{t('preparation')}</BlackCustomText>
<NumberSelector reps={localReps} setReps={handleRepsChange} /> <BlackCustomText>{formatTime(localPreparationTime)}</BlackCustomText>
</CardTextContainer> </CardTextContainer>
</Card> </Card>
<TimerPickerModal
hideHours
visible={showPreparationTimePicker}
setIsVisible={setShowPreparationTimePicker}
onConfirm={(pickedDuration: any) => {
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,
}}
/>
<Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}> <Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}>
<CardTextContainer> <CardTextContainer>
<CustomText>{t('fight')}</CustomText> <CustomText>{t('fight')}</CustomText>
@ -128,6 +154,13 @@ export default function Dashboard() {
overlayOpacity: 0.2, overlayOpacity: 0.2,
}} }}
/> />
<Card backgroundColor="black">
<CardTextContainer>
<CustomText>{t('repetition')}</CustomText>
<NumberSelector reps={localReps} setReps={handleRepsChange} />
</CardTextContainer>
</Card>
</CardContainer> </CardContainer>
<VerticalSpacer heightUnits={5} /> <VerticalSpacer heightUnits={5} />
@ -179,3 +212,7 @@ const CustomText = styled(Text)(({ theme }) => ({
textAlign: 'center', textAlign: 'center',
color: theme.colors.fixed.white color: theme.colors.fixed.white
})) }))
const BlackCustomText = styled(CustomText)(({ theme }) => ({
color: theme.colors.fixed.black
}))

View File

@ -29,6 +29,7 @@ export default function Timer() {
const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true); const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true);
const [isRunning, setIsRunning] = useState<boolean>(false); const [isRunning, setIsRunning] = useState<boolean>(false);
const [isFinish, setIsFinish] = useState<boolean>(false); const [isFinish, setIsFinish] = useState<boolean>(false);
const [isPreparationPhase, setIsPreparationPhase] = useState<boolean>(false);
const [soundEnabled, setSoundEnabled] = useState<boolean>(true); const [soundEnabled, setSoundEnabled] = useState<boolean>(true);
const { playSound } = useAudio( const { playSound } = useAudio(
require("../assets/audios/boxingBell.mp3"), require("../assets/audios/boxingBell.mp3"),
@ -75,13 +76,25 @@ export default function Timer() {
timerId = BackgroundTimer.setInterval(() => { timerId = BackgroundTimer.setInterval(() => {
const newTime = timeLeft - 1; const newTime = timeLeft - 1;
setTimeLeft(newTime); setTimeLeft(newTime);
let phaseText = "Repos";
if (isPreparationPhase) {
phaseText = "Préparation";
} else if (isWorkPhase) {
phaseText = "Travail";
}
updateNotification( updateNotification(
"Timer en cours", "Timer en cours",
`Phase: ${isWorkPhase ? "Travail" : "Repos"}, Temps restant: ${newTime}s`, `Phase: ${phaseText}, Temps restant: ${newTime}s`,
); );
}, 1000); }, 1000);
} else if (isRunning && timeLeft === 0) { } else if (isRunning && timeLeft === 0) {
nextRep(); if (isPreparationPhase) {
startFirstRep();
} else {
nextRep();
}
} }
return () => { return () => {
@ -89,23 +102,43 @@ export default function Timer() {
BackgroundTimer.clearInterval(timerId); BackgroundTimer.clearInterval(timerId);
} }
}; };
}, [isRunning, timeLeft]); }, [isRunning, timeLeft, isPreparationPhase]);
const handleStart = () => { 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); setCurrentRep(1);
setIsWorkPhase(true); setIsWorkPhase(true);
setTimeLeft(timerState.workTime); setTimeLeft(timerState.workTime);
setIsRunning(true);
setIsFinish(false);
updateNotification(
"Timer en cours",
`Phase: ${isWorkPhase ? "Travail" : "Repos"}, Temps restant: ${timeLeft}s`,
);
}; };
const handleReset = () => { const handleReset = () => {
setCurrentRep(0); setCurrentRep(0);
setIsWorkPhase(true); setIsWorkPhase(true);
setIsPreparationPhase(false);
setTimeLeft(timerState.workTime); setTimeLeft(timerState.workTime);
setIsRunning(false); setIsRunning(false);
setIsFinish(false); setIsFinish(false);
@ -155,6 +188,7 @@ export default function Timer() {
const renderBgColor: () => TimerBgColor = () => { const renderBgColor: () => TimerBgColor = () => {
if (isFinish) return "black"; if (isFinish) return "black";
if (isPreparationPhase) return "grey";
if (isWorkPhase) return "red"; if (isWorkPhase) return "red";
return "green"; return "green";
@ -167,6 +201,7 @@ export default function Timer() {
{!isFinish && ( {!isFinish && (
<TimerContent <TimerContent
isWorkPhase={isWorkPhase} isWorkPhase={isWorkPhase}
isPreparationPhase={isPreparationPhase}
timeLeft={timeLeft} timeLeft={timeLeft}
reps={timerState.reps} reps={timerState.reps}
bgColor={renderBgColor()} bgColor={renderBgColor()}

View File

@ -5,11 +5,13 @@ export const en = {
repetition: 'Reps', repetition: 'Reps',
fight: 'Fight time', fight: 'Fight time',
rest: 'Rest', rest: 'Rest',
preparation: 'Preparation',
totalTime: 'Total time', totalTime: 'Total time',
begin: 'Start', begin: 'Start',
settings: 'Settings', settings: 'Settings',
setWorkTime: 'Set fight time', setWorkTime: 'Set fight time',
setRestTime: 'Set rest time', setRestTime: 'Set rest time',
setPreparationTime: 'Set preparation time',
}, },
settings: { settings: {
title: 'Settings', title: 'Settings',

View File

@ -3,6 +3,7 @@ export const fr = {
back: 'Retour', back: 'Retour',
dashboard: { dashboard: {
repetition: 'Reps.', repetition: 'Reps.',
preparation: 'Préparation',
fight: 'Temps du combat', fight: 'Temps du combat',
rest: 'Repos', rest: 'Repos',
totalTime: 'Temps total', totalTime: 'Temps total',
@ -10,6 +11,7 @@ export const fr = {
settings: 'Paramètres', settings: 'Paramètres',
setWorkTime: 'Définir le temps de combat', setWorkTime: 'Définir le temps de combat',
setRestTime: 'Définir le temps de repos', setRestTime: 'Définir le temps de repos',
setPreparationTime: 'Définir le temps de préparation',
}, },
settings: { settings: {
title: 'Paramètres', title: 'Paramètres',
@ -21,6 +23,8 @@ export const fr = {
timer: { timer: {
timerContent: { timerContent: {
fight: 'Combat', fight: 'Combat',
preparation: 'Préparation',
preparationDescription: 'Commencez à vous préparer',
rest: 'Repos', rest: 'Repos',
stop: 'Arrêter', stop: 'Arrêter',
start: 'Commencer', start: 'Commencer',

View File

@ -8,6 +8,7 @@ interface TimerContextType {
updateReps: (reps: number) => Promise<void>; updateReps: (reps: number) => Promise<void>;
updateWorkTime: (workTime: number) => Promise<void>; updateWorkTime: (workTime: number) => Promise<void>;
updateRestTime: (restTime: number) => Promise<void>; updateRestTime: (restTime: number) => Promise<void>;
updatePreparationTime: (preparationTime: number) => Promise<void>;
updateState: (state: Partial<TimerState>) => Promise<void>; updateState: (state: Partial<TimerState>) => Promise<void>;
} }
@ -16,12 +17,14 @@ const defaultContextValue: TimerContextType = {
timerState: { timerState: {
reps: 1, reps: 1,
workTime: 0, workTime: 0,
restTime: 0 restTime: 0,
preparationTime: 3
}, },
isLoading: true, isLoading: true,
updateReps: async () => {}, updateReps: async () => {},
updateWorkTime: async () => {}, updateWorkTime: async () => {},
updateRestTime: async () => {}, updateRestTime: async () => {},
updatePreparationTime: async () => {},
updateState: async () => {} updateState: async () => {}
}; };
@ -41,7 +44,8 @@ export const TimerProvider: React.FC<TimerProviderProps> = ({ children }) => {
const [timerState, setTimerState] = useState<TimerState>({ const [timerState, setTimerState] = useState<TimerState>({
reps: 1, reps: 1,
workTime: 0, workTime: 0,
restTime: 0 restTime: 0,
preparationTime: 3
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -71,6 +75,14 @@ export const TimerProvider: React.FC<TimerProviderProps> = ({ 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 // Update work time
const updateWorkTime = async (workTime: number) => { const updateWorkTime = async (workTime: number) => {
try { try {
@ -108,6 +120,7 @@ export const TimerProvider: React.FC<TimerProviderProps> = ({ children }) => {
updateReps, updateReps,
updateWorkTime, updateWorkTime,
updateRestTime, updateRestTime,
updatePreparationTime,
updateState updateState
}; };

View File

@ -4,7 +4,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
const TIMER_KEYS = { const TIMER_KEYS = {
REPS: 'timer_reps', REPS: 'timer_reps',
WORK_TIME: 'timer_work_time', WORK_TIME: 'timer_work_time',
REST_TIME: 'timer_rest_time' REST_TIME: 'timer_rest_time',
PREPARATION_TIME: 'timer_preparation_time'
}; };
// Store interface // Store interface
@ -12,13 +13,15 @@ export interface TimerState {
reps: number; reps: number;
workTime: number; workTime: number;
restTime: number; restTime: number;
preparationTime: number;
} }
// Default values // Default values
const DEFAULT_STATE: TimerState = { const DEFAULT_STATE: TimerState = {
reps: 1, reps: 1,
workTime: 0, workTime: 0,
restTime: 0 restTime: 0,
preparationTime: 3
}; };
// Store management class // Store management class
@ -32,6 +35,14 @@ class TimerStore {
} }
} }
async savePreparationTime(preparationTime: number): Promise<void> {
try {
await AsyncStorage.setItem(TIMER_KEYS.PREPARATION_TIME, preparationTime.toString());
} catch (error) {
console.error('Error saving preparation time:', error);
}
}
// Method to save work time // Method to save work time
async saveWorkTime(workTime: number): Promise<void> { async saveWorkTime(workTime: number): Promise<void> {
try { try {
@ -56,7 +67,8 @@ class TimerStore {
await Promise.all([ await Promise.all([
this.saveReps(state.reps), this.saveReps(state.reps),
this.saveWorkTime(state.workTime), this.saveWorkTime(state.workTime),
this.saveRestTime(state.restTime) this.saveRestTime(state.restTime),
this.savePreparationTime(state.preparationTime)
]); ]);
} catch (error) { } catch (error) {
console.error('Error saving state:', error); console.error('Error saving state:', error);
@ -74,6 +86,16 @@ class TimerStore {
} }
} }
async getPreparationTime(): Promise<number> {
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 // Method to retrieve work time
async getWorkTime(): Promise<number> { async getWorkTime(): Promise<number> {
try { try {
@ -99,16 +121,18 @@ class TimerStore {
// Method to retrieve complete state // Method to retrieve complete state
async getState(): Promise<TimerState> { async getState(): Promise<TimerState> {
try { try {
const [reps, workTime, restTime] = await Promise.all([ const [reps, workTime, restTime, preparationTime] = await Promise.all([
this.getReps(), this.getReps(),
this.getWorkTime(), this.getWorkTime(),
this.getRestTime() this.getRestTime(),
this.getPreparationTime()
]); ]);
return { return {
reps, reps,
workTime, workTime,
restTime restTime,
preparationTime
}; };
} catch (error) { } catch (error) {
console.error('Error retrieving state:', error); console.error('Error retrieving state:', error);

View File

@ -102,7 +102,8 @@ describe('TimerStore', () => {
const state: TimerState = { const state: TimerState = {
reps: 3, reps: 3,
workTime: 40, workTime: 40,
restTime: 20 restTime: 20,
preparationTime: 3
}; };
await timerStore.saveState(state); await timerStore.saveState(state);
@ -233,7 +234,8 @@ describe('TimerStore', () => {
expect(result).toEqual({ expect(result).toEqual({
reps: 3, reps: 3,
workTime: 40, workTime: 40,
restTime: 20 restTime: 20,
preparationTime: 3
}); });
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps'); expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
@ -250,7 +252,8 @@ describe('TimerStore', () => {
expect(result).toEqual({ expect(result).toEqual({
reps: 1, reps: 1,
workTime: 0, workTime: 0,
restTime: 0 restTime: 0,
preparationTime: 3
}); // Default values }); // Default values
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(

View File

@ -1 +1 @@
export type TimerBgColor = 'red' | 'green' | 'black'; export type TimerBgColor = 'red' | 'green' | 'black' | 'grey';

View File

@ -10,6 +10,7 @@ import { i18n } from '@/app/i18n/i18n';
interface TimerContentProps { interface TimerContentProps {
isWorkPhase: boolean; isWorkPhase: boolean;
isPreparationPhase?: boolean;
timeLeft: number; timeLeft: number;
reps: number; reps: number;
currentRep: number; currentRep: number;
@ -26,6 +27,7 @@ const t = i18n.scoped('timer.timerContent');
export default function TimerContent({ export default function TimerContent({
isWorkPhase, isWorkPhase,
isPreparationPhase = false,
timeLeft, timeLeft,
reps, reps,
currentRep, currentRep,
@ -44,24 +46,40 @@ export default function TimerContent({
handleReset(); 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 ( return (
<> <>
<Title>{isWorkPhase ? t('fight') : t('rest')}</Title> <Title isPreparationPhase={isPreparationPhase}>{getTitle()}</Title>
<VerticalSpacer heightUnits={8} /> <VerticalSpacer heightUnits={8} />
<Time> <Time isPreparationPhase={isPreparationPhase}>
{formatTime(timeLeft)} {formatTime(timeLeft)}
</Time> </Time>
<VerticalSpacer heightUnits={8} /> <VerticalSpacer heightUnits={8} />
<Reps>{currentRep} / {reps}</Reps> {!isPreparationPhase && <Reps>{currentRep} / {reps}</Reps>}
{isPreparationPhase && <Reps isPreparationPhase={true}>{t('preparationDescription')}</Reps>}
<VerticalSpacer heightUnits={8} /> <VerticalSpacer heightUnits={8} />
<ButtonContainer bgColor={bgColor}> <ButtonContainer bgColor={bgColor}>
<Button label={t('prev')} onPress={previousRep} /> <Button
label={t('prev')}
onPress={isPreparationPhase ? handleNoop : previousRep}
status={isPreparationPhase ? 'disabled' : 'ready'}
/>
<ElasticSpacer /> <ElasticSpacer />
@ -73,7 +91,11 @@ export default function TimerContent({
<ElasticSpacer /> <ElasticSpacer />
<Button label={t('next')} onPress={nextRep} /> <Button
label={t('next')}
onPress={isPreparationPhase ? nextRep : nextRep}
status="ready"
/>
</ButtonContainer> </ButtonContainer>
<VerticalSpacer heightUnits={4} /> <VerticalSpacer heightUnits={4} />
@ -89,20 +111,20 @@ const ButtonContainer = styled(View)<{ bgColor: TimerBgColor }>(({ theme, bgColo
justifyContent: 'space-between', justifyContent: 'space-between',
})); }));
const Title = styled(Text)(({ theme }) => ({ const Title = styled(Text)<{ isPreparationPhase?: boolean }>(({ theme, isPreparationPhase = false }) => ({
fontSize: 50, fontSize: 50,
fontWeight: 'bold', fontWeight: 'bold',
color: theme.colors.fixed.white color: isPreparationPhase ? theme.colors.fixed.black : theme.colors.fixed.white
})); }));
const Time = styled(Text)(({ theme }) => ({ const Time = styled(Text)<{ isPreparationPhase?: boolean }>(({ theme, isPreparationPhase = false }) => ({
fontSize: 100, fontSize: 100,
fontWeight: 'bold', fontWeight: 'bold',
color: theme.colors.fixed.white color: isPreparationPhase ? theme.colors.fixed.black : theme.colors.fixed.white
})); }));
const Reps = styled(Text)(({ theme }) => ({ const Reps = styled(Text)<{ isPreparationPhase?: boolean }>(({ theme, isPreparationPhase = false }) => ({
fontSize: 30, fontSize: 30,
fontWeight: 'bold', fontWeight: 'bold',
color: theme.colors.fixed.white color: isPreparationPhase ? theme.colors.fixed.black : theme.colors.fixed.white
})); }));