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() {
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 [showPreparationTimePicker, setShowPreparationTimePicker] = useState<boolean>(false);
const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false);
// Local variables for UI changes
const [localPreparationTime, setLocalPreparationTime] = useState<number>(timerState.preparationTime);
const [localReps, setLocalReps] = useState<number>(timerState.reps);
const [localWorkTime, setLocalWorkTime] = useState<number>(timerState.workTime);
const [localRestTime, setLocalRestTime] = useState<number>(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() {
<VerticalSpacer heightUnits={8} />
<CardContainer>
<Card backgroundColor="black">
<Card backgroundColor="grey" onPress={() => setShowPreparationTimePicker(true)}>
<CardTextContainer>
<CustomText>{t('repetition')}</CustomText>
<NumberSelector reps={localReps} setReps={handleRepsChange} />
<BlackCustomText>{t('preparation')}</BlackCustomText>
<BlackCustomText>{formatTime(localPreparationTime)}</BlackCustomText>
</CardTextContainer>
</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)}>
<CardTextContainer>
<CustomText>{t('fight')}</CustomText>
@ -128,6 +154,13 @@ export default function Dashboard() {
overlayOpacity: 0.2,
}}
/>
<Card backgroundColor="black">
<CardTextContainer>
<CustomText>{t('repetition')}</CustomText>
<NumberSelector reps={localReps} setReps={handleRepsChange} />
</CardTextContainer>
</Card>
</CardContainer>
<VerticalSpacer heightUnits={5} />
@ -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
}))

View File

@ -29,6 +29,7 @@ export default function Timer() {
const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true);
const [isRunning, setIsRunning] = useState<boolean>(false);
const [isFinish, setIsFinish] = useState<boolean>(false);
const [isPreparationPhase, setIsPreparationPhase] = useState<boolean>(false);
const [soundEnabled, setSoundEnabled] = useState<boolean>(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 && (
<TimerContent
isWorkPhase={isWorkPhase}
isPreparationPhase={isPreparationPhase}
timeLeft={timeLeft}
reps={timerState.reps}
bgColor={renderBgColor()}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ interface TimerContextType {
updateReps: (reps: number) => Promise<void>;
updateWorkTime: (workTime: number) => Promise<void>;
updateRestTime: (restTime: number) => Promise<void>;
updatePreparationTime: (preparationTime: number) => Promise<void>;
updateState: (state: Partial<TimerState>) => Promise<void>;
}
@ -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<TimerProviderProps> = ({ children }) => {
const [timerState, setTimerState] = useState<TimerState>({
reps: 1,
workTime: 0,
restTime: 0
restTime: 0,
preparationTime: 3
});
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
const updateWorkTime = async (workTime: number) => {
try {
@ -108,6 +120,7 @@ export const TimerProvider: React.FC<TimerProviderProps> = ({ children }) => {
updateReps,
updateWorkTime,
updateRestTime,
updatePreparationTime,
updateState
};

View File

@ -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<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
async saveWorkTime(workTime: number): Promise<void> {
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<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
async getWorkTime(): Promise<number> {
try {
@ -99,16 +121,18 @@ class TimerStore {
// Method to retrieve complete state
async getState(): Promise<TimerState> {
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);

View File

@ -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(

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