diff --git a/App.tsx b/App.tsx index 66a25d6..166f144 100644 --- a/App.tsx +++ b/App.tsx @@ -18,27 +18,21 @@ */ import React from 'react'; -import { LogBox, Platform, SafeAreaView, View } from 'react-native'; -import { NavigationContainer } from '@react-navigation/native'; -import { Provider as PaperProvider } from 'react-native-paper'; +import { LogBox, Platform } from 'react-native'; import { setSafeBounceHeight } from 'react-navigation-collapsible'; import SplashScreen from 'react-native-splash-screen'; -import { OverflowMenuProvider } from 'react-navigation-header-buttons'; -import AsyncStorageManager from './src/managers/AsyncStorageManager'; -import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider'; -import ThemeManager from './src/managers/ThemeManager'; -import MainNavigator from './src/navigation/MainNavigator'; -import AprilFoolsManager from './src/managers/AprilFoolsManager'; -import Update from './src/constants/Update'; import ConnectionManager from './src/managers/ConnectionManager'; import type { ParsedUrlDataType } from './src/utils/URLHandler'; import URLHandler from './src/utils/URLHandler'; -import { setupStatusBar } from './src/utils/Utils'; import initLocales from './src/utils/Locales'; import { NavigationContainerRef } from '@react-navigation/core'; -import GENERAL_STYLES from './src/constants/Styles'; -import CollapsibleProvider from './src/components/providers/CollapsibleProvider'; -import CacheProvider from './src/components/providers/CacheProvider'; +import { + defaultPreferences, + PreferenceKeys, + retrievePreferences, +} from './src/utils/asyncStorage'; +import PreferencesProvider from './src/components/providers/PreferencesProvider'; +import MainApp from './src/screens/MainApp'; // Native optimizations https://reactnavigation.org/docs/react-native-screens // Crashes app when navigating away from webview on android 9+ @@ -52,10 +46,6 @@ LogBox.ignoreLogs([ type StateType = { isLoading: boolean; - showIntro: boolean; - showUpdate: boolean; - showAprilFools: boolean; - currentTheme: ReactNativePaper.Theme | undefined; }; export default class App extends React.Component<{}, StateType> { @@ -71,10 +61,6 @@ export default class App extends React.Component<{}, StateType> { super(props); this.state = { isLoading: true, - showIntro: true, - showUpdate: true, - showAprilFools: false, - currentTheme: undefined, }; initLocales(); this.navigatorRef = React.createRef(); @@ -114,67 +100,12 @@ export default class App extends React.Component<{}, StateType> { } }; - /** - * Updates the current theme - */ - onUpdateTheme = () => { - this.setState({ - currentTheme: ThemeManager.getCurrentTheme(), - }); - setupStatusBar(); - }; - - /** - * Callback when user ends the intro. Save in preferences to avoid showing back the introSlides - */ - onIntroDone = () => { - this.setState({ - showIntro: false, - showUpdate: false, - showAprilFools: false, - }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.showIntro.key, - false - ); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.updateNumber.key, - Update.number - ); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key, - false - ); - }; - /** * Async loading is done, finish processing startup data */ onLoadFinished = () => { - // Only show intro if this is the first time starting the app - ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme); - // Status bar goes dark if set too fast on ios - if (Platform.OS === 'ios') { - setTimeout(setupStatusBar, 1000); - } else { - setupStatusBar(); - } - this.setState({ isLoading: false, - currentTheme: ThemeManager.getCurrentTheme(), - showIntro: AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.showIntro.key - ), - showUpdate: - AsyncStorageManager.getNumber( - AsyncStorageManager.PREFERENCES.updateNumber.key - ) !== Update.number, - showAprilFools: - AprilFoolsManager.getInstance().isAprilFoolsEnabled() && - AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key - ), }); SplashScreen.hide(); }; @@ -186,7 +117,7 @@ export default class App extends React.Component<{}, StateType> { */ loadAssetsAsync() { Promise.all([ - AsyncStorageManager.getInstance().loadPreferences(), + retrievePreferences(Object.values(PreferenceKeys), defaultPreferences), ConnectionManager.getInstance().recoverLogin(), ]) .then(this.onLoadFinished) @@ -201,43 +132,14 @@ export default class App extends React.Component<{}, StateType> { if (state.isLoading) { return null; } - if (state.showIntro || state.showUpdate || state.showAprilFools) { - return ( - - ); - } return ( - - - - - - - - - - - - - - - + + + ); } } diff --git a/src/components/Lists/CardList/CardList.tsx b/src/components/Lists/CardList/CardList.tsx index 3df77c0..e7a60c8 100644 --- a/src/components/Lists/CardList/CardList.tsx +++ b/src/components/Lists/CardList/CardList.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { Animated, Dimensions, ViewStyle } from 'react-native'; import ImageListItem from './ImageListItem'; import CardListItem from './CardListItem'; -import type { ServiceItemType } from '../../../managers/ServicesManager'; +import { ServiceItemType } from '../../../utils/Services'; type PropsType = { dataset: Array; diff --git a/src/components/Lists/CardList/CardListItem.tsx b/src/components/Lists/CardList/CardListItem.tsx index 155d868..ef48838 100644 --- a/src/components/Lists/CardList/CardListItem.tsx +++ b/src/components/Lists/CardList/CardListItem.tsx @@ -20,8 +20,8 @@ import * as React from 'react'; import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper'; import { StyleSheet, View } from 'react-native'; -import type { ServiceItemType } from '../../../managers/ServicesManager'; import GENERAL_STYLES from '../../../constants/Styles'; +import { ServiceItemType } from '../../../utils/Services'; type PropsType = { item: ServiceItemType; diff --git a/src/components/Lists/CardList/ImageListItem.tsx b/src/components/Lists/CardList/ImageListItem.tsx index 993b24c..dbb69f8 100644 --- a/src/components/Lists/CardList/ImageListItem.tsx +++ b/src/components/Lists/CardList/ImageListItem.tsx @@ -20,8 +20,8 @@ import * as React from 'react'; import { Text, TouchableRipple } from 'react-native-paper'; import { Image, StyleSheet, View } from 'react-native'; -import type { ServiceItemType } from '../../../managers/ServicesManager'; import GENERAL_STYLES from '../../../constants/Styles'; +import { ServiceItemType } from '../../../utils/Services'; type PropsType = { item: ServiceItemType; diff --git a/src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx b/src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx index dc04a4d..41a1586 100644 --- a/src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx +++ b/src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx @@ -23,10 +23,7 @@ import { FlatList, Image, StyleSheet, View } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import DashboardEditItem from './DashboardEditItem'; import AnimatedAccordion from '../../Animations/AnimatedAccordion'; -import type { - ServiceCategoryType, - ServiceItemType, -} from '../../../managers/ServicesManager'; +import { ServiceCategoryType, ServiceItemType } from '../../../utils/Services'; type PropsType = { item: ServiceCategoryType; diff --git a/src/components/Lists/DashboardEdit/DashboardEditItem.tsx b/src/components/Lists/DashboardEdit/DashboardEditItem.tsx index 265b381..f1f38a3 100644 --- a/src/components/Lists/DashboardEdit/DashboardEditItem.tsx +++ b/src/components/Lists/DashboardEdit/DashboardEditItem.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Image, StyleSheet } from 'react-native'; import { List, useTheme } from 'react-native-paper'; -import type { ServiceItemType } from '../../../managers/ServicesManager'; +import { ServiceItemType } from '../../../utils/Services'; type PropsType = { item: ServiceItemType; diff --git a/src/components/Mascot/MascotPopup.tsx b/src/components/Mascot/MascotPopup.tsx index e909179..a3aa7e1 100644 --- a/src/components/Mascot/MascotPopup.tsx +++ b/src/components/Mascot/MascotPopup.tsx @@ -28,17 +28,17 @@ import { View, } from 'react-native'; import Mascot from './Mascot'; -import AsyncStorageManager from '../../managers/AsyncStorageManager'; import GENERAL_STYLES from '../../constants/Styles'; import MascotSpeechBubble, { MascotSpeechBubbleProps, } from './MascotSpeechBubble'; import { useMountEffect } from '../../utils/customHooks'; +import { useRoute } from '@react-navigation/core'; +import { useShouldShowMascot } from '../../context/preferencesContext'; type PropsType = MascotSpeechBubbleProps & { emotion: number; visible?: boolean; - prefKey?: string; }; const styles = StyleSheet.create({ @@ -61,13 +61,14 @@ const BUBBLE_HEIGHT = Dimensions.get('window').height / 3; * Component used to display a popup with the mascot. */ function MascotPopup(props: PropsType) { + const route = useRoute(); + const { shouldShow, setShouldShow } = useShouldShowMascot(route.name); + const isVisible = () => { if (props.visible !== undefined) { return props.visible; - } else if (props.prefKey != null) { - return AsyncStorageManager.getBool(props.prefKey); } else { - return false; + return shouldShow; } }; @@ -164,10 +165,8 @@ function MascotPopup(props: PropsType) { }; const onDismiss = (callback?: () => void) => { - if (props.prefKey != null) { - AsyncStorageManager.set(props.prefKey, false); - setDialogVisible(false); - } + setShouldShow(false); + setDialogVisible(false); if (callback) { callback(); } diff --git a/src/components/Overrides/CustomIntroSlider.tsx b/src/components/Overrides/CustomIntroSlider.tsx index ca2ec57..7c59b5b 100644 --- a/src/components/Overrides/CustomIntroSlider.tsx +++ b/src/components/Overrides/CustomIntroSlider.tsx @@ -32,7 +32,6 @@ import LinearGradient from 'react-native-linear-gradient'; import * as Animatable from 'react-native-animatable'; import { Card } from 'react-native-paper'; import Update from '../../constants/Update'; -import ThemeManager from '../../managers/ThemeManager'; import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot'; import MascotIntroWelcome from '../Intro/MascotIntroWelcome'; import IntroIcon from '../Intro/IconIntro'; @@ -289,9 +288,6 @@ export default class CustomIntroSlider extends React.Component< onDone = () => { const { props } = this; - CustomIntroSlider.setStatusBarColor( - ThemeManager.getCurrentTheme().colors.surface - ); props.onDone(); }; diff --git a/src/components/Screens/PlanexWebview.tsx b/src/components/Screens/PlanexWebview.tsx index 0925653..6614156 100644 --- a/src/components/Screens/PlanexWebview.tsx +++ b/src/components/Screens/PlanexWebview.tsx @@ -3,11 +3,11 @@ import { StyleSheet, View } from 'react-native'; import GENERAL_STYLES from '../../constants/Styles'; import Urls from '../../constants/Urls'; import DateManager from '../../managers/DateManager'; -import ThemeManager from '../../managers/ThemeManager'; import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen'; import ErrorView from './ErrorView'; import WebViewScreen from './WebViewScreen'; import i18n from 'i18n-js'; +import { useTheme } from 'react-native-paper'; type Props = { currentGroup?: PlanexGroupType; @@ -86,7 +86,10 @@ const INJECT_STYLE_DARK = `$('head').append('') * * @param groupID The current group selected */ -const generateInjectedJS = (group: PlanexGroupType | undefined) => { +const generateInjectedJS = ( + group: PlanexGroupType | undefined, + darkMode: boolean +) => { let customInjectedJS = `$(document).ready(function() { ${OBSERVE_MUTATIONS_INJECTED} ${INJECT_STYLE} @@ -97,7 +100,7 @@ const generateInjectedJS = (group: PlanexGroupType | undefined) => { if (DateManager.isWeekend(new Date())) { customInjectedJS += `calendar.next();`; } - if (ThemeManager.getNightMode()) { + if (darkMode) { customInjectedJS += INJECT_STYLE_DARK; } customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios @@ -105,11 +108,12 @@ const generateInjectedJS = (group: PlanexGroupType | undefined) => { }; function PlanexWebview(props: Props) { + const theme = useTheme(); return ( - ) => void; - resetPreferences: () => void; -}; - -export const PreferencesContext = React.createContext({ - preferences: defaultPreferences, - updatePreferences: () => undefined, - resetPreferences: () => undefined, -}); - -export function usePreferences() { - return useContext(PreferencesContext); -} diff --git a/src/context/preferencesContext.tsx b/src/context/preferencesContext.tsx new file mode 100644 index 0000000..6fee694 --- /dev/null +++ b/src/context/preferencesContext.tsx @@ -0,0 +1,97 @@ +import { useNavigation } from '@react-navigation/core'; +import React, { useContext } from 'react'; +import { Appearance } from 'react-native-appearance'; +import { + defaultPreferences, + getPreferenceBool, + getPreferenceObject, + isValidPreferenceKey, + PreferenceKeys, + PreferencesType, +} from '../utils/asyncStorage'; +import { + getAmicaleServices, + getINSAServices, + getSpecialServices, + getStudentServices, +} from '../utils/Services'; + +const colorScheme = Appearance.getColorScheme(); + +export type PreferencesContextType = { + preferences: PreferencesType; + updatePreferences: ( + key: PreferenceKeys, + value: number | string | boolean | object | Array + ) => void; + resetPreferences: () => void; +}; + +export const PreferencesContext = React.createContext({ + preferences: defaultPreferences, + updatePreferences: () => undefined, + resetPreferences: () => undefined, +}); + +export function usePreferences() { + return useContext(PreferencesContext); +} + +export function useShouldShowMascot(route: string) { + const { preferences, updatePreferences } = usePreferences(); + const key = route + 'ShowMascot'; + let shouldShow = false; + if (isValidPreferenceKey(key)) { + shouldShow = getPreferenceBool(key, preferences) !== false; + } + + const setShouldShow = (show: boolean) => { + if (isValidPreferenceKey(key)) { + updatePreferences(key, show); + } else { + console.log('Invalid preference key: ' + key); + } + }; + + return { shouldShow, setShouldShow }; +} + +export function useDarkTheme() { + const { preferences } = usePreferences(); + return ( + (getPreferenceBool(PreferenceKeys.nightMode, preferences) !== false && + (getPreferenceBool(PreferenceKeys.nightModeFollowSystem, preferences) === + false || + colorScheme === 'no-preference')) || + (getPreferenceBool(PreferenceKeys.nightModeFollowSystem, preferences) !== + false && + colorScheme === 'dark') + ); +} + +export function useCurrentDashboard() { + const { preferences, updatePreferences } = usePreferences(); + const navigation = useNavigation(); + const dashboardIdList = getPreferenceObject( + PreferenceKeys.dashboardItems, + preferences + ) as Array; + + const updateCurrentDashboard = (newList: Array) => { + updatePreferences(PreferenceKeys.dashboardItems, newList); + }; + + const allDatasets = [ + ...getAmicaleServices(navigation.navigate), + ...getStudentServices(navigation.navigate), + ...getINSAServices(navigation.navigate), + ...getSpecialServices(navigation.navigate), + ]; + return { + currentDashboard: allDatasets.filter((item) => + dashboardIdList.includes(item.key) + ), + currentDashboardIdList: dashboardIdList, + updateCurrentDashboard: updateCurrentDashboard, + }; +} diff --git a/src/managers/AsyncStorageManager.ts b/src/managers/AsyncStorageManager.ts deleted file mode 100644 index 8db73a0..0000000 --- a/src/managers/AsyncStorageManager.ts +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (c) 2019 - 2020 Arnaud Vergnet. - * - * This file is part of Campus INSAT. - * - * Campus INSAT is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Campus INSAT is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Campus INSAT. If not, see . - */ - -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { SERVICES_KEY } from './ServicesManager'; - -/** - * Singleton used to manage preferences. - * Preferences are fetched at the start of the app and saved in an instance object. - * This allows for a synchronous access to saved data. - */ - -export default class AsyncStorageManager { - static instance: AsyncStorageManager | null = null; - - static PREFERENCES: { [key: string]: { key: string; default: string } } = { - debugUnlocked: { - key: 'debugUnlocked', - default: '0', - }, - showIntro: { - key: 'showIntro', - default: '1', - }, - updateNumber: { - key: 'updateNumber', - default: '0', - }, - proxiwashNotifications: { - key: 'proxiwashNotifications', - default: '5', - }, - nightModeFollowSystem: { - key: 'nightModeFollowSystem', - default: '1', - }, - nightMode: { - key: 'nightMode', - default: '1', - }, - defaultStartScreen: { - key: 'defaultStartScreen', - default: 'home', - }, - servicesShowMascot: { - key: 'servicesShowMascot', - default: '1', - }, - proxiwashShowMascot: { - key: 'proxiwashShowMascot', - default: '1', - }, - homeShowMascot: { - key: 'homeShowMascot', - default: '1', - }, - eventsShowMascot: { - key: 'eventsShowMascot', - default: '1', - }, - planexShowMascot: { - key: 'planexShowMascot', - default: '1', - }, - loginShowMascot: { - key: 'loginShowMascot', - default: '1', - }, - voteShowMascot: { - key: 'voteShowMascot', - default: '1', - }, - equipmentShowMascot: { - key: 'equipmentShowMascot', - default: '1', - }, - gameStartMascot: { - key: 'gameStartMascot', - default: '1', - }, - proxiwashWatchedMachines: { - key: 'proxiwashWatchedMachines', - default: '[]', - }, - showAprilFoolsStart: { - key: 'showAprilFoolsStart', - default: '1', - }, - planexCurrentGroup: { - key: 'planexCurrentGroup', - default: '', - }, - planexFavoriteGroups: { - key: 'planexFavoriteGroups', - default: '[]', - }, - dashboardItems: { - key: 'dashboardItems', - default: JSON.stringify([ - SERVICES_KEY.EMAIL, - SERVICES_KEY.WASHERS, - SERVICES_KEY.PROXIMO, - SERVICES_KEY.TUTOR_INSA, - SERVICES_KEY.RU, - ]), - }, - gameScores: { - key: 'gameScores', - default: '[]', - }, - selectedWash: { - key: 'selectedWash', - default: 'washinsa', - }, - }; - - private currentPreferences: { [key: string]: string }; - - constructor() { - this.currentPreferences = {}; - } - - /** - * Get this class instance or create one if none is found - * @returns {AsyncStorageManager} - */ - static getInstance(): AsyncStorageManager { - if (AsyncStorageManager.instance == null) { - AsyncStorageManager.instance = new AsyncStorageManager(); - } - return AsyncStorageManager.instance; - } - - /** - * Saves the value associated to the given key to preferences. - * - * @param key - * @param value - */ - static set( - key: string, - value: number | string | boolean | object | Array - ) { - AsyncStorageManager.getInstance().setPreference(key, value); - } - - /** - * Gets the string value of the given preference - * - * @param key - * @returns {string} - */ - static getString(key: string): string { - const value = AsyncStorageManager.getInstance().getPreference(key); - return value != null ? value : ''; - } - - /** - * Gets the boolean value of the given preference - * - * @param key - * @returns {boolean} - */ - static getBool(key: string): boolean { - const value = AsyncStorageManager.getString(key); - return value === '1' || value === 'true'; - } - - /** - * Gets the number value of the given preference - * - * @param key - * @returns {number} - */ - static getNumber(key: string): number { - return parseFloat(AsyncStorageManager.getString(key)); - } - - /** - * Gets the object value of the given preference - * - * @param key - * @returns {{...}} - */ - static getObject(key: string): T { - return JSON.parse(AsyncStorageManager.getString(key)); - } - - /** - * Set preferences object current values from AsyncStorage. - * This function should be called at the app's start. - * - * @return {Promise} - */ - async loadPreferences() { - return new Promise((resolve: (val: void) => void) => { - const prefKeys: Array = []; - // Get all available keys - Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => { - prefKeys.push(key); - }); - // Get corresponding values - AsyncStorage.multiGet(prefKeys).then((resultArray) => { - // Save those values for later use - resultArray.forEach((item: [string, string | null]) => { - const key = item[0]; - let val = item[1]; - if (val === null) { - val = AsyncStorageManager.PREFERENCES[key].default; - } - this.currentPreferences[key] = val; - }); - resolve(); - }); - }); - } - - /** - * Saves the value associated to the given key to preferences. - * This updates the preferences object and saves it to AsyncStorage. - * - * @param key - * @param value - */ - setPreference( - key: string, - value: number | string | boolean | object | Array - ) { - if (AsyncStorageManager.PREFERENCES[key] != null) { - let convertedValue; - if (typeof value === 'string') { - convertedValue = value; - } else if (typeof value === 'boolean' || typeof value === 'number') { - convertedValue = value.toString(); - } else { - convertedValue = JSON.stringify(value); - } - this.currentPreferences[key] = convertedValue; - AsyncStorage.setItem(key, convertedValue); - } - } - - /** - * Gets the value at the given key. - * If the key is not available, returns null - * - * @param key - * @returns {string|null} - */ - getPreference(key: string): string | null { - return this.currentPreferences[key]; - } -} diff --git a/src/managers/DashboardManager.ts b/src/managers/DashboardManager.ts deleted file mode 100644 index 34417c7..0000000 --- a/src/managers/DashboardManager.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2019 - 2020 Arnaud Vergnet. - * - * This file is part of Campus INSAT. - * - * Campus INSAT is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Campus INSAT is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Campus INSAT. If not, see . - */ - -import type { ServiceItemType } from './ServicesManager'; -import ServicesManager from './ServicesManager'; -import { getSublistWithIds } from '../utils/Services'; -import AsyncStorageManager from './AsyncStorageManager'; - -export default class DashboardManager extends ServicesManager { - getCurrentDashboard(): Array { - const dashboardIdList = AsyncStorageManager.getObject>( - AsyncStorageManager.PREFERENCES.dashboardItems.key - ); - const allDatasets = [ - ...this.amicaleDataset, - ...this.studentsDataset, - ...this.insaDataset, - ...this.specialDataset, - ]; - return getSublistWithIds(dashboardIdList, allDatasets); - } -} diff --git a/src/managers/ServicesManager.ts b/src/managers/ServicesManager.ts deleted file mode 100644 index ff29d98..0000000 --- a/src/managers/ServicesManager.ts +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (c) 2019 - 2020 Arnaud Vergnet. - * - * This file is part of Campus INSAT. - * - * Campus INSAT is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Campus INSAT is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Campus INSAT. If not, see . - */ - -import i18n from 'i18n-js'; -import { StackNavigationProp } from '@react-navigation/stack'; -import ConnectionManager from './ConnectionManager'; -import type { FullDashboardType } from '../screens/Home/HomeScreen'; -import getStrippedServicesList from '../utils/Services'; -import Urls from '../constants/Urls'; - -const AMICALE_LOGO = require('../../assets/amicale.png'); - -export const SERVICES_KEY = { - CLUBS: 'clubs', - PROFILE: 'profile', - EQUIPMENT: 'equipment', - AMICALE_WEBSITE: 'amicale_website', - VOTE: 'vote', - PROXIMO: 'proximo', - WIKETUD: 'wiketud', - ELUS_ETUDIANTS: 'elus_etudiants', - TUTOR_INSA: 'tutor_insa', - RU: 'ru', - AVAILABLE_ROOMS: 'available_rooms', - BIB: 'bib', - EMAIL: 'email', - ENT: 'ent', - INSA_ACCOUNT: 'insa_account', - WASHERS: 'washers', - DRYERS: 'dryers', -}; - -export const SERVICES_CATEGORIES_KEY = { - AMICALE: 'amicale', - STUDENTS: 'students', - INSA: 'insa', - SPECIAL: 'special', -}; - -export type ServiceItemType = { - key: string; - title: string; - subtitle: string; - image: string | number; - onPress: () => void; - badgeFunction?: (dashboard: FullDashboardType) => number; -}; - -export type ServiceCategoryType = { - key: string; - title: string; - subtitle: string; - image: string | number; - content: Array; -}; - -export default class ServicesManager { - navigation: StackNavigationProp; - - amicaleDataset: Array; - - studentsDataset: Array; - - insaDataset: Array; - - specialDataset: Array; - - categoriesDataset: Array; - - constructor(nav: StackNavigationProp) { - this.navigation = nav; - this.amicaleDataset = [ - { - key: SERVICES_KEY.CLUBS, - title: i18n.t('screens.clubs.title'), - subtitle: i18n.t('screens.services.descriptions.clubs'), - image: Urls.images.clubs, - onPress: (): void => this.onAmicaleServicePress('club-list'), - }, - { - key: SERVICES_KEY.PROFILE, - title: i18n.t('screens.profile.title'), - subtitle: i18n.t('screens.services.descriptions.profile'), - image: Urls.images.profile, - onPress: (): void => this.onAmicaleServicePress('profile'), - }, - { - key: SERVICES_KEY.EQUIPMENT, - title: i18n.t('screens.equipment.title'), - subtitle: i18n.t('screens.services.descriptions.equipment'), - image: Urls.images.equipment, - onPress: (): void => this.onAmicaleServicePress('equipment-list'), - }, - { - key: SERVICES_KEY.AMICALE_WEBSITE, - title: i18n.t('screens.websites.amicale'), - subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'), - image: Urls.images.amicale, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.amicale, - title: i18n.t('screens.websites.amicale'), - }), - }, - { - key: SERVICES_KEY.VOTE, - title: i18n.t('screens.vote.title'), - subtitle: i18n.t('screens.services.descriptions.vote'), - image: Urls.images.vote, - onPress: (): void => this.onAmicaleServicePress('vote'), - }, - ]; - this.studentsDataset = [ - { - key: SERVICES_KEY.PROXIMO, - title: i18n.t('screens.proximo.title'), - subtitle: i18n.t('screens.services.descriptions.proximo'), - image: Urls.images.proximo, - onPress: (): void => nav.navigate('proximo'), - badgeFunction: (dashboard: FullDashboardType): number => - dashboard.proximo_articles, - }, - { - key: SERVICES_KEY.WIKETUD, - title: 'Wiketud', - subtitle: i18n.t('screens.services.descriptions.wiketud'), - image: Urls.images.wiketud, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.wiketud, - title: 'Wiketud', - }), - }, - { - key: SERVICES_KEY.ELUS_ETUDIANTS, - title: 'Élus Étudiants', - subtitle: i18n.t('screens.services.descriptions.elusEtudiants'), - image: Urls.images.elusEtudiants, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.elusEtudiants, - title: 'Élus Étudiants', - }), - }, - { - key: SERVICES_KEY.TUTOR_INSA, - title: "Tutor'INSA", - subtitle: i18n.t('screens.services.descriptions.tutorInsa'), - image: Urls.images.tutorInsa, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.tutorInsa, - title: "Tutor'INSA", - }), - badgeFunction: (dashboard: FullDashboardType): number => - dashboard.available_tutorials, - }, - ]; - this.insaDataset = [ - { - key: SERVICES_KEY.RU, - title: i18n.t('screens.menu.title'), - subtitle: i18n.t('screens.services.descriptions.self'), - image: Urls.images.menu, - onPress: (): void => nav.navigate('self-menu'), - badgeFunction: (dashboard: FullDashboardType): number => - dashboard.today_menu.length, - }, - { - key: SERVICES_KEY.AVAILABLE_ROOMS, - title: i18n.t('screens.websites.rooms'), - subtitle: i18n.t('screens.services.descriptions.availableRooms'), - image: Urls.images.availableRooms, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.availableRooms, - title: i18n.t('screens.websites.rooms'), - }), - }, - { - key: SERVICES_KEY.BIB, - title: i18n.t('screens.websites.bib'), - subtitle: i18n.t('screens.services.descriptions.bib'), - image: Urls.images.bib, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.bib, - title: i18n.t('screens.websites.bib'), - }), - }, - { - key: SERVICES_KEY.EMAIL, - title: i18n.t('screens.websites.mails'), - subtitle: i18n.t('screens.services.descriptions.mails'), - image: Urls.images.bluemind, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.bluemind, - title: i18n.t('screens.websites.mails'), - }), - }, - { - key: SERVICES_KEY.ENT, - title: i18n.t('screens.websites.ent'), - subtitle: i18n.t('screens.services.descriptions.ent'), - image: Urls.images.ent, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.ent, - title: i18n.t('screens.websites.ent'), - }), - }, - { - key: SERVICES_KEY.INSA_ACCOUNT, - title: i18n.t('screens.insaAccount.title'), - subtitle: i18n.t('screens.services.descriptions.insaAccount'), - image: Urls.images.insaAccount, - onPress: (): void => - nav.navigate('website', { - host: Urls.websites.insaAccount, - title: i18n.t('screens.insaAccount.title'), - }), - }, - ]; - this.specialDataset = [ - { - key: SERVICES_KEY.WASHERS, - title: i18n.t('screens.proxiwash.washers'), - subtitle: i18n.t('screens.services.descriptions.washers'), - image: Urls.images.washer, - onPress: (): void => nav.navigate('proxiwash'), - badgeFunction: (dashboard: FullDashboardType): number => - dashboard.available_washers, - }, - { - key: SERVICES_KEY.DRYERS, - title: i18n.t('screens.proxiwash.dryers'), - subtitle: i18n.t('screens.services.descriptions.washers'), - image: Urls.images.dryer, - onPress: (): void => nav.navigate('proxiwash'), - badgeFunction: (dashboard: FullDashboardType): number => - dashboard.available_dryers, - }, - ]; - this.categoriesDataset = [ - { - key: SERVICES_CATEGORIES_KEY.AMICALE, - title: i18n.t('screens.services.categories.amicale'), - subtitle: i18n.t('screens.services.more'), - image: AMICALE_LOGO, - content: this.amicaleDataset, - }, - { - key: SERVICES_CATEGORIES_KEY.STUDENTS, - title: i18n.t('screens.services.categories.students'), - subtitle: i18n.t('screens.services.more'), - image: 'account-group', - content: this.studentsDataset, - }, - { - key: SERVICES_CATEGORIES_KEY.INSA, - title: i18n.t('screens.services.categories.insa'), - subtitle: i18n.t('screens.services.more'), - image: 'school', - content: this.insaDataset, - }, - { - key: SERVICES_CATEGORIES_KEY.SPECIAL, - title: i18n.t('screens.services.categories.special'), - subtitle: i18n.t('screens.services.categories.special'), - image: 'star', - content: this.specialDataset, - }, - ]; - } - - /** - * Redirects the user to the login screen if he is not logged in - * - * @param route - * @returns {null} - */ - onAmicaleServicePress(route: string) { - if (ConnectionManager.getInstance().isLoggedIn()) { - this.navigation.navigate(route); - } else { - this.navigation.navigate('login', { nextScreen: route }); - } - } - - /** - * Gets the list of amicale's services - * - * @param excludedItems Ids of items to exclude from the returned list - * @returns {Array} - */ - getAmicaleServices(excludedItems?: Array): Array { - if (excludedItems != null) { - return getStrippedServicesList(excludedItems, this.amicaleDataset); - } - return this.amicaleDataset; - } - - /** - * Gets the list of students' services - * - * @param excludedItems Ids of items to exclude from the returned list - * @returns {Array} - */ - getStudentServices(excludedItems?: Array): Array { - if (excludedItems != null) { - return getStrippedServicesList(excludedItems, this.studentsDataset); - } - return this.studentsDataset; - } - - /** - * Gets the list of INSA's services - * - * @param excludedItems Ids of items to exclude from the returned list - * @returns {Array} - */ - getINSAServices(excludedItems?: Array): Array { - if (excludedItems != null) { - return getStrippedServicesList(excludedItems, this.insaDataset); - } - return this.insaDataset; - } - - /** - * Gets the list of special services - * - * @param excludedItems Ids of items to exclude from the returned list - * @returns {Array} - */ - getSpecialServices(excludedItems?: Array): Array { - if (excludedItems != null) { - return getStrippedServicesList(excludedItems, this.specialDataset); - } - return this.specialDataset; - } - - /** - * Gets all services sorted by category - * - * @param excludedItems Ids of categories to exclude from the returned list - * @returns {Array} - */ - getCategories(excludedItems?: Array): Array { - if (excludedItems != null) { - return getStrippedServicesList(excludedItems, this.categoriesDataset); - } - return this.categoriesDataset; - } -} diff --git a/src/navigation/MainNavigator.tsx b/src/navigation/MainNavigator.tsx index 9bfcd5b..7f9938d 100644 --- a/src/navigation/MainNavigator.tsx +++ b/src/navigation/MainNavigator.tsx @@ -46,16 +46,20 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen'; +import { usePreferences } from '../context/preferencesContext'; +import { getPreferenceBool, PreferenceKeys } from '../utils/asyncStorage'; +import IntroScreen from '../screens/Intro/IntroScreen'; export enum MainRoutes { Main = 'main', + Intro = 'Intro', Gallery = 'gallery', Settings = 'settings', DashboardEdit = 'dashboard-edit', About = 'about', Dependencies = 'dependencies', Debug = 'debug', - GameStart = 'game-start', + GameStart = 'game', GameMain = 'game-main', Login = 'login', SelfMenu = 'self-menu', @@ -66,11 +70,12 @@ export enum MainRoutes { ClubList = 'club-list', ClubInformation = 'club-information', ClubAbout = 'club-about', - EquipmentList = 'equipment-list', + EquipmentList = 'equipment', EquipmentRent = 'equipment-rent', EquipmentConfirm = 'equipment-confirm', Vote = 'vote', Feedback = 'feedback', + Website = 'website', } type DefaultParams = { [key in MainRoutes]: object | undefined }; @@ -96,13 +101,31 @@ export type MainStackParamsList = FullParamsList & const MainStack = createStackNavigator(); +function getIntroScreens() { + return ( + <> + + + ); +} + function MainStackComponent(props: { + showIntro: boolean; createTabNavigator: () => React.ReactElement; }) { - const { createTabNavigator } = props; + const { showIntro, createTabNavigator } = props; + if (showIntro) { + return getIntroScreens(); + } return ( } /> ); diff --git a/src/navigation/TabNavigator.tsx b/src/navigation/TabNavigator.tsx index 437a380..c5cc56e 100644 --- a/src/navigation/TabNavigator.tsx +++ b/src/navigation/TabNavigator.tsx @@ -31,7 +31,6 @@ import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen'; import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen'; import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen'; -import AsyncStorageManager from '../managers/AsyncStorageManager'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ScannerScreen from '../screens/Home/ScannerScreen'; import FeedItemScreen from '../screens/Home/FeedItemScreen'; @@ -41,6 +40,8 @@ import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot'; +import { usePreferences } from '../context/preferencesContext'; +import { getPreferenceString, PreferenceKeys } from '../utils/asyncStorage'; const styles = StyleSheet.create({ header: { @@ -56,6 +57,20 @@ const styles = StyleSheet.create({ }, }); +type DefaultParams = { [key in TabRoutes]: object | undefined }; + +export type FullParamsList = DefaultParams & { + [TabRoutes.Home]: { + nextScreen: string; + data: Record; + }; +}; + +// Don't know why but TS is complaining without this +// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined +export type TabStackParamsList = FullParamsList & + Record; + const ServicesStack = createStackNavigator(); function ServicesStackComponent() { @@ -214,7 +229,7 @@ function PlanexStackComponent() { ); } -const Tab = createBottomTabNavigator(); +const Tab = createBottomTabNavigator(); type PropsType = { defaultHomeRoute: string | null; @@ -249,65 +264,70 @@ const ICONS: { }, }; -export default class TabNavigator extends React.Component { - defaultRoute: string; - createHomeStackComponent: () => any; - - constructor(props: PropsType) { - super(props); - this.defaultRoute = 'home'; - if (!props.defaultHomeRoute) { - this.defaultRoute = AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.defaultStartScreen.key - ).toLowerCase(); - } - this.createHomeStackComponent = () => - HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData); +export default function TabNavigator(props: PropsType) { + const { preferences } = usePreferences(); + let defaultRoute = getPreferenceString( + PreferenceKeys.defaultStartScreen, + preferences + ); + if (!defaultRoute) { + defaultRoute = 'home'; + } else { + defaultRoute = defaultRoute.toLowerCase(); } - render() { - const LABELS: { - [key: string]: string; - } = { - services: i18n.t('screens.services.title'), - proxiwash: i18n.t('screens.proxiwash.title'), - home: i18n.t('screens.home.title'), - planning: i18n.t('screens.planning.title'), - planex: i18n.t('screens.planex.title'), - }; - return ( - ( - - )} - > - - - - - - - ); - } + const createHomeStackComponent = () => + HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData); + + const LABELS: { + [key: string]: string; + } = { + services: i18n.t('screens.services.title'), + proxiwash: i18n.t('screens.proxiwash.title'), + home: i18n.t('screens.home.title'), + planning: i18n.t('screens.planning.title'), + planex: i18n.t('screens.planex.title'), + }; + return ( + ( + + )} + > + + + + + + + ); +} + +export enum TabRoutes { + Services = 'services', + Proxiwash = 'proxiwash', + Home = 'home', + Planning = 'events', + Planex = 'planex', } diff --git a/src/screens/About/DebugScreen.tsx b/src/screens/About/DebugScreen.tsx index c66a7a4..10ce98c 100644 --- a/src/screens/About/DebugScreen.tsx +++ b/src/screens/About/DebugScreen.tsx @@ -17,7 +17,7 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useRef, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { Button, @@ -25,12 +25,17 @@ import { Subheading, TextInput, Title, - withTheme, + useTheme, } from 'react-native-paper'; import { Modalize } from 'react-native-modalize'; import CustomModal from '../../components/Overrides/CustomModal'; -import AsyncStorageManager from '../../managers/AsyncStorageManager'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; +import { usePreferences } from '../../context/preferencesContext'; +import { + defaultPreferences, + isValidPreferenceKey, + PreferenceKeys, +} from '../../utils/asyncStorage'; type PreferenceItemType = { key: string; @@ -38,15 +43,6 @@ type PreferenceItemType = { current: string; }; -type PropsType = { - theme: ReactNativePaper.Theme; -}; - -type StateType = { - modalCurrentDisplayItem: PreferenceItemType | null; - currentPreferences: Array; -}; - const styles = StyleSheet.create({ container: { flex: 1, @@ -62,47 +58,35 @@ const styles = StyleSheet.create({ * Class defining the Debug screen. * This screen allows the user to get and modify information on the app/device. */ -class DebugScreen extends React.Component { - modalRef: { current: Modalize | null }; +function DebugScreen() { + const theme = useTheme(); + const { preferences, updatePreferences } = usePreferences(); + const modalRef = useRef(null); - modalInputValue: string; + const [modalInputValue, setModalInputValue] = useState(''); + const [ + modalCurrentDisplayItem, + setModalCurrentDisplayItem, + ] = useState(null); - /** - * Copies user preferences to state for easier manipulation - * - * @param props - */ - constructor(props: PropsType) { - super(props); - this.modalRef = React.createRef(); - this.modalInputValue = ''; - const currentPreferences: Array = []; - Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => { - const newObject: PreferenceItemType = { ...object }; - newObject.current = AsyncStorageManager.getString(newObject.key); - currentPreferences.push(newObject); - }); - this.state = { - modalCurrentDisplayItem: null, - currentPreferences, + const currentPreferences: Array = []; + Object.values(PreferenceKeys).forEach((key) => { + const newObject: PreferenceItemType = { + key: key, + current: preferences[key], + default: defaultPreferences[key], }; - } + currentPreferences.push(newObject); + }); - /** - * Gets the edit modal content - * - * @return {*} - */ - getModalContent() { - const { props, state } = this; + const getModalContent = () => { let key = ''; let defaultValue = ''; let current = ''; - if (state.modalCurrentDisplayItem) { - key = state.modalCurrentDisplayItem.key; - defaultValue = state.modalCurrentDisplayItem.default; - defaultValue = state.modalCurrentDisplayItem.default; - current = state.modalCurrentDisplayItem.current; + if (modalCurrentDisplayItem) { + key = modalCurrentDisplayItem.key; + defaultValue = modalCurrentDisplayItem.default; + current = modalCurrentDisplayItem.current; } return ( @@ -110,19 +94,14 @@ class DebugScreen extends React.Component { {key} Default: {defaultValue} Current: {current} - { - this.modalInputValue = text; - }} - /> + - {this.getDashboard(currentDashboard)} + {getDashboard(currentDashboard)} @@ -158,43 +134,28 @@ class DashboardEditScreen extends React.Component { ); - } + }; - updateDashboard = (service: ServiceItemType) => { - const { currentDashboard, currentDashboardIdList, activeItem } = this.state; - currentDashboard[activeItem] = service; - currentDashboardIdList[activeItem] = service.key; - this.setState({ - currentDashboard, - currentDashboardIdList, - }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.dashboardItems.key, - currentDashboardIdList + const updateDashboard = (service: ServiceItemType) => { + updateCurrentDashboard( + currentDashboardIdList.map((id, index) => + index === activeItem ? service.key : id + ) ); }; - undoDashboard = () => { - this.setState({ - currentDashboard: [...this.initialDashboard], - currentDashboardIdList: [...this.initialDashboardIdList], - }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.dashboardItems.key, - this.initialDashboardIdList - ); + const undoDashboard = () => { + updateCurrentDashboard(initialDashboard.current); }; - render() { - return ( - - ); - } + return ( + + ); } export default DashboardEditScreen; diff --git a/src/screens/Other/Settings/SettingsScreen.tsx b/src/screens/Other/Settings/SettingsScreen.tsx index 631fef3..4a70e4d 100644 --- a/src/screens/Other/Settings/SettingsScreen.tsx +++ b/src/screens/Other/Settings/SettingsScreen.tsx @@ -26,28 +26,20 @@ import { List, Switch, ToggleButton, - withTheme, + useTheme, } from 'react-native-paper'; import { Appearance } from 'react-native-appearance'; -import { StackNavigationProp } from '@react-navigation/stack'; -import ThemeManager from '../../../managers/ThemeManager'; -import AsyncStorageManager from '../../../managers/AsyncStorageManager'; import CustomSlider from '../../../components/Overrides/CustomSlider'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import GENERAL_STYLES from '../../../constants/Styles'; - -type PropsType = { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; -}; - -type StateType = { - nightMode: boolean; - nightModeFollowSystem: boolean; - startScreenPickerSelected: string; - selectedWash: string; - isDebugUnlocked: boolean; -}; +import { usePreferences } from '../../../context/preferencesContext'; +import { useNavigation } from '@react-navigation/core'; +import { + getPreferenceBool, + getPreferenceNumber, + getPreferenceString, + PreferenceKeys, +} from '../../../utils/asyncStorage'; const styles = StyleSheet.create({ slider: { @@ -66,98 +58,67 @@ const styles = StyleSheet.create({ /** * Class defining the Settings screen. This screen shows controls to modify app preferences. */ -class SettingsScreen extends React.Component { - savedNotificationReminder: number; +function SettingsScreen() { + const navigation = useNavigation(); + const theme = useTheme(); + const { preferences, updatePreferences } = usePreferences(); - /** - * Loads user preferences into state - */ - constructor(props: PropsType) { - super(props); - const notifReminder = AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.proxiwashNotifications.key - ); - this.savedNotificationReminder = parseInt(notifReminder, 10); - if (Number.isNaN(this.savedNotificationReminder)) { - this.savedNotificationReminder = 0; - } + const nightMode = getPreferenceBool( + PreferenceKeys.nightMode, + preferences + ) as boolean; + const nightModeFollowSystem = + (getPreferenceBool( + PreferenceKeys.nightModeFollowSystem, + preferences + ) as boolean) && Appearance.getColorScheme() !== 'no-preference'; + const startScreenPickerSelected = getPreferenceString( + PreferenceKeys.defaultStartScreen, + preferences + ) as string; + const selectedWash = getPreferenceString( + PreferenceKeys.selectedWash, + preferences + ) as string; + const isDebugUnlocked = getPreferenceBool( + PreferenceKeys.debugUnlocked, + preferences + ) as boolean; + const notif = getPreferenceNumber( + PreferenceKeys.proxiwashNotifications, + preferences + ); + const savedNotificationReminder = !notif || Number.isNaN(notif) ? 0 : notif; - this.state = { - nightMode: ThemeManager.getNightMode(), - nightModeFollowSystem: - AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key - ) && Appearance.getColorScheme() !== 'no-preference', - startScreenPickerSelected: AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.defaultStartScreen.key - ), - selectedWash: AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.selectedWash.key - ), - isDebugUnlocked: AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.debugUnlocked.key - ), - }; - } - - /** - * Saves the value for the proxiwash reminder notification time - * - * @param value The value to store - */ - onProxiwashNotifPickerValueChange = (value: number) => { - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.proxiwashNotifications.key, - value - ); + const onProxiwashNotifPickerValueChange = (value: number) => { + updatePreferences(PreferenceKeys.proxiwashNotifications, value); }; - /** - * Saves the value for the proxiwash reminder notification time - * - * @param value The value to store - */ - onStartScreenPickerValueChange = (value: string) => { + const onStartScreenPickerValueChange = (value: string) => { if (value != null) { - this.setState({ startScreenPickerSelected: value }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.defaultStartScreen.key, - value - ); + updatePreferences(PreferenceKeys.defaultStartScreen, value); } }; - /** - * Returns a picker allowing the user to select the proxiwash reminder notification time - * - * @returns {React.Node} - */ - getProxiwashNotifPicker() { - const { theme } = this.props; + const getProxiwashNotifPicker = () => { return ( ); - } + }; - /** - * Returns a radio picker allowing the user to select the proxiwash - * - * @returns {React.Node} - */ - getProxiwashChangePicker() { - const { selectedWash } = this.state; + const getProxiwashChangePicker = () => { return ( { /> ); - } + }; - /** - * Returns a picker allowing the user to select the start screen - * - * @returns {React.Node} - */ - getStartScreenPicker() { - const { startScreenPickerSelected } = this.state; + const getStartScreenPicker = () => { return ( @@ -192,30 +147,17 @@ class SettingsScreen extends React.Component { ); - } - - /** - * Toggles night mode and saves it to preferences - */ - onToggleNightMode = () => { - const { nightMode } = this.state; - ThemeManager.getInstance().setNightMode(!nightMode); - this.setState({ nightMode: !nightMode }); }; - onToggleNightModeFollowSystem = () => { - const { nightModeFollowSystem } = this.state; - const value = !nightModeFollowSystem; - this.setState({ nightModeFollowSystem: value }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key, - value + const onToggleNightMode = () => { + updatePreferences(PreferenceKeys.nightMode, !nightMode); + }; + + const onToggleNightModeFollowSystem = () => { + updatePreferences( + PreferenceKeys.nightModeFollowSystem, + !nightModeFollowSystem ); - if (value) { - const nightMode = Appearance.getColorScheme() === 'dark'; - ThemeManager.getInstance().setNightMode(nightMode); - this.setState({ nightMode }); - } }; /** @@ -228,13 +170,13 @@ class SettingsScreen extends React.Component { * @param state The current state of the switch * @returns {React.Node} */ - static getToggleItem( + const getToggleItem = ( onPressCallback: () => void, icon: string, title: string, subtitle: string, state: boolean - ) { + ) => { return ( { right={() => } /> ); - } + }; - getNavigateItem( + const getNavigateItem = ( route: string, icon: string, title: string, subtitle: string, onLongPress?: () => void - ) { - const { navigation } = this.props; + ) => { return ( { onLongPress={onLongPress} /> ); - } + }; - /** - * Saves the value for the proxiwash selected wash - * - * @param value The value to store - */ - onSelectWashValueChange = (value: string) => { + const onSelectWashValueChange = (value: string) => { if (value != null) { - this.setState({ selectedWash: value }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.selectedWash.key, - value - ); + updatePreferences(PreferenceKeys.selectedWash, value); } }; - /** - * Unlocks debug mode and saves its state to user preferences - */ - unlockDebugMode = () => { - this.setState({ isDebugUnlocked: true }); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.debugUnlocked.key, - true - ); + const unlockDebugMode = () => { + updatePreferences(PreferenceKeys.debugUnlocked, true); }; - render() { - const { nightModeFollowSystem, nightMode, isDebugUnlocked } = this.state; - return ( - - - - - {Appearance.getColorScheme() !== 'no-preference' - ? SettingsScreen.getToggleItem( - this.onToggleNightModeFollowSystem, - 'theme-light-dark', - i18n.t('screens.settings.nightModeAuto'), - i18n.t('screens.settings.nightModeAutoSub'), - nightModeFollowSystem - ) - : null} - {Appearance.getColorScheme() === 'no-preference' || - !nightModeFollowSystem - ? SettingsScreen.getToggleItem( - this.onToggleNightMode, - 'theme-light-dark', - i18n.t('screens.settings.nightMode'), - nightMode - ? i18n.t('screens.settings.nightModeSubOn') - : i18n.t('screens.settings.nightModeSubOff'), - nightMode - ) - : null} - ( - - )} - /> - {this.getStartScreenPicker()} - {this.getNavigateItem( - 'dashboard-edit', - 'view-dashboard', - i18n.t('screens.settings.dashboard'), - i18n.t('screens.settings.dashboardSub') + return ( + + + + + {Appearance.getColorScheme() !== 'no-preference' + ? getToggleItem( + onToggleNightModeFollowSystem, + 'theme-light-dark', + i18n.t('screens.settings.nightModeAuto'), + i18n.t('screens.settings.nightModeAutoSub'), + nightModeFollowSystem + ) + : null} + {Appearance.getColorScheme() === 'no-preference' || + !nightModeFollowSystem + ? getToggleItem( + onToggleNightMode, + 'theme-light-dark', + i18n.t('screens.settings.nightMode'), + nightMode + ? i18n.t('screens.settings.nightModeSubOn') + : i18n.t('screens.settings.nightModeSubOff'), + nightMode + ) + : null} + ( + )} - - - - - - ( - - )} - /> - - {this.getProxiwashNotifPicker()} - - ( - - )} - /> - - {this.getProxiwashChangePicker()} - - - - - - - {isDebugUnlocked - ? this.getNavigateItem( - 'debug', - 'bug-check', - i18n.t('screens.debug.title'), - '' - ) - : null} - {this.getNavigateItem( - 'about', - 'information', - i18n.t('screens.about.title'), - i18n.t('screens.about.buttonDesc'), - this.unlockDebugMode + /> + {getStartScreenPicker()} + {getNavigateItem( + 'dashboard-edit', + 'view-dashboard', + i18n.t('screens.settings.dashboard'), + i18n.t('screens.settings.dashboardSub') + )} + + + + + + ( + )} - {this.getNavigateItem( - 'feedback', - 'comment-quote', - i18n.t('screens.feedback.homeButtonTitle'), - i18n.t('screens.feedback.homeButtonSubtitle') + /> + + {getProxiwashNotifPicker()} + + ( + )} - - - - ); - } + /> + + {getProxiwashChangePicker()} + + + + + + + {isDebugUnlocked + ? getNavigateItem( + 'debug', + 'bug-check', + i18n.t('screens.debug.title'), + '' + ) + : null} + {getNavigateItem( + 'about', + 'information', + i18n.t('screens.about.title'), + i18n.t('screens.about.buttonDesc'), + unlockDebugMode + )} + {getNavigateItem( + 'feedback', + 'comment-quote', + i18n.t('screens.feedback.homeButtonTitle'), + i18n.t('screens.feedback.homeButtonSubtitle') + )} + + + + ); } -export default withTheme(SettingsScreen); +export default SettingsScreen; diff --git a/src/screens/Planex/GroupSelectionScreen.tsx b/src/screens/Planex/GroupSelectionScreen.tsx index 73ee8cd..b41de5a 100644 --- a/src/screens/Planex/GroupSelectionScreen.tsx +++ b/src/screens/Planex/GroupSelectionScreen.tsx @@ -17,23 +17,19 @@ * along with Campus INSAT. If not, see . */ -import React, { - useCallback, - useEffect, - useLayoutEffect, - useState, -} from 'react'; +import React, { useCallback, useLayoutEffect, useState } from 'react'; import { Platform } from 'react-native'; import i18n from 'i18n-js'; import { Searchbar } from 'react-native-paper'; import { stringMatchQuery } from '../../utils/Search'; import WebSectionList from '../../components/Screens/WebSectionList'; import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion'; -import AsyncStorageManager from '../../managers/AsyncStorageManager'; import Urls from '../../constants/Urls'; import { readData } from '../../utils/WebData'; import { useNavigation } from '@react-navigation/core'; import { useCachedPlanexGroups } from '../../context/cacheContext'; +import { usePreferences } from '../../context/preferencesContext'; +import { getPreferenceObject, PreferenceKeys } from '../../utils/asyncStorage'; export type PlanexGroupType = { name: string; @@ -63,13 +59,23 @@ function sortName( function GroupSelectionScreen() { const navigation = useNavigation(); + const { preferences, updatePreferences } = usePreferences(); const { groups, setGroups } = useCachedPlanexGroups(); const [currentSearchString, setCurrentSearchString] = useState(''); - const [favoriteGroups, setFavoriteGroups] = useState>( - AsyncStorageManager.getObject( - AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key - ) - ); + + const getFavoriteGroups = (): Array => { + const data = getPreferenceObject( + PreferenceKeys.planexFavoriteGroups, + preferences + ); + if (data) { + return data as Array; + } else { + return []; + } + }; + + const favoriteGroups = getFavoriteGroups(); useLayoutEffect(() => { navigation.setOptions({ @@ -140,10 +146,8 @@ function GroupSelectionScreen() { * @param item The article pressed */ const onListItemPress = (item: PlanexGroupType) => { - navigation.navigate('planex', { - screen: 'index', - params: { group: item }, - }); + updatePreferences(PreferenceKeys.planexCurrentGroup, item); + navigation.goBack(); }; /** @@ -153,12 +157,16 @@ function GroupSelectionScreen() { */ const onListFavoritePress = useCallback( (group: PlanexGroupType) => { + const updateFavorites = (newValue: Array) => { + updatePreferences(PreferenceKeys.planexFavoriteGroups, newValue); + }; + const removeGroupFromFavorites = (g: PlanexGroupType) => { - setFavoriteGroups(favoriteGroups.filter((f) => f.id !== g.id)); + updateFavorites(favoriteGroups.filter((f) => f.id !== g.id)); }; const addGroupToFavorites = (g: PlanexGroupType) => { - setFavoriteGroups([...favoriteGroups, g].sort(sortName)); + updateFavorites([...favoriteGroups, g].sort(sortName)); }; if (favoriteGroups.some((f) => f.id === group.id)) { @@ -167,16 +175,9 @@ function GroupSelectionScreen() { addGroupToFavorites(group); } }, - [favoriteGroups] + [favoriteGroups, updatePreferences] ); - useEffect(() => { - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key, - favoriteGroups - ); - }, [favoriteGroups]); - /** * Generates the dataset to be used in the FlatList. * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top. diff --git a/src/screens/Planex/PlanexScreen.tsx b/src/screens/Planex/PlanexScreen.tsx index 2c3e62e..855e0d5 100644 --- a/src/screens/Planex/PlanexScreen.tsx +++ b/src/screens/Planex/PlanexScreen.tsx @@ -17,17 +17,12 @@ * along with Campus INSAT. If not, see . */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Title, useTheme } from 'react-native-paper'; import i18n from 'i18n-js'; import { StyleSheet, View } from 'react-native'; -import { - CommonActions, - useFocusEffect, - useNavigation, -} from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import Autolink from 'react-native-autolink'; -import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AlertDialog from '../../components/Dialogs/AlertDialog'; import { dateToString, getTimeOnlyString } from '../../utils/Planning'; import DateManager from '../../managers/DateManager'; @@ -38,6 +33,8 @@ import { getPrettierPlanexGroupName } from '../../utils/Utils'; import GENERAL_STYLES from '../../constants/Styles'; import PlanexWebview from '../../components/Screens/PlanexWebview'; import PlanexBottomBar from '../../components/Animations/PlanexBottomBar'; +import { usePreferences } from '../../context/preferencesContext'; +import { getPreferenceString, PreferenceKeys } from '../../utils/asyncStorage'; const styles = StyleSheet.create({ container: { @@ -50,17 +47,10 @@ const styles = StyleSheet.create({ }, }); -type Props = { - route: { - params: { - group?: PlanexGroupType; - }; - }; -}; - -function PlanexScreen(props: Props) { +function PlanexScreen() { const navigation = useNavigation(); const theme = useTheme(); + const { preferences } = usePreferences(); const [dialogContent, setDialogContent] = useState< | undefined @@ -72,12 +62,13 @@ function PlanexScreen(props: Props) { >(); const [injectJS, setInjectJS] = useState(''); - const getCurrentGroup = (): PlanexGroupType | undefined => { - let currentGroupString = AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.planexCurrentGroup.key + const getCurrentGroup: () => PlanexGroupType | undefined = useCallback(() => { + let currentGroupString = getPreferenceString( + PreferenceKeys.planexCurrentGroup, + preferences ); let group: PlanexGroupType; - if (currentGroupString !== '') { + if (currentGroupString) { group = JSON.parse(currentGroupString); navigation.setOptions({ title: getPrettierPlanexGroupName(group.name), @@ -85,22 +76,10 @@ function PlanexScreen(props: Props) { return group; } return undefined; - }; + }, [navigation, preferences]); - const [currentGroup, setCurrentGroup] = useState( - getCurrentGroup() - ); + const currentGroup = getCurrentGroup(); - useFocusEffect( - useCallback(() => { - if (props.route.params?.group) { - // reset params to prevent infinite loop - selectNewGroup(props.route.params.group); - navigation.dispatch(CommonActions.setParams({ group: undefined })); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.route.params]) - ); /** * Gets the Webview, with an error view on top if no group is selected. * @@ -194,21 +173,20 @@ function PlanexScreen(props: Props) { const hideDialog = () => setDialogContent(undefined); - /** - * Sends the webpage a message with the new group to select and save it to preferences - * - * @param group The group object selected - */ - const selectNewGroup = (group: PlanexGroupType) => { - sendMessage('setGroup', group.id.toString()); - setCurrentGroup(group); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, - group - ); + useEffect(() => { + const group = getCurrentGroup(); + if (group) { + sendMessage('setGroup', group.id.toString()); + navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getCurrentGroup, navigation]); - navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); - }; + const showMascot = + getPreferenceString( + PreferenceKeys.defaultStartScreen, + preferences + )?.toLowerCase() !== 'planex'; return ( @@ -220,11 +198,8 @@ function PlanexScreen(props: Props) { {getWebView()} )} - {AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.defaultStartScreen.key - ).toLowerCase() !== 'planex' ? ( + {showMascot ? ( { } /> . */ -import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { SectionListData, SectionListRenderItemInfo, @@ -28,7 +28,6 @@ import i18n from 'i18n-js'; import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper'; import { Modalize } from 'react-native-modalize'; import WebSectionList from '../../components/Screens/WebSectionList'; -import AsyncStorageManager from '../../managers/AsyncStorageManager'; import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem'; import ProxiwashConstants, { MachineStates, @@ -50,9 +49,16 @@ import type { SectionListDataType } from '../../components/Screens/WebSectionLis import type { LaundromatType } from './ProxiwashAboutScreen'; import GENERAL_STYLES from '../../constants/Styles'; import { readData } from '../../utils/WebData'; -import { useFocusEffect, useNavigation } from '@react-navigation/core'; +import { useNavigation } from '@react-navigation/core'; import { setupMachineNotification } from '../../utils/Notifications'; import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader'; +import { usePreferences } from '../../context/preferencesContext'; +import { + getPreferenceNumber, + getPreferenceObject, + getPreferenceString, + PreferenceKeys, +} from '../../utils/asyncStorage'; const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const LIST_ITEM_HEIGHT = 64; @@ -91,23 +97,35 @@ const styles = StyleSheet.create({ function ProxiwashScreen() { const navigation = useNavigation(); const theme = useTheme(); + const { preferences, updatePreferences } = usePreferences(); const [ modalCurrentDisplayItem, setModalCurrentDisplayItem, ] = useState(null); - const [machinesWatched, setMachinesWatched] = useState< - Array - >( - AsyncStorageManager.getObject( - AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key - ) + const reminder = getPreferenceNumber( + PreferenceKeys.proxiwashNotifications, + preferences ); - const [selectedWash, setSelectedWash] = useState( - AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.selectedWash.key - ) as 'tripodeB' | 'washinsa' - ); + const getMachinesWatched = () => { + const data = getPreferenceObject( + PreferenceKeys.proxiwashWatchedMachines, + preferences + ) as Array; + return data ? (data as Array) : []; + }; + + const getSelectedWash = () => { + const data = getPreferenceString(PreferenceKeys.selectedWash, preferences); + if (data !== 'washinsa' && data !== 'tripodeB') { + return 'washinsa'; + } else { + return data; + } + }; + + const machinesWatched: Array = getMachinesWatched(); + const selectedWash: 'washinsa' | 'tripodeB' = getSelectedWash(); const modalStateStrings: { [key in MachineStates]: string } = { [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'), @@ -137,17 +155,6 @@ function ProxiwashScreen() { }); }, [navigation]); - useFocusEffect( - useCallback(() => { - const selected = AsyncStorageManager.getString( - AsyncStorageManager.PREFERENCES.selectedWash.key - ) as 'tripodeB' | 'washinsa'; - if (selected !== selectedWash) { - setSelectedWash(selected); - } - }, [selectedWash]) - ); - /** * Callback used when the user clicks on enable notifications for a machine * @@ -293,6 +300,7 @@ function ProxiwashScreen() { setupMachineNotification( machine.number, true, + reminder, getMachineEndDate(machine) ); saveNotificationToState(machine); @@ -342,7 +350,7 @@ function ProxiwashScreen() { ...data.washers, ]); if (cleanedList !== machinesWatched) { - setMachinesWatched(machinesWatched); + updatePreferences(PreferenceKeys.proxiwashWatchedMachines, cleanedList); } return [ { @@ -407,11 +415,7 @@ function ProxiwashScreen() { }; const saveNewWatchedList = (list: Array) => { - setMachinesWatched(list); - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key, - list - ); + updatePreferences(PreferenceKeys.proxiwashWatchedMachines, list); }; const renderListHeaderComponent = ( @@ -451,7 +455,6 @@ function ProxiwashScreen() { /> ; @@ -66,8 +66,7 @@ class ServicesScreen extends React.Component { constructor(props: PropsType) { super(props); - const services = new ServicesManager(props.navigation); - this.finalDataset = services.getCategories([ + this.finalDataset = getCategories(props.navigation.navigate, [ SERVICES_CATEGORIES_KEY.SPECIAL, ]); } @@ -159,7 +158,6 @@ class ServicesScreen extends React.Component { hasTab /> ; diff --git a/src/utils/Notifications.ts b/src/utils/Notifications.ts index 17a441a..3ef0f26 100644 --- a/src/utils/Notifications.ts +++ b/src/utils/Notifications.ts @@ -18,7 +18,6 @@ */ import i18n from 'i18n-js'; -import AsyncStorageManager from '../managers/AsyncStorageManager'; import PushNotificationIOS from '@react-native-community/push-notification-ios'; import PushNotification from 'react-native-push-notification'; import { Platform } from 'react-native'; @@ -79,11 +78,8 @@ PushNotification.configure({ * @param machineID The machine id to schedule notifications for. This is used as id and in the notification string. * @param date The date to trigger the notification at */ -function createNotifications(machineID: string, date: Date) { - const reminder = AsyncStorageManager.getNumber( - AsyncStorageManager.PREFERENCES.proxiwashNotifications.key - ); - if (!Number.isNaN(reminder) && reminder > 0) { +function createNotifications(machineID: string, date: Date, reminder?: number) { + if (reminder && !Number.isNaN(reminder) && reminder > 0) { const id = reminderIdFactor * parseInt(machineID, 10); const reminderDate = new Date(date); reminderDate.setMinutes(reminderDate.getMinutes() - reminder); @@ -122,10 +118,11 @@ function createNotifications(machineID: string, date: Date) { export function setupMachineNotification( machineID: string, isEnabled: boolean, + reminder?: number, endDate?: Date | null ) { if (isEnabled && endDate) { - createNotifications(machineID, endDate); + createNotifications(machineID, endDate, reminder); } else { PushNotification.cancelLocalNotifications({ id: machineID }); const reminderId = reminderIdFactor * parseInt(machineID, 10); diff --git a/src/utils/Services.ts b/src/utils/Services.ts index bae30c3..8f91a59 100644 --- a/src/utils/Services.ts +++ b/src/utils/Services.ts @@ -16,6 +16,11 @@ * You should have received a copy of the GNU General Public License * along with Campus INSAT. If not, see . */ +import i18n from 'i18n-js'; +import type { FullDashboardType } from '../screens/Home/HomeScreen'; +import Urls from '../constants/Urls'; +import { MainRoutes } from '../navigation/MainNavigator'; +import { TabRoutes } from '../navigation/TabNavigator'; /** * Gets the given services list without items of the given ids @@ -25,45 +30,296 @@ * @returns {[]} */ export default function getStrippedServicesList( - idList: Array, - sourceList: Array + sourceList: Array, + idList?: Array ) { - const newArray: Array = []; - sourceList.forEach((item: T) => { - if (!idList.includes(item.key)) { - newArray.push(item); - } - }); - return newArray; + if (idList) { + return sourceList.filter((item) => !idList.includes(item.key)); + } else { + return sourceList; + } } -/** - * Gets a sublist of the given list with items of the given ids only - * - * The given list must have a field id or key - * - * @param idList The ids of items to find - * @param originalList The original list - * @returns {[]} - */ -export function getSublistWithIds( - idList: Array, - originalList: Array -) { - const subList: Array = []; - for (let i = 0; i < idList.length; i += 1) { - subList.push(null); - } - let itemsAdded = 0; - for (let i = 0; i < originalList.length; i += 1) { - const item = originalList[i]; - if (idList.includes(item.key)) { - subList[idList.indexOf(item.key)] = item; - itemsAdded += 1; - if (itemsAdded === idList.length) { - break; - } - } - } - return subList; +const AMICALE_LOGO = require('../../assets/amicale.png'); + +export const SERVICES_KEY = { + CLUBS: 'clubs', + PROFILE: 'profile', + EQUIPMENT: 'equipment', + AMICALE_WEBSITE: 'amicale_website', + VOTE: 'vote', + PROXIMO: 'proximo', + WIKETUD: 'wiketud', + ELUS_ETUDIANTS: 'elus_etudiants', + TUTOR_INSA: 'tutor_insa', + RU: 'ru', + AVAILABLE_ROOMS: 'available_rooms', + BIB: 'bib', + EMAIL: 'email', + ENT: 'ent', + INSA_ACCOUNT: 'insa_account', + WASHERS: 'washers', + DRYERS: 'dryers', +}; + +export const SERVICES_CATEGORIES_KEY = { + AMICALE: 'amicale', + STUDENTS: 'students', + INSA: 'insa', + SPECIAL: 'special', +}; + +export type ServiceItemType = { + key: string; + title: string; + subtitle: string; + image: string | number; + onPress: () => void; + badgeFunction?: (dashboard: FullDashboardType) => number; +}; + +export type ServiceCategoryType = { + key: string; + title: string; + subtitle: string; + image: string | number; + content: Array; +}; + +export function getAmicaleServices( + onPress: (route: string, params?: { [key: string]: any }) => void, + excludedItems?: Array +): Array { + const amicaleDataset = [ + { + key: SERVICES_KEY.CLUBS, + title: i18n.t('screens.clubs.title'), + subtitle: i18n.t('screens.services.descriptions.clubs'), + image: Urls.images.clubs, + onPress: () => onPress(MainRoutes.ClubList), + }, + { + key: SERVICES_KEY.PROFILE, + title: i18n.t('screens.profile.title'), + subtitle: i18n.t('screens.services.descriptions.profile'), + image: Urls.images.profile, + onPress: () => onPress(MainRoutes.Profile), + }, + { + key: SERVICES_KEY.EQUIPMENT, + title: i18n.t('screens.equipment.title'), + subtitle: i18n.t('screens.services.descriptions.equipment'), + image: Urls.images.equipment, + onPress: () => onPress(MainRoutes.EquipmentList), + }, + { + key: SERVICES_KEY.AMICALE_WEBSITE, + title: i18n.t('screens.websites.amicale'), + subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'), + image: Urls.images.amicale, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.amicale, + title: i18n.t('screens.websites.amicale'), + }), + }, + { + key: SERVICES_KEY.VOTE, + title: i18n.t('screens.vote.title'), + subtitle: i18n.t('screens.services.descriptions.vote'), + image: Urls.images.vote, + onPress: () => onPress(MainRoutes.Vote), + }, + ]; + return getStrippedServicesList(amicaleDataset, excludedItems); +} + +export function getStudentServices( + onPress: (route: string, params?: { [key: string]: any }) => void, + excludedItems?: Array +): Array { + const studentsDataset = [ + { + key: SERVICES_KEY.PROXIMO, + title: i18n.t('screens.proximo.title'), + subtitle: i18n.t('screens.services.descriptions.proximo'), + image: Urls.images.proximo, + onPress: () => onPress(MainRoutes.Proximo), + badgeFunction: (dashboard: FullDashboardType): number => + dashboard.proximo_articles, + }, + { + key: SERVICES_KEY.WIKETUD, + title: 'Wiketud', + subtitle: i18n.t('screens.services.descriptions.wiketud'), + image: Urls.images.wiketud, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.wiketud, + title: 'Wiketud', + }), + }, + { + key: SERVICES_KEY.ELUS_ETUDIANTS, + title: 'Élus Étudiants', + subtitle: i18n.t('screens.services.descriptions.elusEtudiants'), + image: Urls.images.elusEtudiants, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.elusEtudiants, + title: 'Élus Étudiants', + }), + }, + { + key: SERVICES_KEY.TUTOR_INSA, + title: "Tutor'INSA", + subtitle: i18n.t('screens.services.descriptions.tutorInsa'), + image: Urls.images.tutorInsa, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.tutorInsa, + title: "Tutor'INSA", + }), + badgeFunction: (dashboard: FullDashboardType): number => + dashboard.available_tutorials, + }, + ]; + return getStrippedServicesList(studentsDataset, excludedItems); +} + +export function getINSAServices( + onPress: (route: string, params?: { [key: string]: any }) => void, + excludedItems?: Array +): Array { + const insaDataset = [ + { + key: SERVICES_KEY.RU, + title: i18n.t('screens.menu.title'), + subtitle: i18n.t('screens.services.descriptions.self'), + image: Urls.images.menu, + onPress: () => onPress(MainRoutes.SelfMenu), + badgeFunction: (dashboard: FullDashboardType): number => + dashboard.today_menu.length, + }, + { + key: SERVICES_KEY.AVAILABLE_ROOMS, + title: i18n.t('screens.websites.rooms'), + subtitle: i18n.t('screens.services.descriptions.availableRooms'), + image: Urls.images.availableRooms, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.availableRooms, + title: i18n.t('screens.websites.rooms'), + }), + }, + { + key: SERVICES_KEY.BIB, + title: i18n.t('screens.websites.bib'), + subtitle: i18n.t('screens.services.descriptions.bib'), + image: Urls.images.bib, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.bib, + title: i18n.t('screens.websites.bib'), + }), + }, + { + key: SERVICES_KEY.EMAIL, + title: i18n.t('screens.websites.mails'), + subtitle: i18n.t('screens.services.descriptions.mails'), + image: Urls.images.bluemind, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.bluemind, + title: i18n.t('screens.websites.mails'), + }), + }, + { + key: SERVICES_KEY.ENT, + title: i18n.t('screens.websites.ent'), + subtitle: i18n.t('screens.services.descriptions.ent'), + image: Urls.images.ent, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.ent, + title: i18n.t('screens.websites.ent'), + }), + }, + { + key: SERVICES_KEY.INSA_ACCOUNT, + title: i18n.t('screens.insaAccount.title'), + subtitle: i18n.t('screens.services.descriptions.insaAccount'), + image: Urls.images.insaAccount, + onPress: () => + onPress(MainRoutes.Website, { + host: Urls.websites.insaAccount, + title: i18n.t('screens.insaAccount.title'), + }), + }, + ]; + return getStrippedServicesList(insaDataset, excludedItems); +} + +export function getSpecialServices( + onPress: (route: string, params?: { [key: string]: any }) => void, + excludedItems?: Array +): Array { + const specialDataset = [ + { + key: SERVICES_KEY.WASHERS, + title: i18n.t('screens.proxiwash.washers'), + subtitle: i18n.t('screens.services.descriptions.washers'), + image: Urls.images.washer, + onPress: () => onPress(TabRoutes.Proxiwash), + badgeFunction: (dashboard: FullDashboardType): number => + dashboard.available_washers, + }, + { + key: SERVICES_KEY.DRYERS, + title: i18n.t('screens.proxiwash.dryers'), + subtitle: i18n.t('screens.services.descriptions.washers'), + image: Urls.images.dryer, + onPress: () => onPress(TabRoutes.Proxiwash), + badgeFunction: (dashboard: FullDashboardType): number => + dashboard.available_dryers, + }, + ]; + return getStrippedServicesList(specialDataset, excludedItems); +} + +export function getCategories( + onPress: (route: string, params?: { [key: string]: any }) => void, + excludedItems?: Array +): Array { + const categoriesDataset = [ + { + key: SERVICES_CATEGORIES_KEY.AMICALE, + title: i18n.t('screens.services.categories.amicale'), + subtitle: i18n.t('screens.services.more'), + image: AMICALE_LOGO, + content: getAmicaleServices(onPress), + }, + { + key: SERVICES_CATEGORIES_KEY.STUDENTS, + title: i18n.t('screens.services.categories.students'), + subtitle: i18n.t('screens.services.more'), + image: 'account-group', + content: getStudentServices(onPress), + }, + { + key: SERVICES_CATEGORIES_KEY.INSA, + title: i18n.t('screens.services.categories.insa'), + subtitle: i18n.t('screens.services.more'), + image: 'school', + content: getINSAServices(onPress), + }, + { + key: SERVICES_CATEGORIES_KEY.SPECIAL, + title: i18n.t('screens.services.categories.special'), + subtitle: i18n.t('screens.services.categories.special'), + image: 'star', + content: getSpecialServices(onPress), + }, + ]; + return getStrippedServicesList(categoriesDataset, excludedItems); } diff --git a/src/managers/ThemeManager.ts b/src/utils/Themes.ts similarity index 53% rename from src/managers/ThemeManager.ts rename to src/utils/Themes.ts index 9b4d341..14cecfe 100644 --- a/src/managers/ThemeManager.ts +++ b/src/utils/Themes.ts @@ -1,28 +1,4 @@ -/* - * Copyright (c) 2019 - 2020 Arnaud Vergnet. - * - * This file is part of Campus INSAT. - * - * Campus INSAT is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Campus INSAT is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Campus INSAT. If not, see . - */ - import { DarkTheme, DefaultTheme } from 'react-native-paper'; -import { Appearance } from 'react-native-appearance'; -import AsyncStorageManager from './AsyncStorageManager'; -import AprilFoolsManager from './AprilFoolsManager'; - -const colorScheme = Appearance.getColorScheme(); declare global { namespace ReactNativePaper { @@ -83,7 +59,7 @@ declare global { } } -const CustomWhiteTheme: ReactNativePaper.Theme = { +export const CustomWhiteTheme: ReactNativePaper.Theme = { ...DefaultTheme, colors: { ...DefaultTheme.colors, @@ -142,7 +118,7 @@ const CustomWhiteTheme: ReactNativePaper.Theme = { }, }; -const CustomDarkTheme: ReactNativePaper.Theme = { +export const CustomDarkTheme: ReactNativePaper.Theme = { ...DarkTheme, colors: { ...DarkTheme.colors, @@ -200,99 +176,3 @@ const CustomDarkTheme: ReactNativePaper.Theme = { mascotMessageArrow: '#323232', }, }; - -/** - * Singleton class used to manage themes - */ -export default class ThemeManager { - static instance: ThemeManager | null = null; - - updateThemeCallback: null | (() => void); - - constructor() { - this.updateThemeCallback = null; - } - - /** - * Get this class instance or create one if none is found - * - * @returns {ThemeManager} - */ - static getInstance(): ThemeManager { - if (ThemeManager.instance == null) { - ThemeManager.instance = new ThemeManager(); - } - return ThemeManager.instance; - } - - /** - * Gets night mode status. - * If Follow System Preferences is enabled, will first use system theme. - * If disabled or not available, will use value stored din preferences - * - * @returns {boolean} Night mode state - */ - static getNightMode(): boolean { - return ( - (AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.nightMode.key - ) && - (!AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key - ) || - colorScheme === 'no-preference')) || - (AsyncStorageManager.getBool( - AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key - ) && - colorScheme === 'dark') - ); - } - - /** - * Get the current theme based on night mode and events - * - * @returns {ReactNativePaper.Theme} The current theme - */ - static getCurrentTheme(): ReactNativePaper.Theme { - if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { - return AprilFoolsManager.getAprilFoolsTheme(CustomWhiteTheme); - } - return ThemeManager.getBaseTheme(); - } - - /** - * Get the theme based on night mode - * - * @return {ReactNativePaper.Theme} The theme - */ - static getBaseTheme(): ReactNativePaper.Theme { - if (ThemeManager.getNightMode()) { - return CustomDarkTheme; - } - return CustomWhiteTheme; - } - - /** - * Sets the function to be called when the theme is changed (allows for general reload of the app) - * - * @param callback Function to call after theme change - */ - setUpdateThemeCallback(callback: () => void) { - this.updateThemeCallback = callback; - } - - /** - * Set night mode and save it to preferences - * - * @param isNightMode True to enable night mode, false to disable - */ - setNightMode(isNightMode: boolean) { - AsyncStorageManager.set( - AsyncStorageManager.PREFERENCES.nightMode.key, - isNightMode - ); - if (this.updateThemeCallback != null) { - this.updateThemeCallback(); - } - } -} diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index d8c9af4..6ff6085 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -18,23 +18,21 @@ */ import { Platform, StatusBar } from 'react-native'; -import ThemeManager from '../managers/ThemeManager'; /** * Updates status bar content color if on iOS only, * as the android status bar is always set to black. */ -export function setupStatusBar() { - if (ThemeManager.getNightMode()) { - StatusBar.setBarStyle('light-content', true); - } else { - StatusBar.setBarStyle('dark-content', true); - } - if (Platform.OS === 'android') { - StatusBar.setBackgroundColor( - ThemeManager.getCurrentTheme().colors.surface, - true - ); +export function setupStatusBar(theme?: ReactNativePaper.Theme) { + if (theme) { + if (theme.dark) { + StatusBar.setBarStyle('light-content', true); + } else { + StatusBar.setBarStyle('dark-content', true); + } + if (Platform.OS === 'android') { + StatusBar.setBackgroundColor(theme.colors.surface, true); + } } } diff --git a/src/utils/asyncStorage.ts b/src/utils/asyncStorage.ts index 0755e68..0c19b9c 100644 --- a/src/utils/asyncStorage.ts +++ b/src/utils/asyncStorage.ts @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { SERVICES_KEY } from '../managers/ServicesManager'; +import { SERVICES_KEY } from './Services'; export enum PreferenceKeys { debugUnlocked = 'debugUnlocked', @@ -9,6 +9,7 @@ export enum PreferenceKeys { nightModeFollowSystem = 'nightModeFollowSystem', nightMode = 'nightMode', defaultStartScreen = 'defaultStartScreen', + servicesShowMascot = 'servicesShowMascot', proxiwashShowMascot = 'proxiwashShowMascot', homeShowMascot = 'homeShowMascot', @@ -17,7 +18,8 @@ export enum PreferenceKeys { loginShowMascot = 'loginShowMascot', voteShowMascot = 'voteShowMascot', equipmentShowMascot = 'equipmentShowMascot', - gameStartMascot = 'gameStartMascot', + gameShowMascot = 'gameShowMascot', + proxiwashWatchedMachines = 'proxiwashWatchedMachines', showAprilFoolsStart = 'showAprilFoolsStart', planexCurrentGroup = 'planexCurrentGroup', @@ -45,7 +47,7 @@ export const defaultPreferences: { [key in PreferenceKeys]: string } = { [PreferenceKeys.loginShowMascot]: '1', [PreferenceKeys.voteShowMascot]: '1', [PreferenceKeys.equipmentShowMascot]: '1', - [PreferenceKeys.gameStartMascot]: '1', + [PreferenceKeys.gameShowMascot]: '1', [PreferenceKeys.proxiwashWatchedMachines]: '[]', [PreferenceKeys.showAprilFoolsStart]: '1', [PreferenceKeys.planexCurrentGroup]: '', @@ -114,6 +116,10 @@ export function setPreference( return prevPreferences; } +export function isValidPreferenceKey(key: string): key is PreferenceKeys { + return key in Object.values(PreferenceKeys); +} + /** * Gets the boolean value of the given preference *