Merge branch 'add-setting' into 'main'
Add setting See merge request torpenn/boxons!4merge-requests/10/head
commit
3dda62869a
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [] Create a simple timer (number of repetitions, rest, and exercise) with simple sound design. Button Skip Last reps
|
- [x] Create a simple timer (number of repetitions, rest, and exercise) with simple sound design.
|
||||||
- [] Configure your own sounds
|
- [] Configure your own sounds
|
||||||
- [] Be able to create exercise presets with a name
|
- [] Be able to create exercise presets with a name
|
||||||
- [] Have a history in the form of a list or diary
|
- [] Have a history in the form of a list or diary
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link, Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { Text, View } from '@/components/shared/Themed';
|
import { Text, View } from '@/components/shared/Themed';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
dashboard: undefined;
|
Dashboard: undefined;
|
||||||
timer: { reps: number, restTime: number, workTime: number};
|
Timer: { reps: number, restTime: number, workTime: number};
|
||||||
|
Settings: undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Text } from '@/components/shared/Themed';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||||
|
import { loadUserSettings, saveUserSettings } from '@/components/shared/business/AsyncStorage';
|
||||||
|
import { Switch } from 'react-native';
|
||||||
|
import { VerticalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import Button from '@/components/shared/Button';
|
||||||
|
|
||||||
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
const [soundEnabled, setSoundEnabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
const soundEnabledLocal = await loadUserSettings('soundEnabled');
|
||||||
|
console.log('soundEnabledLocal', soundEnabledLocal);
|
||||||
|
|
||||||
|
if (soundEnabledLocal === null) {
|
||||||
|
console.log('soundEnabledLocal is null');
|
||||||
|
setSoundEnabled(true);
|
||||||
|
} else {
|
||||||
|
console.log('soundEnabledLocal is present', );
|
||||||
|
setSoundEnabled(Boolean(Number(soundEnabledLocal)));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Erreur lors du chargement des paramètres utilisateur');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveUserSettings('soundEnabled', String(Number(soundEnabled)));
|
||||||
|
}, [soundEnabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>Preferences</Title>
|
||||||
|
|
||||||
|
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Text>Son activé ?</Text>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
onValueChange={() => setSoundEnabled(previousState => !previousState)}
|
||||||
|
value={soundEnabled}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title = styled(Text)(({ theme }) => ({
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.black
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Row = styled.View({
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
})
|
||||||
|
|
||||||
|
const Container = styled.View({
|
||||||
|
padding: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||||
import { ThemeProvider } from '@emotion/react';
|
import { ThemeProvider } from '@emotion/react';
|
||||||
import { theme } from '@/app/shared/theme';
|
import { theme } from '@/app/shared/theme/index';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
|
|
@ -14,7 +14,7 @@ export {
|
||||||
|
|
||||||
export const unstable_settings = {
|
export const unstable_settings = {
|
||||||
// Ensure that reloading on `/modal` keeps a back button present.
|
// Ensure that reloading on `/modal` keeps a back button present.
|
||||||
initialRouteName: 'dashboard',
|
initialRouteName: 'Dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
|
|
@ -47,9 +47,10 @@ export default function RootLayout() {
|
||||||
function RootLayoutNav() {
|
function RootLayoutNav() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Stack>
|
<Stack initialRouteName="Dashboard">
|
||||||
<Stack.Screen name="dashboard" options={{ headerShown: false }} />
|
<Stack.Screen name="Dashboard" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="timer" options={{ headerShown: false }} />
|
<Stack.Screen name="Timer" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="Setting" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { TimerPickerModal } from 'react-native-timer-picker';
|
||||||
import Button from '@/components/shared/Button';
|
import Button from '@/components/shared/Button';
|
||||||
import NumberSelector from '@/components/shared/NumberSelector';
|
import NumberSelector from '@/components/shared/NumberSelector';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'timer'>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Timer'>;
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
|
@ -97,7 +97,11 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<VerticalSpacer heightUnits={5} />
|
<VerticalSpacer heightUnits={5} />
|
||||||
|
|
||||||
<Button label="Commencer" onPress={() => navigation.navigate('timer', { reps, restTime, workTime})} />
|
<Button label="Commencer" onPress={() => navigation.navigate('Timer', { reps, restTime, workTime})} />
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={5} />
|
||||||
|
|
||||||
|
<Button label="Préférences" onPress={() => navigation.navigate('Settings')} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { TimerBgColor } from '@/components/useCases/timer/business/type';
|
||||||
import { Audio } from 'expo-av';
|
import { Audio } from 'expo-av';
|
||||||
import { Sound } from 'expo-av/build/Audio';
|
import { Sound } from 'expo-av/build/Audio';
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
|
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
|
||||||
|
import { loadUserSettings } from '@/components/shared/business/AsyncStorage';
|
||||||
|
|
||||||
interface TimerProps {
|
interface TimerProps {
|
||||||
reps: number;
|
reps: number;
|
||||||
|
|
@ -27,11 +28,23 @@ export default function Timer() {
|
||||||
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 [sound, setSound] = useState<Sound>();
|
const [sound, setSound] = useState<Sound>();
|
||||||
|
const [soundEnabled, setSoundEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configureAudio();
|
const init = async () => {
|
||||||
handleStart();
|
try {
|
||||||
activateKeepAwakeAsync();
|
const soundEnabledLocal = await loadUserSettings('soundEnabled');
|
||||||
|
setSoundEnabled(Boolean(Number(soundEnabledLocal)));
|
||||||
|
|
||||||
|
if (soundEnabled) await configureAudio();
|
||||||
|
handleStart();
|
||||||
|
await activateKeepAwakeAsync();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Erreur lors du chargement des paramètres utilisateur');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// when the user exits the screen, desactivate the keepAwake
|
// when the user exits the screen, desactivate the keepAwake
|
||||||
|
|
@ -94,6 +107,8 @@ export default function Timer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playSound() {
|
async function playSound() {
|
||||||
|
if (!soundEnabled) return;
|
||||||
|
|
||||||
const { sound } = await Audio.Sound.createAsync(require('../assets/audios/boxingBell.mp3'));
|
const { sound } = await Audio.Sound.createAsync(require('../assets/audios/boxingBell.mp3'));
|
||||||
setSound(sound);
|
setSound(sound);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
export const saveUserSettings = async (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(key, value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Failed to load the data from AsyncStorage');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadUserSettings = async (key: string) => {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(key);
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Failed to load the data from AsyncStorage');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { saveUserSettings, loadUserSettings } from '../AsyncStorage';
|
||||||
|
|
||||||
|
// Mock de AsyncStorage
|
||||||
|
jest.mock('@react-native-async-storage/async-storage', () => ({
|
||||||
|
setItem: jest.fn(),
|
||||||
|
getItem: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('UserSettings functions', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveUserSettings', () => {
|
||||||
|
test('should save data to AsyncStorage', async () => {
|
||||||
|
const key = 'theme';
|
||||||
|
const value = 'dark';
|
||||||
|
|
||||||
|
await saveUserSettings(key, value);
|
||||||
|
|
||||||
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an error if AsyncStorage.setItem fails', async () => {
|
||||||
|
AsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage error'));
|
||||||
|
|
||||||
|
await expect(saveUserSettings('theme', 'dark')).rejects.toThrow(
|
||||||
|
'Failed to load the data from AsyncStorage'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadUserSettings', () => {
|
||||||
|
test('should load data from AsyncStorage', async () => {
|
||||||
|
const key = 'theme';
|
||||||
|
const value = 'dark';
|
||||||
|
|
||||||
|
AsyncStorage.getItem.mockResolvedValueOnce(value);
|
||||||
|
|
||||||
|
const result = await loadUserSettings(key);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
|
||||||
|
expect(AsyncStorage.getItem).toHaveBeenCalledWith(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the value does not exist in AsyncStorage', async () => {
|
||||||
|
AsyncStorage.getItem.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const result = await loadUserSettings('nonexistent_key');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an error if AsyncStorage.getItem fails', async () => {
|
||||||
|
AsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage error'));
|
||||||
|
|
||||||
|
await expect(loadUserSettings('theme')).rejects.toThrow(
|
||||||
|
'Failed to load the data from AsyncStorage'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -11,7 +11,7 @@ type FinishContentProps = {
|
||||||
handleStart: () => void;
|
handleStart: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'dashboard'>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||||
|
|
||||||
export default function FinishContent({
|
export default function FinishContent({
|
||||||
handleStart
|
handleStart
|
||||||
|
|
@ -29,7 +29,7 @@ export default function FinishContent({
|
||||||
|
|
||||||
<HorizontalSpacer widthUnits={3} />
|
<HorizontalSpacer widthUnits={3} />
|
||||||
|
|
||||||
<Button label="Retour" onPress={() => navigation.navigate('dashboard')} />
|
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ interface TimerContentProps {
|
||||||
bgColor: TimerBgColor;
|
bgColor: TimerBgColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'dashboard'>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Dashboard'>;
|
||||||
|
|
||||||
export default function TimerContent({
|
export default function TimerContent({
|
||||||
isWorkPhase,
|
isWorkPhase,
|
||||||
|
|
@ -58,7 +58,7 @@ export default function TimerContent({
|
||||||
|
|
||||||
<VerticalSpacer heightUnits={3} />
|
<VerticalSpacer heightUnits={3} />
|
||||||
|
|
||||||
<Button label="Retour" onPress={() => navigation.navigate('dashboard')} />
|
<Button label="Retour" onPress={() => navigation.navigate('Dashboard')} />
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"@emotion/native": "^11.11.0",
|
"@emotion/native": "^11.11.0",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
||||||
"@react-native-picker/picker": "2.7.5",
|
"@react-native-picker/picker": "2.7.5",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"@testing-library/react-native": "^12.7.2",
|
"@testing-library/react-native": "^12.7.2",
|
||||||
|
|
@ -5085,6 +5086,18 @@
|
||||||
"react": "^16.8 || ^17.0 || ^18.0"
|
"react": "^16.8 || ^17.0 || ^18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-async-storage/async-storage": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-af6H9JjfL6G/PktBfUivvexoiFKQTJGQCtSWxMdivLzNIY94mu9DdiY0JqCSg/LyPCLGKhHPUlRQhNvpu3/KVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"merge-options": "^3.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native-community/cli": {
|
"node_modules/@react-native-community/cli": {
|
||||||
"version": "13.6.9",
|
"version": "13.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.9.tgz",
|
||||||
|
|
@ -14133,6 +14146,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-obj": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-object": {
|
"node_modules/is-plain-object": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||||
|
|
@ -17793,6 +17815,18 @@
|
||||||
"integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==",
|
"integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-options": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-plain-obj": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"@emotion/native": "^11.11.0",
|
"@emotion/native": "^11.11.0",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
||||||
"@react-native-picker/picker": "2.7.5",
|
"@react-native-picker/picker": "2.7.5",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"@testing-library/react-native": "^12.7.2",
|
"@testing-library/react-native": "^12.7.2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue