Compare commits

..

No commits in common. "b15b200846eff925163a94c6933210f07c63bc71" and "b5d4ad83c3678846d69fad605bfd9e9c1e2ba629" have entirely different histories.

54 changed files with 2853 additions and 2632 deletions

211
App.tsx
View file

@ -18,36 +18,27 @@
*/ */
import React from 'react'; import React from 'react';
import { LogBox, Platform } from 'react-native'; import { LogBox, Platform, SafeAreaView, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Provider as PaperProvider } from 'react-native-paper';
import { setSafeBounceHeight } from 'react-navigation-collapsible'; import { setSafeBounceHeight } from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen'; 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 ConnectionManager from './src/managers/ConnectionManager';
import type { ParsedUrlDataType } from './src/utils/URLHandler'; import type { ParsedUrlDataType } from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler'; import URLHandler from './src/utils/URLHandler';
import { setupStatusBar } from './src/utils/Utils';
import initLocales from './src/utils/Locales'; import initLocales from './src/utils/Locales';
import { NavigationContainerRef } from '@react-navigation/core'; import { NavigationContainerRef } from '@react-navigation/core';
import { import GENERAL_STYLES from './src/constants/Styles';
defaultMascotPreferences, import CollapsibleProvider from './src/components/providers/CollapsibleProvider';
defaultPlanexPreferences, import CacheProvider from './src/components/providers/CacheProvider';
defaultPreferences,
defaultProxiwashPreferences,
GeneralPreferenceKeys,
GeneralPreferencesType,
MascotPreferenceKeys,
MascotPreferencesType,
PlanexPreferenceKeys,
PlanexPreferencesType,
ProxiwashPreferenceKeys,
ProxiwashPreferencesType,
retrievePreferences,
} from './src/utils/asyncStorage';
import {
GeneralPreferencesProvider,
MascotPreferencesProvider,
PlanexPreferencesProvider,
ProxiwashPreferencesProvider,
} from './src/components/providers/PreferencesProvider';
import MainApp from './src/screens/MainApp';
// Native optimizations https://reactnavigation.org/docs/react-native-screens // Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+ // Crashes app when navigating away from webview on android 9+
@ -61,20 +52,18 @@ LogBox.ignoreLogs([
type StateType = { type StateType = {
isLoading: boolean; isLoading: boolean;
initialPreferences: { showIntro: boolean;
general: GeneralPreferencesType; showUpdate: boolean;
planex: PlanexPreferencesType; showAprilFools: boolean;
proxiwash: ProxiwashPreferencesType; currentTheme: ReactNativePaper.Theme | undefined;
mascot: MascotPreferencesType;
};
}; };
export default class App extends React.Component<{}, StateType> { export default class App extends React.Component<{}, StateType> {
navigatorRef: { current: null | NavigationContainerRef }; navigatorRef: { current: null | NavigationContainerRef };
defaultHomeRoute: string | undefined; defaultHomeRoute: string | null;
defaultHomeData: { [key: string]: string } | undefined; defaultHomeData: { [key: string]: string };
urlHandler: URLHandler; urlHandler: URLHandler;
@ -82,17 +71,15 @@ export default class App extends React.Component<{}, StateType> {
super(props); super(props);
this.state = { this.state = {
isLoading: true, isLoading: true,
initialPreferences: { showIntro: true,
general: defaultPreferences, showUpdate: true,
planex: defaultPlanexPreferences, showAprilFools: false,
proxiwash: defaultProxiwashPreferences, currentTheme: undefined,
mascot: defaultMascotPreferences,
},
}; };
initLocales(); initLocales();
this.navigatorRef = React.createRef(); this.navigatorRef = React.createRef();
this.defaultHomeRoute = undefined; this.defaultHomeRoute = null;
this.defaultHomeData = undefined; this.defaultHomeData = {};
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL); this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen(); this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20); setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
@ -127,27 +114,67 @@ 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 * Async loading is done, finish processing startup data
*/ */
onLoadFinished = ( onLoadFinished = () => {
values: Array< // Only show intro if this is the first time starting the app
| GeneralPreferencesType ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
| PlanexPreferencesType // Status bar goes dark if set too fast on ios
| ProxiwashPreferencesType if (Platform.OS === 'ios') {
| MascotPreferencesType setTimeout(setupStatusBar, 1000);
| void } else {
> setupStatusBar();
) => { }
const [general, planex, proxiwash, mascot] = values;
this.setState({ this.setState({
isLoading: false, isLoading: false,
initialPreferences: { currentTheme: ThemeManager.getCurrentTheme(),
general: general as GeneralPreferencesType, showIntro: AsyncStorageManager.getBool(
planex: planex as PlanexPreferencesType, AsyncStorageManager.PREFERENCES.showIntro.key
proxiwash: proxiwash as ProxiwashPreferencesType, ),
mascot: mascot as MascotPreferencesType, showUpdate:
}, AsyncStorageManager.getNumber(
AsyncStorageManager.PREFERENCES.updateNumber.key
) !== Update.number,
showAprilFools:
AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key
),
}); });
SplashScreen.hide(); SplashScreen.hide();
}; };
@ -159,22 +186,7 @@ export default class App extends React.Component<{}, StateType> {
*/ */
loadAssetsAsync() { loadAssetsAsync() {
Promise.all([ Promise.all([
retrievePreferences( AsyncStorageManager.getInstance().loadPreferences(),
Object.values(GeneralPreferenceKeys),
defaultPreferences
),
retrievePreferences(
Object.values(PlanexPreferenceKeys),
defaultPlanexPreferences
),
retrievePreferences(
Object.values(ProxiwashPreferenceKeys),
defaultProxiwashPreferences
),
retrievePreferences(
Object.values(MascotPreferenceKeys),
defaultMascotPreferences
),
ConnectionManager.getInstance().recoverLogin(), ConnectionManager.getInstance().recoverLogin(),
]) ])
.then(this.onLoadFinished) .then(this.onLoadFinished)
@ -189,28 +201,43 @@ export default class App extends React.Component<{}, StateType> {
if (state.isLoading) { if (state.isLoading) {
return null; return null;
} }
if (state.showIntro || state.showUpdate || state.showAprilFools) {
return (
<CustomIntroSlider
onDone={this.onIntroDone}
isUpdate={state.showUpdate && !state.showIntro}
isAprilFools={state.showAprilFools && !state.showIntro}
/>
);
}
return ( return (
<GeneralPreferencesProvider <PaperProvider theme={state.currentTheme}>
initialPreferences={this.state.initialPreferences.general} <CollapsibleProvider>
> <CacheProvider>
<PlanexPreferencesProvider <OverflowMenuProvider>
initialPreferences={this.state.initialPreferences.planex} <View
> style={{
<ProxiwashPreferencesProvider backgroundColor: ThemeManager.getCurrentTheme().colors
initialPreferences={this.state.initialPreferences.proxiwash} .background,
> ...GENERAL_STYLES.flex,
<MascotPreferencesProvider }}
initialPreferences={this.state.initialPreferences.mascot} >
> <SafeAreaView style={GENERAL_STYLES.flex}>
<MainApp <NavigationContainer
ref={this.navigatorRef} theme={state.currentTheme}
defaultHomeData={this.defaultHomeData} ref={this.navigatorRef}
defaultHomeRoute={this.defaultHomeRoute} >
/> <MainNavigator
</MascotPreferencesProvider> defaultHomeRoute={this.defaultHomeRoute}
</ProxiwashPreferencesProvider> defaultHomeData={this.defaultHomeData}
</PlanexPreferencesProvider> />
</GeneralPreferencesProvider> </NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
</CacheProvider>
</CollapsibleProvider>
</PaperProvider>
); );
} }
} }

View file

@ -463,7 +463,6 @@
"badToken": "You are not logged in. Please login and try again.", "badToken": "You are not logged in. Please login and try again.",
"noConsent": "You did not give your consent for data processing to the Amicale.", "noConsent": "You did not give your consent for data processing to the Amicale.",
"tokenSave": "Could not save session token. Please contact support.", "tokenSave": "Could not save session token. Please contact support.",
"tokenRetrieve": "Could not retrieve session token. Please contact support.",
"badInput": "Invalid input. Please try again.", "badInput": "Invalid input. Please try again.",
"forbidden": "You do not have access to this data.", "forbidden": "You do not have access to this data.",
"connectionError": "Network error. Please check your internet connection.", "connectionError": "Network error. Please check your internet connection.",

View file

@ -463,7 +463,6 @@
"badToken": "Tu n'est pas connecté. Merci de te connecter puis réessayes.", "badToken": "Tu n'est pas connecté. Merci de te connecter puis réessayes.",
"noConsent": "Tu n'as pas donné ton consentement pour l'utilisation de tes données personnelles.", "noConsent": "Tu n'as pas donné ton consentement pour l'utilisation de tes données personnelles.",
"tokenSave": "Impossible de sauvegarder le token de session. Merci de contacter le support.", "tokenSave": "Impossible de sauvegarder le token de session. Merci de contacter le support.",
"tokenRetrieve": "Impossible de récupérer le token de session. Merci de contacter le support.",
"badInput": "Entrée invalide. Merci de réessayer.", "badInput": "Entrée invalide. Merci de réessayer.",
"forbidden": "Tu n'as pas accès à cette information.", "forbidden": "Tu n'as pas accès à cette information.",
"connectionError": "Erreur de réseau. Merci de vérifier ta connexion Internet.", "connectionError": "Erreur de réseau. Merci de vérifier ta connexion Internet.",

View file

@ -51,7 +51,6 @@ function CollapsibleComponent(props: Props) {
const { paddedProps, headerColors } = props; const { paddedProps, headerColors } = props;
const Comp = props.component; const Comp = props.component;
const theme = useTheme(); const theme = useTheme();
const { setCollapsible } = useCollapsible(); const { setCollapsible } = useCollapsible();
const collapsible = useCollapsibleHeader({ const collapsible = useCollapsibleHeader({
@ -59,11 +58,6 @@ function CollapsibleComponent(props: Props) {
collapsedColor: headerColors ? headerColors : theme.colors.surface, collapsedColor: headerColors ? headerColors : theme.colors.surface,
useNativeDriver: true, useNativeDriver: true,
}, },
navigationOptions: {
headerStyle: {
backgroundColor: headerColors ? headerColors : theme.colors.surface,
},
},
}); });
useFocusEffect( useFocusEffect(

View file

@ -21,7 +21,7 @@ import * as React from 'react';
import { Animated, Dimensions, ViewStyle } from 'react-native'; import { Animated, Dimensions, ViewStyle } from 'react-native';
import ImageListItem from './ImageListItem'; import ImageListItem from './ImageListItem';
import CardListItem from './CardListItem'; import CardListItem from './CardListItem';
import { ServiceItemType } from '../../../utils/Services'; import type { ServiceItemType } from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
dataset: Array<ServiceItemType>; dataset: Array<ServiceItemType>;

View file

@ -20,8 +20,8 @@
import * as React from 'react'; import * as React from 'react';
import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper'; import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import type { ServiceItemType } from '../../../managers/ServicesManager';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { ServiceItemType } from '../../../utils/Services';
type PropsType = { type PropsType = {
item: ServiceItemType; item: ServiceItemType;

View file

@ -20,8 +20,8 @@
import * as React from 'react'; import * as React from 'react';
import { Text, TouchableRipple } from 'react-native-paper'; import { Text, TouchableRipple } from 'react-native-paper';
import { Image, StyleSheet, View } from 'react-native'; import { Image, StyleSheet, View } from 'react-native';
import type { ServiceItemType } from '../../../managers/ServicesManager';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { ServiceItemType } from '../../../utils/Services';
type PropsType = { type PropsType = {
item: ServiceItemType; item: ServiceItemType;

View file

@ -23,7 +23,10 @@ import { FlatList, Image, StyleSheet, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardEditItem from './DashboardEditItem'; import DashboardEditItem from './DashboardEditItem';
import AnimatedAccordion from '../../Animations/AnimatedAccordion'; import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import { ServiceCategoryType, ServiceItemType } from '../../../utils/Services'; import type {
ServiceCategoryType,
ServiceItemType,
} from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
item: ServiceCategoryType; item: ServiceCategoryType;

View file

@ -20,7 +20,7 @@
import * as React from 'react'; import * as React from 'react';
import { Image, StyleSheet } from 'react-native'; import { Image, StyleSheet } from 'react-native';
import { List, useTheme } from 'react-native-paper'; import { List, useTheme } from 'react-native-paper';
import { ServiceItemType } from '../../../utils/Services'; import type { ServiceItemType } from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
item: ServiceItemType; item: ServiceItemType;

View file

@ -28,17 +28,17 @@ import {
View, View,
} from 'react-native'; } from 'react-native';
import Mascot from './Mascot'; import Mascot from './Mascot';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import MascotSpeechBubble, { import MascotSpeechBubble, {
MascotSpeechBubbleProps, MascotSpeechBubbleProps,
} from './MascotSpeechBubble'; } from './MascotSpeechBubble';
import { useMountEffect } from '../../utils/customHooks'; import { useMountEffect } from '../../utils/customHooks';
import { useRoute } from '@react-navigation/core';
import { useShouldShowMascot } from '../../context/preferencesContext';
type PropsType = MascotSpeechBubbleProps & { type PropsType = MascotSpeechBubbleProps & {
emotion: number; emotion: number;
visible?: boolean; visible?: boolean;
prefKey?: string;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -61,14 +61,13 @@ const BUBBLE_HEIGHT = Dimensions.get('window').height / 3;
* Component used to display a popup with the mascot. * Component used to display a popup with the mascot.
*/ */
function MascotPopup(props: PropsType) { function MascotPopup(props: PropsType) {
const route = useRoute();
const { shouldShow, setShouldShow } = useShouldShowMascot(route.name);
const isVisible = () => { const isVisible = () => {
if (props.visible !== undefined) { if (props.visible !== undefined) {
return props.visible; return props.visible;
} else if (props.prefKey != null) {
return AsyncStorageManager.getBool(props.prefKey);
} else { } else {
return shouldShow; return false;
} }
}; };
@ -165,8 +164,10 @@ function MascotPopup(props: PropsType) {
}; };
const onDismiss = (callback?: () => void) => { const onDismiss = (callback?: () => void) => {
setShouldShow(false); if (props.prefKey != null) {
setDialogVisible(false); AsyncStorageManager.set(props.prefKey, false);
setDialogVisible(false);
}
if (callback) { if (callback) {
callback(); callback();
} }

View file

@ -32,6 +32,7 @@ import LinearGradient from 'react-native-linear-gradient';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { Card } from 'react-native-paper'; import { Card } from 'react-native-paper';
import Update from '../../constants/Update'; import Update from '../../constants/Update';
import ThemeManager from '../../managers/ThemeManager';
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
import MascotIntroWelcome from '../Intro/MascotIntroWelcome'; import MascotIntroWelcome from '../Intro/MascotIntroWelcome';
import IntroIcon from '../Intro/IconIntro'; import IntroIcon from '../Intro/IconIntro';
@ -288,6 +289,9 @@ export default class CustomIntroSlider extends React.Component<
onDone = () => { onDone = () => {
const { props } = this; const { props } = this;
CustomIntroSlider.setStatusBarColor(
ThemeManager.getCurrentTheme().colors.surface
);
props.onDone(); props.onDone();
}; };

View file

@ -3,11 +3,11 @@ import { StyleSheet, View } from 'react-native';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import DateManager from '../../managers/DateManager'; import DateManager from '../../managers/DateManager';
import ThemeManager from '../../managers/ThemeManager';
import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen'; import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import WebViewScreen from './WebViewScreen'; import WebViewScreen from './WebViewScreen';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { useTheme } from 'react-native-paper';
type Props = { type Props = {
currentGroup?: PlanexGroupType; currentGroup?: PlanexGroupType;
@ -67,14 +67,6 @@ calendar.option({
} }
});`; });`;
export const JS_LOADED_MESSAGE = '1';
const NOTIFY_JS_INJECTED = `
function notifyJsInjected() {
window.ReactNativeWebView.postMessage('${JS_LOADED_MESSAGE}');
}
`;
// Mobile friendly CSS // Mobile friendly CSS
const CUSTOM_CSS = const CUSTOM_CSS =
'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; 'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
@ -94,35 +86,30 @@ const INJECT_STYLE_DARK = `$('head').append('<style>${CUSTOM_CSS_DARK}</style>')
* *
* @param groupID The current group selected * @param groupID The current group selected
*/ */
const generateInjectedJS = ( const generateInjectedJS = (group: PlanexGroupType | undefined) => {
group: PlanexGroupType | undefined,
darkMode: boolean
) => {
let customInjectedJS = `$(document).ready(function() { let customInjectedJS = `$(document).ready(function() {
${OBSERVE_MUTATIONS_INJECTED} ${OBSERVE_MUTATIONS_INJECTED}
${INJECT_STYLE} ${INJECT_STYLE}
${FULL_CALENDAR_SETTINGS} ${FULL_CALENDAR_SETTINGS}`;
${NOTIFY_JS_INJECTED}`;
if (group) { if (group) {
customInjectedJS += `displayAde(${group.id});`; customInjectedJS += `displayAde(${group.id});`;
} }
if (DateManager.isWeekend(new Date())) { if (DateManager.isWeekend(new Date())) {
customInjectedJS += `calendar.next();`; customInjectedJS += `calendar.next();`;
} }
if (darkMode) { if (ThemeManager.getNightMode()) {
customInjectedJS += INJECT_STYLE_DARK; customInjectedJS += INJECT_STYLE_DARK;
} }
customInjectedJS += `notifyJsInjected();});true;`; // Prevents crash on ios customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
return customInjectedJS; return customInjectedJS;
}; };
function PlanexWebview(props: Props) { function PlanexWebview(props: Props) {
const theme = useTheme();
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<WebViewScreen <WebViewScreen
url={Urls.planex.planning} url={Urls.planex.planning}
initialJS={generateInjectedJS(props.currentGroup, theme.dark)} initialJS={generateInjectedJS(props.currentGroup)}
injectJS={props.injectJS} injectJS={props.injectJS}
onMessage={props.onMessage} onMessage={props.onMessage}
showAdvancedControls={false} showAdvancedControls={false}

View file

@ -41,7 +41,7 @@ export type RequestProps = {
type Props<T> = RequestScreenProps<T>; type Props<T> = RequestScreenProps<T>;
const MIN_REFRESH_TIME = 3 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
export default function RequestScreen<T>(props: Props<T>) { export default function RequestScreen<T>(props: Props<T>) {
const navigation = useNavigation<StackNavigationProp<any>>(); const navigation = useNavigation<StackNavigationProp<any>>();
@ -94,7 +94,8 @@ export default function RequestScreen<T>(props: Props<T>) {
clearInterval(refreshInterval.current); clearInterval(refreshInterval.current);
} }
}; };
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime, refreshData]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.cache, props.refreshOnFocus])
); );
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => { const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {

View file

@ -44,7 +44,12 @@ type Props<ItemT, RawData> = Omit<
> & > &
Omit< Omit<
RequestScreenProps<RawData>, RequestScreenProps<RawData>,
'render' | 'showLoading' | 'showError' | 'onMajorError' | 'render'
| 'showLoading'
| 'showError'
| 'refresh'
| 'onFinish'
| 'onMajorError'
> & > &
Omit< Omit<
SectionListProps<ItemT>, SectionListProps<ItemT>,
@ -166,8 +171,6 @@ function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
refreshOnFocus={props.refreshOnFocus} refreshOnFocus={props.refreshOnFocus}
cache={props.cache} cache={props.cache}
onCacheUpdate={props.onCacheUpdate} onCacheUpdate={props.onCacheUpdate}
refresh={props.refresh}
onFinish={props.onFinish}
/> />
); );
} }

View file

@ -1,173 +1,67 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
defaultMascotPreferences,
defaultPlanexPreferences,
defaultPreferences, defaultPreferences,
defaultProxiwashPreferences,
GeneralPreferenceKeys,
GeneralPreferencesType,
MascotPreferenceKeys,
MascotPreferencesType,
PlanexPreferenceKeys,
PlanexPreferencesType,
PreferenceKeys, PreferenceKeys,
PreferencesType, PreferencesType,
ProxiwashPreferenceKeys,
ProxiwashPreferencesType,
setPreference, setPreference,
} from '../../utils/asyncStorage'; } from '../../utils/asyncStorage';
import { import {
MascotPreferencesContext,
PlanexPreferencesContext,
PreferencesContext, PreferencesContext,
PreferencesContextType, PreferencesContextType,
ProxiwashPreferencesContext,
} from '../../context/preferencesContext'; } from '../../context/preferencesContext';
function updateState<T extends Partial<PreferencesType>, K extends string>( type Props = {
key: K, children: React.ReactChild;
value: number | string | boolean | object | Array<any>, initialPreferences: PreferencesType;
prevState: PreferencesContextType<T, K> };
) {
const prevPreferences = { ...prevState.preferences };
const newPrefs = setPreference(key as PreferenceKeys, value, prevPreferences);
const newSate = {
...prevState,
preferences: { ...newPrefs },
};
return newSate;
}
function resetState< export default function PreferencesProvider(props: Props) {
T extends Partial<PreferencesType>,
K extends Partial<PreferenceKeys>
>(
keys: Array<PreferenceKeys>,
defaults: T,
prevState: PreferencesContextType<T, K>
) {
const prevPreferences = { ...prevState.preferences };
let newPreferences = { ...prevPreferences };
keys.forEach((key) => {
newPreferences = setPreference(key, defaults[key], prevPreferences);
});
const newSate = {
...prevState,
preferences: { ...newPreferences },
};
return newSate;
}
function usePreferencesProvider<
T extends Partial<PreferencesType>,
K extends Partial<PreferenceKeys>
>(initialPreferences: T, defaults: T, keys: Array<K>) {
const updatePreferences = ( const updatePreferences = (
key: K, key: PreferenceKeys,
value: number | string | boolean | object | Array<any> value: number | string | boolean | object | Array<any>
) => { ) => {
setPreferencesState((prevState) => updateState(key, value, prevState)); setPreferencesState((prevState) => {
const prevPreferences = { ...prevState.preferences };
const newPrefs = setPreference(key, value, prevPreferences);
const newSate = {
...prevState,
preferences: { ...newPrefs },
};
return newSate;
});
}; };
const resetPreferences = () => { const resetPreferences = () => {
setPreferencesState((prevState) => resetState(keys, defaults, prevState)); setPreferencesState((prevState) => {
const prevPreferences = { ...prevState.preferences };
let newPreferences = { ...prevPreferences };
Object.values(PreferenceKeys).forEach((key) => {
newPreferences = setPreference(
key,
defaultPreferences[key],
prevPreferences
);
});
const newSate = {
...prevState,
preferences: { ...newPreferences },
};
return newSate;
});
}; };
const [preferencesState, setPreferencesState] = useState< const [
PreferencesContextType<T, K> preferencesState,
>({ setPreferencesState,
preferences: { ...initialPreferences }, ] = useState<PreferencesContextType>({
preferences: { ...props.initialPreferences },
updatePreferences: updatePreferences, updatePreferences: updatePreferences,
resetPreferences: resetPreferences, resetPreferences: resetPreferences,
}); });
return preferencesState;
}
type PreferencesProviderProps<
T extends Partial<PreferencesType>,
K extends Partial<PreferenceKeys>
> = {
children: React.ReactChild;
initialPreferences: T;
defaults: T;
keys: Array<K>;
Context: React.Context<PreferencesContextType<T, K>>;
};
function PreferencesProvider<
T extends Partial<PreferencesType>,
K extends Partial<PreferenceKeys>
>(props: PreferencesProviderProps<T, K>) {
const { Context } = props;
const preferencesState = usePreferencesProvider<T, K>(
props.initialPreferences,
props.defaults,
Object.values(props.keys)
);
return ( return (
<Context.Provider value={preferencesState}> <PreferencesContext.Provider value={preferencesState}>
{props.children} {props.children}
</Context.Provider> </PreferencesContext.Provider>
);
}
type Props<T> = {
children: React.ReactChild;
initialPreferences: T;
};
export function GeneralPreferencesProvider(
props: Props<GeneralPreferencesType>
) {
return (
<PreferencesProvider
Context={PreferencesContext}
initialPreferences={props.initialPreferences}
defaults={defaultPreferences}
keys={Object.values(GeneralPreferenceKeys)}
>
{props.children}
</PreferencesProvider>
);
}
export function PlanexPreferencesProvider(props: Props<PlanexPreferencesType>) {
return (
<PreferencesProvider
Context={PlanexPreferencesContext}
initialPreferences={props.initialPreferences}
defaults={defaultPlanexPreferences}
keys={Object.values(PlanexPreferenceKeys)}
>
{props.children}
</PreferencesProvider>
);
}
export function ProxiwashPreferencesProvider(
props: Props<ProxiwashPreferencesType>
) {
return (
<PreferencesProvider
Context={ProxiwashPreferencesContext}
initialPreferences={props.initialPreferences}
defaults={defaultProxiwashPreferences}
keys={Object.values(ProxiwashPreferenceKeys)}
>
{props.children}
</PreferencesProvider>
);
}
export function MascotPreferencesProvider(props: Props<MascotPreferencesType>) {
return (
<PreferencesProvider
Context={MascotPreferencesContext}
initialPreferences={props.initialPreferences}
defaults={defaultMascotPreferences}
keys={Object.values(MascotPreferenceKeys)}
>
{props.children}
</PreferencesProvider>
); );
} }

View file

@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import {
defaultPreferences,
PreferenceKeys,
PreferencesType,
} from '../utils/asyncStorage';
export type PreferencesContextType = {
preferences: PreferencesType;
updatePreferences: (
key: PreferenceKeys,
value: number | string | boolean | object | Array<any>
) => void;
resetPreferences: () => void;
};
export const PreferencesContext = React.createContext<PreferencesContextType>({
preferences: defaultPreferences,
updatePreferences: () => undefined,
resetPreferences: () => undefined,
});
export function usePreferences() {
return useContext(PreferencesContext);
}

View file

@ -1,160 +0,0 @@
import { useNavigation } from '@react-navigation/core';
import React, { useContext } from 'react';
import { Appearance } from 'react-native-appearance';
import {
defaultMascotPreferences,
defaultPlanexPreferences,
defaultPreferences,
defaultProxiwashPreferences,
getPreferenceBool,
getPreferenceObject,
MascotPreferenceKeys,
MascotPreferencesType,
PlanexPreferenceKeys,
PlanexPreferencesType,
GeneralPreferenceKeys,
GeneralPreferencesType,
ProxiwashPreferenceKeys,
ProxiwashPreferencesType,
isValidMascotPreferenceKey,
PreferencesType,
} from '../utils/asyncStorage';
import {
getAmicaleServices,
getINSAServices,
getSpecialServices,
getStudentServices,
} from '../utils/Services';
const colorScheme = Appearance.getColorScheme();
export type PreferencesContextType<
T extends Partial<PreferencesType>,
K extends string
> = {
preferences: T;
updatePreferences: (
key: K,
value: number | string | boolean | object | Array<any>
) => void;
resetPreferences: () => void;
};
// CONTEXTES
// Preferences are separated into several contextes to improve performances
export const PreferencesContext = React.createContext<
PreferencesContextType<GeneralPreferencesType, GeneralPreferenceKeys>
>({
preferences: defaultPreferences,
updatePreferences: () => undefined,
resetPreferences: () => undefined,
});
export const PlanexPreferencesContext = React.createContext<
PreferencesContextType<PlanexPreferencesType, PlanexPreferenceKeys>
>({
preferences: defaultPlanexPreferences,
updatePreferences: () => undefined,
resetPreferences: () => undefined,
});
export const ProxiwashPreferencesContext = React.createContext<
PreferencesContextType<ProxiwashPreferencesType, ProxiwashPreferenceKeys>
>({
preferences: defaultProxiwashPreferences,
updatePreferences: () => undefined,
resetPreferences: () => undefined,
});
export const MascotPreferencesContext = React.createContext<
PreferencesContextType<MascotPreferencesType, MascotPreferenceKeys>
>({
preferences: defaultMascotPreferences,
updatePreferences: () => undefined,
resetPreferences: () => undefined,
});
// Context Hooks
export function usePreferences() {
return useContext(PreferencesContext);
}
export function usePlanexPreferences() {
return useContext(PlanexPreferencesContext);
}
export function useProxiwashPreferences() {
return useContext(ProxiwashPreferencesContext);
}
export function useMascotPreferences() {
return useContext(MascotPreferencesContext);
}
// Custom Hooks
export function useShouldShowMascot(route: string) {
const { preferences, updatePreferences } = useMascotPreferences();
const key = route + 'ShowMascot';
let shouldShow = false;
if (isValidMascotPreferenceKey(key)) {
shouldShow = getPreferenceBool(key, preferences) !== false;
}
const setShouldShow = (show: boolean) => {
if (isValidMascotPreferenceKey(key)) {
updatePreferences(key, show);
} else {
console.log('Invalid preference key: ' + key);
}
};
return { shouldShow, setShouldShow };
}
export function useDarkTheme() {
const { preferences } = usePreferences();
return (
(getPreferenceBool(GeneralPreferenceKeys.nightMode, preferences) !==
false &&
(getPreferenceBool(
GeneralPreferenceKeys.nightModeFollowSystem,
preferences
) === false ||
colorScheme === 'no-preference')) ||
(getPreferenceBool(
GeneralPreferenceKeys.nightModeFollowSystem,
preferences
) !== false &&
colorScheme === 'dark')
);
}
export function useCurrentDashboard() {
const { preferences, updatePreferences } = usePreferences();
const navigation = useNavigation();
const dashboardIdList = getPreferenceObject(
GeneralPreferenceKeys.dashboardItems,
preferences
) as Array<string>;
const updateCurrentDashboard = (newList: Array<string>) => {
updatePreferences(GeneralPreferenceKeys.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,
};
}

View file

@ -0,0 +1,269 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<any>
) {
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<T>(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<void>}
*/
async loadPreferences() {
return new Promise((resolve: (val: void) => void) => {
const prefKeys: Array<string> = [];
// 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<any>
) {
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];
}
}

View file

@ -0,0 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<ServiceItemType | null> {
const dashboardIdList = AsyncStorageManager.getObject<Array<string>>(
AsyncStorageManager.PREFERENCES.dashboardItems.key
);
const allDatasets = [
...this.amicaleDataset,
...this.studentsDataset,
...this.insaDataset,
...this.specialDataset,
];
return getSublistWithIds(dashboardIdList, allDatasets);
}
}

View file

@ -0,0 +1,371 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<ServiceItemType>;
};
export default class ServicesManager {
navigation: StackNavigationProp<any>;
amicaleDataset: Array<ServiceItemType>;
studentsDataset: Array<ServiceItemType>;
insaDataset: Array<ServiceItemType>;
specialDataset: Array<ServiceItemType>;
categoriesDataset: Array<ServiceCategoryType>;
constructor(nav: StackNavigationProp<any>) {
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<ServiceItemType>}
*/
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
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<ServiceItemType>}
*/
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
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<ServiceItemType>}
*/
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
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<ServiceItemType>}
*/
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
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<ServiceCategoryType>}
*/
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.categoriesDataset);
}
return this.categoriesDataset;
}
}

View file

@ -1,4 +1,28 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { DarkTheme, DefaultTheme } from 'react-native-paper'; 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 { declare global {
namespace ReactNativePaper { namespace ReactNativePaper {
@ -59,7 +83,7 @@ declare global {
} }
} }
export const CustomWhiteTheme: ReactNativePaper.Theme = { const CustomWhiteTheme: ReactNativePaper.Theme = {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
...DefaultTheme.colors, ...DefaultTheme.colors,
@ -118,7 +142,7 @@ export const CustomWhiteTheme: ReactNativePaper.Theme = {
}, },
}; };
export const CustomDarkTheme: ReactNativePaper.Theme = { const CustomDarkTheme: ReactNativePaper.Theme = {
...DarkTheme, ...DarkTheme,
colors: { colors: {
...DarkTheme.colors, ...DarkTheme.colors,
@ -176,3 +200,99 @@ export const CustomDarkTheme: ReactNativePaper.Theme = {
mascotMessageArrow: '#323232', 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();
}
}
}

View file

@ -46,23 +46,16 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import GameStartScreen from '../screens/Game/screens/GameStartScreen';
import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen'; import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
import { usePreferences } from '../context/preferencesContext';
import {
getPreferenceBool,
GeneralPreferenceKeys,
} from '../utils/asyncStorage';
import IntroScreen from '../screens/Intro/IntroScreen';
export enum MainRoutes { export enum MainRoutes {
Main = 'main', Main = 'main',
Intro = 'Intro',
Gallery = 'gallery', Gallery = 'gallery',
Settings = 'settings', Settings = 'settings',
DashboardEdit = 'dashboard-edit', DashboardEdit = 'dashboard-edit',
About = 'about', About = 'about',
Dependencies = 'dependencies', Dependencies = 'dependencies',
Debug = 'debug', Debug = 'debug',
GameStart = 'game', GameStart = 'game-start',
GameMain = 'game-main', GameMain = 'game-main',
Login = 'login', Login = 'login',
SelfMenu = 'self-menu', SelfMenu = 'self-menu',
@ -73,12 +66,11 @@ export enum MainRoutes {
ClubList = 'club-list', ClubList = 'club-list',
ClubInformation = 'club-information', ClubInformation = 'club-information',
ClubAbout = 'club-about', ClubAbout = 'club-about',
EquipmentList = 'equipment', EquipmentList = 'equipment-list',
EquipmentRent = 'equipment-rent', EquipmentRent = 'equipment-rent',
EquipmentConfirm = 'equipment-confirm', EquipmentConfirm = 'equipment-confirm',
Vote = 'vote', Vote = 'vote',
Feedback = 'feedback', Feedback = 'feedback',
Website = 'website',
} }
type DefaultParams = { [key in MainRoutes]: object | undefined }; type DefaultParams = { [key in MainRoutes]: object | undefined };
@ -104,23 +96,15 @@ export type MainStackParamsList = FullParamsList &
const MainStack = createStackNavigator<MainStackParamsList>(); const MainStack = createStackNavigator<MainStackParamsList>();
function getIntroScreens() { function MainStackComponent(props: {
createTabNavigator: () => React.ReactElement;
}) {
const { createTabNavigator } = props;
return ( return (
<> <MainStack.Navigator
<MainStack.Screen initialRouteName={MainRoutes.Main}
name={MainRoutes.Intro} headerMode={'screen'}
component={IntroScreen} >
options={{
headerShown: false,
}}
/>
</>
);
}
function getRegularScreens(createTabNavigator: () => React.ReactElement) {
return (
<>
<MainStack.Screen <MainStack.Screen
name={MainRoutes.Main} name={MainRoutes.Main}
component={createTabNavigator} component={createTabNavigator}
@ -199,7 +183,7 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
}} }}
/> />
<MainStack.Screen <MainStack.Screen
name={MainRoutes.Website} name={'website'}
component={WebsiteScreen} component={WebsiteScreen}
options={{ options={{
title: '', title: '',
@ -296,48 +280,19 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
title: i18n.t('screens.feedback.title'), title: i18n.t('screens.feedback.title'),
}} }}
/> />
</>
);
}
function MainStackComponent(props: {
showIntro: boolean;
createTabNavigator: () => React.ReactElement;
}) {
const { showIntro, createTabNavigator } = props;
return (
<MainStack.Navigator
initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
headerMode={'screen'}
>
{showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)}
</MainStack.Navigator> </MainStack.Navigator>
); );
} }
type PropsType = { type PropsType = {
defaultHomeRoute?: string; defaultHomeRoute: string | null;
defaultHomeData?: { [key: string]: string }; defaultHomeData: { [key: string]: string };
}; };
function MainNavigator(props: PropsType) { export default function MainNavigator(props: PropsType) {
const { preferences } = usePreferences();
const showIntro = getPreferenceBool(
GeneralPreferenceKeys.showIntro,
preferences
);
const createTabNavigator = () => <TabNavigator {...props} />;
return ( return (
<MainStackComponent <MainStackComponent
showIntro={showIntro !== false} createTabNavigator={() => <TabNavigator {...props} />}
createTabNavigator={createTabNavigator}
/> />
); );
} }
export default React.memo(
MainNavigator,
(pp: PropsType, np: PropsType) =>
pp.defaultHomeRoute === np.defaultHomeRoute &&
pp.defaultHomeData === np.defaultHomeData
);

View file

@ -31,6 +31,7 @@ import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen'; import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen'; import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
import PlanexScreen from '../screens/Planex/PlanexScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen';
import AsyncStorageManager from '../managers/AsyncStorageManager';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import ScannerScreen from '../screens/Home/ScannerScreen'; import ScannerScreen from '../screens/Home/ScannerScreen';
import FeedItemScreen from '../screens/Home/FeedItemScreen'; import FeedItemScreen from '../screens/Home/FeedItemScreen';
@ -40,11 +41,6 @@ import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
import { usePreferences } from '../context/preferencesContext';
import {
getPreferenceString,
GeneralPreferenceKeys,
} from '../utils/asyncStorage';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { header: {
@ -60,20 +56,6 @@ const styles = StyleSheet.create({
}, },
}); });
type DefaultParams = { [key in TabRoutes]: object | undefined };
export type FullParamsList = DefaultParams & {
[TabRoutes.Home]: {
nextScreen: string;
data: Record<string, object | undefined>;
};
};
// 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<string, object | undefined>;
const ServicesStack = createStackNavigator(); const ServicesStack = createStackNavigator();
function ServicesStackComponent() { function ServicesStackComponent() {
@ -139,8 +121,8 @@ function PlanningStackComponent() {
const HomeStack = createStackNavigator(); const HomeStack = createStackNavigator();
function HomeStackComponent( function HomeStackComponent(
initialRoute?: string, initialRoute: string | null,
defaultData?: { [key: string]: string } defaultData: { [key: string]: string }
) { ) {
let params; let params;
if (initialRoute) { if (initialRoute) {
@ -232,11 +214,11 @@ function PlanexStackComponent() {
); );
} }
const Tab = createBottomTabNavigator<TabStackParamsList>(); const Tab = createBottomTabNavigator();
type PropsType = { type PropsType = {
defaultHomeRoute?: string; defaultHomeRoute: string | null;
defaultHomeData?: { [key: string]: string }; defaultHomeData: { [key: string]: string };
}; };
const ICONS: { const ICONS: {
@ -257,7 +239,7 @@ const ICONS: {
normal: '', normal: '',
focused: '', focused: '',
}, },
events: { planning: {
normal: 'calendar-range-outline', normal: 'calendar-range-outline',
focused: 'calendar-range', focused: 'calendar-range',
}, },
@ -267,77 +249,65 @@ const ICONS: {
}, },
}; };
function TabNavigator(props: PropsType) { export default class TabNavigator extends React.Component<PropsType> {
const { preferences } = usePreferences(); defaultRoute: string;
let defaultRoute = getPreferenceString( createHomeStackComponent: () => any;
GeneralPreferenceKeys.defaultStartScreen,
preferences constructor(props: PropsType) {
); super(props);
if (!defaultRoute) { this.defaultRoute = 'home';
defaultRoute = 'home'; if (!props.defaultHomeRoute) {
} else { this.defaultRoute = AsyncStorageManager.getString(
defaultRoute = defaultRoute.toLowerCase(); AsyncStorageManager.PREFERENCES.defaultStartScreen.key
).toLowerCase();
}
this.createHomeStackComponent = () =>
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
} }
const createHomeStackComponent = () => render() {
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData); const LABELS: {
[key: string]: string;
const LABELS: { } = {
[key: string]: string; services: i18n.t('screens.services.title'),
} = { proxiwash: i18n.t('screens.proxiwash.title'),
services: i18n.t('screens.services.title'), home: i18n.t('screens.home.title'),
proxiwash: i18n.t('screens.proxiwash.title'), planning: i18n.t('screens.planning.title'),
home: i18n.t('screens.home.title'), planex: i18n.t('screens.planex.title'),
events: i18n.t('screens.planning.title'), };
planex: i18n.t('screens.planex.title'), return (
}; <Tab.Navigator
return ( initialRouteName={this.defaultRoute}
<Tab.Navigator tabBar={(tabProps) => (
initialRouteName={defaultRoute} <CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
tabBar={(tabProps) => ( )}
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} /> >
)} <Tab.Screen
> name={'services'}
<Tab.Screen component={ServicesStackComponent}
name={'services'} options={{ title: i18n.t('screens.services.title') }}
component={ServicesStackComponent} />
options={{ title: i18n.t('screens.services.title') }} <Tab.Screen
/> name={'proxiwash'}
<Tab.Screen component={ProxiwashStackComponent}
name={'proxiwash'} options={{ title: i18n.t('screens.proxiwash.title') }}
component={ProxiwashStackComponent} />
options={{ title: i18n.t('screens.proxiwash.title') }} <Tab.Screen
/> name={'home'}
<Tab.Screen component={this.createHomeStackComponent}
name={'home'} options={{ title: i18n.t('screens.home.title') }}
component={createHomeStackComponent} />
options={{ title: i18n.t('screens.home.title') }} <Tab.Screen
/> name={'planning'}
<Tab.Screen component={PlanningStackComponent}
name={'events'} options={{ title: i18n.t('screens.planning.title') }}
component={PlanningStackComponent} />
options={{ title: i18n.t('screens.planning.title') }} <Tab.Screen
/> name={'planex'}
<Tab.Screen component={PlanexStackComponent}
name={'planex'} options={{ title: i18n.t('screens.planex.title') }}
component={PlanexStackComponent} />
options={{ title: i18n.t('screens.planex.title') }} </Tab.Navigator>
/> );
</Tab.Navigator> }
);
}
export default React.memo(
TabNavigator,
(pp: PropsType, np: PropsType) =>
pp.defaultHomeRoute === np.defaultHomeRoute &&
pp.defaultHomeData === np.defaultHomeData
);
export enum TabRoutes {
Services = 'services',
Proxiwash = 'proxiwash',
Home = 'home',
Planning = 'events',
Planex = 'planex',
} }

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useRef, useState } from 'react'; import * as React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { import {
Button, Button,
@ -25,17 +25,12 @@ import {
Subheading, Subheading,
TextInput, TextInput,
Title, Title,
useTheme, withTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import CustomModal from '../../components/Overrides/CustomModal'; import CustomModal from '../../components/Overrides/CustomModal';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import { usePreferences } from '../../context/preferencesContext';
import {
defaultPreferences,
GeneralPreferenceKeys,
isValidGeneralPreferenceKey,
} from '../../utils/asyncStorage';
type PreferenceItemType = { type PreferenceItemType = {
key: string; key: string;
@ -43,6 +38,15 @@ type PreferenceItemType = {
current: string; current: string;
}; };
type PropsType = {
theme: ReactNativePaper.Theme;
};
type StateType = {
modalCurrentDisplayItem: PreferenceItemType | null;
currentPreferences: Array<PreferenceItemType>;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
@ -58,35 +62,47 @@ const styles = StyleSheet.create({
* Class defining the Debug screen. * Class defining the Debug screen.
* This screen allows the user to get and modify information on the app/device. * This screen allows the user to get and modify information on the app/device.
*/ */
function DebugScreen() { class DebugScreen extends React.Component<PropsType, StateType> {
const theme = useTheme(); modalRef: { current: Modalize | null };
const { preferences, updatePreferences } = usePreferences();
const modalRef = useRef<Modalize>(null);
const [modalInputValue, setModalInputValue] = useState<string>(''); modalInputValue: string;
const [
modalCurrentDisplayItem,
setModalCurrentDisplayItem,
] = useState<PreferenceItemType | null>(null);
const currentPreferences: Array<PreferenceItemType> = []; /**
Object.values(GeneralPreferenceKeys).forEach((key) => { * Copies user preferences to state for easier manipulation
const newObject: PreferenceItemType = { *
key: key, * @param props
current: preferences[key], */
default: defaultPreferences[key], constructor(props: PropsType) {
super(props);
this.modalRef = React.createRef<Modalize>();
this.modalInputValue = '';
const currentPreferences: Array<PreferenceItemType> = [];
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,
}; };
currentPreferences.push(newObject); }
});
const getModalContent = () => { /**
* Gets the edit modal content
*
* @return {*}
*/
getModalContent() {
const { props, state } = this;
let key = ''; let key = '';
let defaultValue = ''; let defaultValue = '';
let current = ''; let current = '';
if (modalCurrentDisplayItem) { if (state.modalCurrentDisplayItem) {
key = modalCurrentDisplayItem.key; key = state.modalCurrentDisplayItem.key;
defaultValue = modalCurrentDisplayItem.default; defaultValue = state.modalCurrentDisplayItem.default;
current = modalCurrentDisplayItem.current; defaultValue = state.modalCurrentDisplayItem.default;
current = state.modalCurrentDisplayItem.current;
} }
return ( return (
@ -94,14 +110,19 @@ function DebugScreen() {
<Title>{key}</Title> <Title>{key}</Title>
<Subheading>Default: {defaultValue}</Subheading> <Subheading>Default: {defaultValue}</Subheading>
<Subheading>Current: {current}</Subheading> <Subheading>Current: {current}</Subheading>
<TextInput label={'New Value'} onChangeText={setModalInputValue} /> <TextInput
label="New Value"
onChangeText={(text: string) => {
this.modalInputValue = text;
}}
/>
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
mode="contained" mode="contained"
dark dark
color={theme.colors.success} color={props.theme.colors.success}
onPress={() => { onPress={() => {
saveNewPrefs(key, modalInputValue); this.saveNewPrefs(key, this.modalInputValue);
}} }}
> >
Save new value Save new value
@ -109,9 +130,9 @@ function DebugScreen() {
<Button <Button
mode="contained" mode="contained"
dark dark
color={theme.colors.danger} color={props.theme.colors.danger}
onPress={() => { onPress={() => {
saveNewPrefs(key, defaultValue); this.saveNewPrefs(key, defaultValue);
}} }}
> >
Reset to default Reset to default
@ -119,46 +140,85 @@ function DebugScreen() {
</View> </View>
</View> </View>
); );
}; }
const getRenderItem = ({ item }: { item: PreferenceItemType }) => { getRenderItem = ({ item }: { item: PreferenceItemType }) => {
return ( return (
<List.Item <List.Item
title={item.key} title={item.key}
description="Click to edit" description="Click to edit"
onPress={() => { onPress={() => {
showEditModal(item); this.showEditModal(item);
}} }}
/> />
); );
}; };
const showEditModal = (item: PreferenceItemType) => { /**
setModalCurrentDisplayItem(item); * Shows the edit modal
if (modalRef.current) { *
modalRef.current.open(); * @param item
*/
showEditModal(item: PreferenceItemType) {
this.setState({
modalCurrentDisplayItem: item,
});
if (this.modalRef.current) {
this.modalRef.current.open();
} }
}; }
const saveNewPrefs = (key: string, value: string) => { /**
if (isValidGeneralPreferenceKey(key)) { * Finds the index of the given key in the preferences array
updatePreferences(key, value); *
* @param key THe key to find the index of
* @returns {number}
*/
findIndexOfKey(key: string): number {
const { currentPreferences } = this.state;
let index = -1;
for (let i = 0; i < currentPreferences.length; i += 1) {
if (currentPreferences[i].key === key) {
index = i;
break;
}
} }
if (modalRef.current) { return index;
modalRef.current.close(); }
}
};
return ( /**
<View> * Saves the new value of the given preference
<CustomModal ref={modalRef}>{getModalContent()}</CustomModal> *
<CollapsibleFlatList * @param key The pref key
data={currentPreferences} * @param value The pref value
extraData={currentPreferences} */
renderItem={getRenderItem} saveNewPrefs(key: string, value: string) {
/> this.setState((prevState: StateType): {
</View> currentPreferences: Array<PreferenceItemType>;
); } => {
const currentPreferences = [...prevState.currentPreferences];
currentPreferences[this.findIndexOfKey(key)].current = value;
return { currentPreferences };
});
AsyncStorageManager.set(key, value);
if (this.modalRef.current) {
this.modalRef.current.close();
}
}
render() {
const { state } = this;
return (
<View>
<CustomModal ref={this.modalRef}>{this.getModalContent()}</CustomModal>
<CollapsibleFlatList
data={state.currentPreferences}
extraData={state.currentPreferences}
renderItem={this.getRenderItem}
/>
</View>
);
}
} }
export default DebugScreen; export default withTheme(DebugScreen);

View file

@ -25,6 +25,7 @@ import i18n from 'i18n-js';
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem'; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import MascotPopup from '../../../components/Mascot/MascotPopup'; import MascotPopup from '../../../components/Mascot/MascotPopup';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import ConnectionManager from '../../../managers/ConnectionManager'; import ConnectionManager from '../../../managers/ConnectionManager';
import { ApiRejectType } from '../../../utils/WebData'; import { ApiRejectType } from '../../../utils/WebData';
@ -35,7 +36,7 @@ type PropsType = {
}; };
type StateType = { type StateType = {
mascotDialogVisible: boolean | undefined; mascotDialogVisible: boolean;
}; };
export type DeviceType = { export type DeviceType = {
@ -74,7 +75,9 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
super(props); super(props);
this.userRents = null; this.userRents = null;
this.state = { this.state = {
mascotDialogVisible: undefined, mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.equipmentShowMascot.key
),
}; };
} }
@ -142,6 +145,10 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.equipmentShowMascot.key,
false
);
this.setState({ mascotDialogVisible: false }); this.setState({ mascotDialogVisible: false });
}; };

View file

@ -31,6 +31,7 @@ import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import ErrorDialog from '../../components/Dialogs/ErrorDialog'; import ErrorDialog from '../../components/Dialogs/ErrorDialog';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
@ -55,7 +56,7 @@ type StateType = {
loading: boolean; loading: boolean;
dialogVisible: boolean; dialogVisible: boolean;
dialogError: ApiRejectType; dialogError: ApiRejectType;
mascotDialogVisible: boolean | undefined; mascotDialogVisible: boolean;
}; };
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
@ -117,7 +118,9 @@ class LoginScreen extends React.Component<Props, StateType> {
loading: false, loading: false,
dialogVisible: false, dialogVisible: false,
dialogError: { status: REQUEST_STATUS.SUCCESS }, dialogError: { status: REQUEST_STATUS.SUCCESS },
mascotDialogVisible: undefined, mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.loginShowMascot.key
),
}; };
} }
@ -318,6 +321,10 @@ class LoginScreen extends React.Component<Props, StateType> {
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.loginShowMascot.key,
false
);
this.setState({ mascotDialogVisible: false }); this.setState({ mascotDialogVisible: false });
}; };
@ -350,11 +357,10 @@ class LoginScreen extends React.Component<Props, StateType> {
handleSuccess = () => { handleSuccess = () => {
const { navigation } = this.props; const { navigation } = this.props;
// Do not show the home login banner again // Do not show the home login banner again
// TODO AsyncStorageManager.set(
// AsyncStorageManager.set( AsyncStorageManager.PREFERENCES.homeShowMascot.key,
// AsyncStorageManager.PREFERENCES.homeShowMascot.key, false
// false );
// );
if (this.nextScreen == null) { if (this.nextScreen == null) {
navigation.goBack(); navigation.goBack();
} else { } else {

View file

@ -36,16 +36,13 @@ import MaterialHeaderButtons, {
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import ServicesManager, { SERVICES_KEY } from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type { ServiceItemType } from '../../managers/ServicesManager';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import RequestScreen from '../../components/Screens/RequestScreen'; import RequestScreen from '../../components/Screens/RequestScreen';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import {
getAmicaleServices,
ServiceItemType,
SERVICES_KEY,
} from '../../utils/Services';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -103,9 +100,8 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
super(props); super(props);
this.data = undefined; this.data = undefined;
this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }]; this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [ const services = new ServicesManager(props.navigation);
SERVICES_KEY.PROFILE, this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
]);
this.state = { this.state = {
dialogVisible: false, dialogVisible: false,
}; };

View file

@ -28,6 +28,7 @@ import VoteResults from '../../components/Amicale/Vote/VoteResults';
import VoteWait from '../../components/Amicale/Vote/VoteWait'; import VoteWait from '../../components/Amicale/Vote/VoteWait';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable'; import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
@ -117,7 +118,7 @@ type PropsType = {};
type StateType = { type StateType = {
hasVoted: boolean; hasVoted: boolean;
mascotDialogVisible: boolean | undefined; mascotDialogVisible: boolean;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -153,7 +154,9 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
this.dates = undefined; this.dates = undefined;
this.state = { this.state = {
hasVoted: false, hasVoted: false,
mascotDialogVisible: undefined, mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.voteShowMascot.key
),
}; };
this.hasVoted = false; this.hasVoted = false;
this.today = new Date(); this.today = new Date();
@ -325,6 +328,10 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.voteShowMascot.key,
false
);
this.setState({ mascotDialogVisible: false }); this.setState({ mascotDialogVisible: false });
}; };

View file

@ -1,44 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { GamePodium } from './GamePodium';
type Props = {
scores: Array<number>;
isHighScore: boolean;
};
const styles = StyleSheet.create({
topScoreContainer: {
marginBottom: 20,
marginTop: 20,
},
topScoreSubcontainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
});
export default function FullGamePodium(props: Props) {
const { scores, isHighScore } = props;
const gold = scores.length > 0 ? scores[0] : '-';
const silver = scores.length > 1 ? scores[1] : '-';
const bronze = scores.length > 2 ? scores[2] : '-';
return (
<View style={styles.topScoreContainer}>
<GamePodium place={1} score={gold.toString()} isHighScore={isHighScore} />
<View style={styles.topScoreSubcontainer}>
<GamePodium
place={3}
score={bronze.toString()}
isHighScore={isHighScore}
/>
<GamePodium
place={2}
score={silver.toString()}
isHighScore={isHighScore}
/>
</View>
</View>
);
}

View file

@ -1,65 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import { useTheme } from 'react-native-paper';
import GridManager from '../logic/GridManager';
import Piece from '../logic/Piece';
import GridComponent from './GridComponent';
const styles = StyleSheet.create({
pieceContainer: {
position: 'absolute',
width: '100%',
height: '100%',
},
pieceBackground: {
position: 'absolute',
},
});
export default function GameBackground() {
const theme = useTheme();
const gridManager = new GridManager(4, 4, theme);
const gridList = [];
for (let i = 0; i < 18; i += 1) {
gridList.push(gridManager.getEmptyGrid(4, 4));
const piece = new Piece(theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={styles.pieceContainer}>
{gridList.map((item, index) => {
const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360);
const left = (index % 6) * 20;
const animDelay = size * 20;
const animDuration = 2 * (2000 - size * 30);
return (
<Animatable.View
useNativeDriver={true}
animation={'fadeInDownBig'}
delay={animDelay}
duration={animDuration}
key={`piece${index.toString()}`}
style={{
width: `${size}%`,
top: `${top}%`,
left: `${left}%`,
...styles.pieceBackground,
}}
>
<GridComponent
width={4}
height={4}
grid={item}
style={{
transform: [{ rotateZ: `${rot}deg` }],
}}
/>
</Animatable.View>
);
})}
</View>
);
}

View file

@ -1,62 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, useTheme } from 'react-native-paper';
import GENERAL_STYLES from '../../../constants/Styles';
import GameLogic, { MovementCallbackType } from '../logic/GameLogic';
type Props = {
logic: GameLogic;
onDirectionPressed: MovementCallbackType;
};
const styles = StyleSheet.create({
controlsContainer: {
height: 80,
flexDirection: 'row',
},
directionsContainer: {
flexDirection: 'row',
flex: 4,
},
});
function GameControls(props: Props) {
const { logic } = props;
const theme = useTheme();
return (
<View style={styles.controlsContainer}>
<IconButton
icon={'rotate-right-variant'}
size={40}
onPress={() => logic.rotatePressed(props.onDirectionPressed)}
style={GENERAL_STYLES.flex}
/>
<View style={styles.directionsContainer}>
<IconButton
icon={'chevron-left'}
size={40}
style={GENERAL_STYLES.flex}
onPress={() => logic.pressedOut()}
onPressIn={() => logic.leftPressedIn(props.onDirectionPressed)}
/>
<IconButton
icon={'chevron-right'}
size={40}
style={GENERAL_STYLES.flex}
onPress={() => logic.pressedOut()}
onPressIn={() => logic.rightPressed(props.onDirectionPressed)}
/>
</View>
<IconButton
icon={'arrow-down-bold'}
size={40}
onPressIn={() => logic.downPressedIn(props.onDirectionPressed)}
onPress={() => logic.pressedOut()}
style={GENERAL_STYLES.flex}
color={theme.colors.tetrisScore}
/>
</View>
);
}
export default React.memo(GameControls, () => true);

View file

@ -1,95 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from 'react-native-animatable';
type Props = {
place: 1 | 2 | 3;
score: string;
isHighScore: boolean;
};
const styles = StyleSheet.create({
podiumContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
},
podiumIconContainer: {
position: 'absolute',
top: -20,
},
centertext: {
textAlign: 'center',
},
});
export function GamePodium(props: Props) {
const { place, score, isHighScore } = props;
const theme = useTheme();
let icon = 'podium-gold';
let color = theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = 'podium-silver';
color = theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = 'podium-bronze';
color = theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
const marginLeft = place === 2 ? 20 : 'auto';
const marginRight = place === 3 ? 20 : 'auto';
const fontWeight = place === 1 ? 'bold' : undefined;
return (
<View
style={{
marginLeft: marginLeft,
marginRight: marginRight,
...styles.podiumContainer,
}}
>
{isHighScore && place === 1 ? (
<Animatable.View
animation="swing"
iterationCount="infinite"
duration={2000}
delay={1000}
useNativeDriver
style={styles.podiumIconContainer}
>
<Animatable.View
animation="pulse"
iterationCount="infinite"
useNativeDriver
>
<MaterialCommunityIcons
name="decagram"
color={theme.colors.gameGold}
size={150}
/>
</Animatable.View>
</Animatable.View>
) : null}
<MaterialCommunityIcons
name={icon}
color={isHighScore && place === 1 ? '#fff' : color}
size={size}
/>
<Text
style={{
fontWeight: fontWeight,
fontSize,
...styles.centertext,
}}
>
{score}
</Text>
</View>
);
}

View file

@ -1,81 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js';
import { Text, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles';
type Props = {
score: number;
highScore?: number;
};
const styles = StyleSheet.create({
scoreMainContainer: {
marginTop: 10,
marginBottom: 10,
},
scoreCurrentContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
scoreText: {
marginLeft: 5,
fontSize: 20,
},
scoreBestContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 5,
},
centerVerticalSmallMargin: {
...GENERAL_STYLES.centerVertical,
marginLeft: 5,
},
});
function GameScore(props: Props) {
const { score, highScore } = props;
const theme = useTheme();
const displayHighScore =
highScore == null || score > highScore ? score : highScore;
return (
<View style={styles.scoreMainContainer}>
<View style={styles.scoreCurrentContainer}>
<Text style={styles.scoreText}>
{i18n.t('screens.game.score', { score: score })}
</Text>
<MaterialCommunityIcons
name="star"
color={theme.colors.tetrisScore}
size={20}
style={styles.centerVerticalSmallMargin}
/>
</View>
<View style={styles.scoreBestContainer}>
<Text
style={{
...styles.scoreText,
color: theme.colors.textDisabled,
}}
>
{i18n.t('screens.game.highScore', { score: displayHighScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={theme.colors.tetrisScore}
size={10}
style={styles.centerVerticalSmallMargin}
/>
</View>
</View>
);
}
export default React.memo(
GameScore,
(pp, np) => pp.highScore === np.highScore && pp.score === np.score
);

View file

@ -1,96 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Caption, Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GENERAL_STYLES from '../../../constants/Styles';
import i18n from 'i18n-js';
type Props = {
time: number;
level: number;
};
const styles = StyleSheet.create({
centerSmallMargin: {
...GENERAL_STYLES.centerHorizontal,
marginBottom: 5,
},
centerBigMargin: {
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
},
statusContainer: {
flexDirection: 'row',
},
statusIcon: {
marginLeft: 5,
},
});
function getFormattedTime(seconds: number): string {
const date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours()) {
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
} else if (date.getMinutes()) {
format = `${date.getMinutes()}:${date.getSeconds()}`;
} else {
format = date.getSeconds().toString();
}
return format;
}
function GameStatus(props: Props) {
const theme = useTheme();
return (
<View
style={{
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerVertical,
}}
>
<View style={GENERAL_STYLES.centerHorizontal}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.time')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name={'timer'}
color={theme.colors.subtitle}
size={20}
/>
<Text
style={{
...styles.statusIcon,
color: theme.colors.subtitle,
}}
>
{getFormattedTime(props.time)}
</Text>
</View>
</View>
<View style={styles.centerBigMargin}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.level')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name={'gamepad-square'}
color={theme.colors.text}
size={20}
/>
<Text style={styles.statusIcon}>{props.level}</Text>
</View>
</View>
</View>
);
}
export default React.memo(
GameStatus,
(pp, np) => pp.level === np.level && pp.time === np.time
);

View file

@ -1,130 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Card, Divider, Headline, Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import GENERAL_STYLES from '../../../constants/Styles';
import i18n from 'i18n-js';
type GameStatsType = {
score: number;
level: number;
time: number;
};
type Props = {
isHighScore: boolean;
stats: GameStatsType;
};
const styles = StyleSheet.create({
recapCard: {
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
},
recapContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
recapScoreContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
},
recapScore: {
fontSize: 20,
},
recapScoreIcon: {
marginLeft: 5,
},
recapIcon: {
marginRight: 5,
marginLeft: 5,
},
centertext: {
textAlign: 'center',
},
});
export default function PostGameContent(props: Props) {
const { isHighScore, stats } = props;
const theme = useTheme();
const width = isHighScore ? '50%' : '30%';
const margin = isHighScore ? 'auto' : undefined;
const marginLeft = isHighScore ? '60%' : '20%';
const color = isHighScore ? theme.colors.gameGold : theme.colors.primary;
return (
<View style={GENERAL_STYLES.flex}>
<Mascot
emotion={isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
animated={isHighScore}
style={{
width: width,
marginLeft: margin,
marginRight: margin,
}}
/>
<SpeechArrow
style={{ marginLeft: marginLeft }}
size={20}
color={theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: theme.colors.mascotMessageArrow,
...styles.recapCard,
}}
>
<Card.Content>
<Headline
style={{
color: color,
...styles.centertext,
}}
>
{isHighScore
? i18n.t('screens.game.newHighScore')
: i18n.t('screens.game.gameOver')}
</Headline>
<Divider />
<View style={styles.recapScoreContainer}>
<Text style={styles.recapScore}>
{i18n.t('screens.game.score', { score: stats.score })}
</Text>
<MaterialCommunityIcons
name={'star'}
color={theme.colors.tetrisScore}
size={30}
style={styles.recapScoreIcon}
/>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.level')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name={'gamepad-square'}
size={20}
color={theme.colors.textDisabled}
/>
<Text>{stats.level}</Text>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.time')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name={'timer'}
size={20}
color={theme.colors.textDisabled}
/>
<Text>{stats.time}</Text>
</View>
</Card.Content>
</Card>
</View>
);
}

View file

@ -1,70 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
Card,
Divider,
Headline,
Paragraph,
useTheme,
} from 'react-native-paper';
import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import i18n from 'i18n-js';
const styles = StyleSheet.create({
welcomeMascot: {
width: '40%',
marginLeft: 'auto',
marginRight: 'auto',
},
welcomeCard: {
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
},
speechArrow: {
marginLeft: '60%',
},
welcomeText: {
textAlign: 'center',
marginTop: 10,
},
centertext: {
textAlign: 'center',
},
});
export default function WelcomeGameContent() {
const theme = useTheme();
return (
<View>
<Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
<SpeechArrow
style={styles.speechArrow}
size={20}
color={theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: theme.colors.mascotMessageArrow,
...styles.welcomeCard,
}}
>
<Card.Content>
<Headline
style={{
color: theme.colors.primary,
...styles.centertext,
}}
>
{i18n.t('screens.game.welcomeTitle')}
</Headline>
<Divider />
<Paragraph style={styles.welcomeText}>
{i18n.t('screens.game.welcomeMessage')}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}

View file

@ -17,9 +17,10 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; import * as React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { useTheme } from 'react-native-paper'; import { Caption, IconButton, Text, withTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import GameLogic from '../logic/GameLogic'; import GameLogic from '../logic/GameLogic';
@ -32,16 +33,25 @@ import MaterialHeaderButtons, {
import type { OptionsDialogButtonType } from '../../../components/Dialogs/OptionsDialog'; import type { OptionsDialogButtonType } from '../../../components/Dialogs/OptionsDialog';
import OptionsDialog from '../../../components/Dialogs/OptionsDialog'; import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { MainRoutes } from '../../../navigation/MainNavigator';
import GameStatus from '../components/GameStatus'; type PropsType = {
import GameControls from '../components/GameControls'; navigation: StackNavigationProp<any>;
import GameScore from '../components/GameScore'; route: { params: { highScore: number } };
import { usePreferences } from '../../../context/preferencesContext'; theme: ReactNativePaper.Theme;
import { };
getPreferenceObject,
PreferenceKeys, type StateType = {
} from '../../../utils/asyncStorage'; grid: GridType;
import { useFocusEffect, useNavigation } from '@react-navigation/core'; gameTime: number;
gameScore: number;
gameLevel: number;
dialogVisible: boolean;
dialogTitle: string;
dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>;
onDialogDismiss: () => void;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -51,6 +61,44 @@ const styles = StyleSheet.create({
gridContainer: { gridContainer: {
flex: 4, flex: 4,
}, },
centerSmallMargin: {
...GENERAL_STYLES.centerHorizontal,
marginBottom: 5,
},
centerVerticalSmallMargin: {
...GENERAL_STYLES.centerVertical,
marginLeft: 5,
},
centerBigMargin: {
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
},
statusContainer: {
flexDirection: 'row',
},
statusIcon: {
marginLeft: 5,
},
scoreMainContainer: {
marginTop: 10,
marginBottom: 10,
},
scoreCurrentContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
scoreText: {
marginLeft: 5,
fontSize: 20,
},
scoreBestContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 5,
},
controlsContainer: { controlsContainer: {
height: 80, height: 80,
flexDirection: 'row', flexDirection: 'row',
@ -66,133 +114,273 @@ const styles = StyleSheet.create({
}, },
}); });
export default function GameMainScreen() { class GameMainScreen extends React.Component<PropsType, StateType> {
const theme = useTheme(); static getFormattedTime(seconds: number): string {
const navigation = useNavigation<StackNavigationProp<any>>(); const date = new Date();
const logic = useRef(new GameLogic(20, 10, theme)); date.setHours(0);
date.setMinutes(0);
const [gameTime, setGameTime] = useState(0); date.setSeconds(seconds);
let format;
const [gameState, setGameState] = useState({ if (date.getHours()) {
grid: logic.current.getCurrentGrid(), format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
gameScore: 0, } else if (date.getMinutes()) {
gameLevel: 0, format = `${date.getMinutes()}:${date.getSeconds()}`;
});
const [dialogContent, setDialogContent] = useState<{
dialogTitle: string;
dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>;
onDialogDismiss: () => void;
}>();
const { preferences, updatePreferences } = usePreferences();
function getScores() {
const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
| Array<number>
| undefined;
if (pref) {
return pref.sort((a, b) => b - a);
} else { } else {
return []; format = date.getSeconds().toString();
}
return format;
}
logic: GameLogic;
highScore: number | null;
constructor(props: PropsType) {
super(props);
this.highScore = null;
this.logic = new GameLogic(20, 10, props.theme);
this.state = {
grid: this.logic.getCurrentGrid(),
gameTime: 0,
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: '',
dialogMessage: '',
dialogButtons: [],
onDialogDismiss: () => {},
};
if (props.route.params != null) {
this.highScore = props.route.params.highScore;
} }
} }
const savedScores = getScores(); componentDidMount() {
const highScore = savedScores.length > 0 ? savedScores[0] : undefined; const { navigation } = this.props;
useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerRight: getRightButton, headerRight: this.getRightButton,
}); });
startGame(); this.startGame();
// eslint-disable-next-line react-hooks/exhaustive-deps }
}, [navigation]);
useFocusEffect( componentWillUnmount() {
useCallback(() => { this.logic.endGame(true);
const l = logic.current; }
return () => l.endGame(true);
}, [])
);
const getRightButton = () => ( getRightButton = () => {
<MaterialHeaderButtons> return (
<Item title={'pause'} iconName={'pause'} onPress={togglePause} /> <MaterialHeaderButtons>
</MaterialHeaderButtons> <Item title="pause" iconName="pause" onPress={this.togglePause} />
); </MaterialHeaderButtons>
);
};
const onTick = (score: number, level: number, newGrid: GridType) => { onTick = (score: number, level: number, newGrid: GridType) => {
setGameState({ this.setState({
gameScore: score, gameScore: score,
gameLevel: level, gameLevel: level,
grid: newGrid, grid: newGrid,
}); });
}; };
const onDialogDismiss = () => setDialogContent(undefined); onClock = (time: number) => {
this.setState({
gameTime: time,
});
};
const onGameEnd = (time: number, score: number, isRestart: boolean) => { onDialogDismiss = () => {
setGameState((prevState) => ({ this.setState({ dialogVisible: false });
...prevState, };
onGameEnd = (time: number, score: number, isRestart: boolean) => {
const { props, state } = this;
this.setState({
gameTime: time,
gameScore: score, gameScore: score,
})); });
setGameTime(time);
const newScores = [...savedScores];
const isHighScore = newScores.length === 0 || score > newScores[0];
for (let i = 0; i < 3; i += 1) {
if (newScores.length > i && score > newScores[i]) {
newScores.splice(i, 0, score);
break;
} else if (newScores.length <= i) {
newScores.push(score);
break;
}
}
if (newScores.length > 3) {
newScores.splice(3, 1);
}
if (newScores.some((item, i) => item !== savedScores[i])) {
updatePreferences(PreferenceKeys.gameScores, newScores);
}
if (!isRestart) { if (!isRestart) {
navigation.replace(MainRoutes.GameStart, { props.navigation.replace('game-start', {
score: score, score: state.gameScore,
level: gameState.gameLevel, level: state.gameLevel,
time: time, time: state.gameTime,
isHighScore: isHighScore,
}); });
} }
}; };
const onDirectionPressed = (newGrid: GridType, score?: number) => { getStatusIcons() {
setGameState((prevState) => ({ const { props, state } = this;
...prevState, return (
<View
style={{
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerVertical,
}}
>
<View style={GENERAL_STYLES.centerHorizontal}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.time')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name="timer"
color={props.theme.colors.subtitle}
size={20}
/>
<Text
style={{
...styles.statusIcon,
color: props.theme.colors.subtitle,
}}
>
{GameMainScreen.getFormattedTime(state.gameTime)}
</Text>
</View>
</View>
<View style={styles.centerBigMargin}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.level')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name="gamepad-square"
color={props.theme.colors.text}
size={20}
/>
<Text style={styles.statusIcon}>{state.gameLevel}</Text>
</View>
</View>
</View>
);
}
getScoreIcon() {
const { props, state } = this;
const highScore =
this.highScore == null || state.gameScore > this.highScore
? state.gameScore
: this.highScore;
return (
<View style={styles.scoreMainContainer}>
<View style={styles.scoreCurrentContainer}>
<Text style={styles.scoreText}>
{i18n.t('screens.game.score', { score: state.gameScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={20}
style={styles.centerVerticalSmallMargin}
/>
</View>
<View style={styles.scoreBestContainer}>
<Text
style={{
...styles.scoreText,
color: props.theme.colors.textDisabled,
}}
>
{i18n.t('screens.game.highScore', { score: highScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={10}
style={styles.centerVerticalSmallMargin}
/>
</View>
</View>
);
}
getControlButtons() {
const { props } = this;
return (
<View style={styles.controlsContainer}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => {
this.logic.rotatePressed(this.updateGrid);
}}
style={GENERAL_STYLES.flex}
/>
<View style={styles.directionsContainer}>
<IconButton
icon="chevron-left"
size={40}
style={GENERAL_STYLES.flex}
onPress={() => {
this.logic.pressedOut();
}}
onPressIn={() => {
this.logic.leftPressedIn(this.updateGrid);
}}
/>
<IconButton
icon="chevron-right"
size={40}
style={GENERAL_STYLES.flex}
onPress={() => {
this.logic.pressedOut();
}}
onPressIn={() => {
this.logic.rightPressed(this.updateGrid);
}}
/>
</View>
<IconButton
icon="arrow-down-bold"
size={40}
onPressIn={() => {
this.logic.downPressedIn(this.updateGridScore);
}}
onPress={() => {
this.logic.pressedOut();
}}
style={GENERAL_STYLES.flex}
color={props.theme.colors.tetrisScore}
/>
</View>
);
}
updateGrid = (newGrid: GridType) => {
this.setState({
grid: newGrid,
});
};
updateGridScore = (newGrid: GridType, score?: number) => {
this.setState((prevState: StateType): {
grid: GridType;
gameScore: number;
} => ({
grid: newGrid, grid: newGrid,
gameScore: score != null ? score : prevState.gameScore, gameScore: score != null ? score : prevState.gameScore,
})); }));
}; };
const togglePause = () => { togglePause = () => {
logic.current.togglePause(); this.logic.togglePause();
if (logic.current.isGamePaused()) { if (this.logic.isGamePaused()) {
showPausePopup(); this.showPausePopup();
} }
}; };
const showPausePopup = () => { showPausePopup = () => {
const onDismiss = () => { const onDismiss = () => {
togglePause(); this.togglePause();
onDialogDismiss(); this.onDialogDismiss();
}; };
setDialogContent({ this.setState({
dialogVisible: true,
dialogTitle: i18n.t('screens.game.pause'), dialogTitle: i18n.t('screens.game.pause'),
dialogMessage: i18n.t('screens.game.pauseMessage'), dialogMessage: i18n.t('screens.game.pauseMessage'),
dialogButtons: [ dialogButtons: [
{ {
title: i18n.t('screens.game.restart.text'), title: i18n.t('screens.game.restart.text'),
onPress: showRestartConfirm, onPress: this.showRestartConfirm,
}, },
{ {
title: i18n.t('screens.game.resume'), title: i18n.t('screens.game.resume'),
@ -203,68 +391,71 @@ export default function GameMainScreen() {
}); });
}; };
const showRestartConfirm = () => { showRestartConfirm = () => {
setDialogContent({ this.setState({
dialogVisible: true,
dialogTitle: i18n.t('screens.game.restart.confirm'), dialogTitle: i18n.t('screens.game.restart.confirm'),
dialogMessage: i18n.t('screens.game.restart.confirmMessage'), dialogMessage: i18n.t('screens.game.restart.confirmMessage'),
dialogButtons: [ dialogButtons: [
{ {
title: i18n.t('screens.game.restart.confirmYes'), title: i18n.t('screens.game.restart.confirmYes'),
onPress: () => { onPress: () => {
onDialogDismiss(); this.onDialogDismiss();
startGame(); this.startGame();
}, },
}, },
{ {
title: i18n.t('screens.game.restart.confirmNo'), title: i18n.t('screens.game.restart.confirmNo'),
onPress: showPausePopup, onPress: this.showPausePopup,
}, },
], ],
onDialogDismiss: showPausePopup, onDialogDismiss: this.showPausePopup,
}); });
}; };
const startGame = () => { startGame = () => {
logic.current.startGame(onTick, setGameTime, onGameEnd); this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
}; };
return ( render() {
<View style={GENERAL_STYLES.flex}> const { props, state } = this;
<View style={styles.container}> return (
<GameStatus time={gameTime} level={gameState.gameLevel} /> <View style={GENERAL_STYLES.flex}>
<View style={styles.gridContainer}> <View style={styles.container}>
<GameScore score={gameState.gameScore} highScore={highScore} /> {this.getStatusIcons()}
<GridComponent <View style={styles.gridContainer}>
width={logic.current.getWidth()} {this.getScoreIcon()}
height={logic.current.getHeight()} <GridComponent
grid={gameState.grid} width={this.logic.getWidth()}
style={{ height={this.logic.getHeight()}
backgroundColor: theme.colors.tetrisBackground, grid={state.grid}
...GENERAL_STYLES.flex, style={{
...GENERAL_STYLES.centerHorizontal, backgroundColor: props.theme.colors.tetrisBackground,
}} ...GENERAL_STYLES.flex,
/> ...GENERAL_STYLES.centerHorizontal,
}}
/>
</View>
<View style={GENERAL_STYLES.flex}>
<Preview
items={this.logic.getNextPiecesPreviews()}
style={styles.preview}
/>
</View>
</View> </View>
<View style={GENERAL_STYLES.flex}> {this.getControlButtons()}
<Preview
items={logic.current.getNextPiecesPreviews()}
style={styles.preview}
/>
</View>
</View>
<GameControls
logic={logic.current}
onDirectionPressed={onDirectionPressed}
/>
{dialogContent ? (
<OptionsDialog <OptionsDialog
visible={dialogContent !== undefined} visible={state.dialogVisible}
title={dialogContent.dialogTitle} title={state.dialogTitle}
message={dialogContent.dialogMessage} message={state.dialogMessage}
buttons={dialogContent.dialogButtons} buttons={state.dialogButtons}
onDismiss={dialogContent.onDialogDismiss} onDismiss={state.onDialogDismiss}
/> />
) : null} </View>
</View> );
); }
} }
export default withTheme(GameMainScreen);

View file

@ -18,121 +18,477 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Button, useTheme } from 'react-native-paper'; import { StackNavigationProp } from '@react-navigation/stack';
import {
Button,
Card,
Divider,
Headline,
Paragraph,
Text,
withTheme,
} from 'react-native-paper';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import MascotPopup from '../../../components/Mascot/MascotPopup'; import MascotPopup from '../../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import type { GridType } from '../components/GridComponent';
import GridComponent from '../components/GridComponent';
import GridManager from '../logic/GridManager';
import Piece from '../logic/Piece';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import GameBackground from '../components/GameBrackground';
import PostGameContent from '../components/PostGameContent';
import WelcomeGameContent from '../components/WelcomeGameContent';
import FullGamePodium from '../components/FullGamePodium';
import { useNavigation } from '@react-navigation/core';
import { usePreferences } from '../../../context/preferencesContext';
import {
getPreferenceObject,
PreferenceKeys,
} from '../../../utils/asyncStorage';
import { StackNavigationProp } from '@react-navigation/stack';
type GameStatsType = { type GameStatsType = {
score: number; score: number;
level: number; level: number;
time: number; time: number;
isHighScore: boolean;
}; };
type Props = { type PropsType = {
navigation: StackNavigationProp<any>;
route: { route: {
params?: GameStatsType; params: GameStatsType;
}; };
theme: ReactNativePaper.Theme;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
pieceContainer: {
position: 'absolute',
width: '100%',
height: '100%',
},
pieceBackground: {
position: 'absolute',
},
playButton: { playButton: {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginTop: 10, marginTop: 10,
}, },
recapCard: {
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
},
recapContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
recapScoreContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
},
recapScore: {
fontSize: 20,
},
recapScoreIcon: {
marginLeft: 5,
},
recapIcon: {
marginRight: 5,
marginLeft: 5,
},
welcomeMascot: {
width: '40%',
marginLeft: 'auto',
marginRight: 'auto',
},
welcomeCard: {
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
},
centertext: {
textAlign: 'center',
},
welcomeText: {
textAlign: 'center',
marginTop: 10,
},
speechArrow: {
marginLeft: '60%',
},
podiumContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
},
podiumIconContainer: {
position: 'absolute',
top: -20,
},
topScoreContainer: {
marginBottom: 20,
marginTop: 20,
},
topScoreSubcontainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
}); });
export default function GameStartScreen(props: Props) { class GameStartScreen extends React.Component<PropsType> {
const theme = useTheme(); gridManager: GridManager;
const navigation = useNavigation<StackNavigationProp<any>>();
const { preferences } = usePreferences(); scores: Array<number>;
function getScores() { gameStats?: GameStatsType;
const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
| Array<number> isHighScore: boolean;
| undefined;
if (pref) { constructor(props: PropsType) {
return pref.sort((a, b) => b - a); super(props);
} else { this.isHighScore = false;
return []; this.gridManager = new GridManager(4, 4, props.theme);
this.scores = AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.gameScores.key
);
this.scores.sort((a: number, b: number): number => b - a);
if (props.route.params != null) {
this.recoverGameScore();
} }
} }
const scores = getScores(); getPiecesBackground() {
const lastGameStats = props.route.params; const { theme } = this.props;
const gridList = [];
for (let i = 0; i < 18; i += 1) {
gridList.push(this.gridManager.getEmptyGrid(4, 4));
const piece = new Piece(theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={styles.pieceContainer}>
{gridList.map((item: GridType, index: number) => {
const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360);
const left = (index % 6) * 20;
const animDelay = size * 20;
const animDuration = 2 * (2000 - size * 30);
return (
<Animatable.View
useNativeDriver
animation="fadeInDownBig"
delay={animDelay}
duration={animDuration}
key={`piece${index.toString()}`}
style={{
width: `${size}%`,
top: `${top}%`,
left: `${left}%`,
...styles.pieceBackground,
}}
>
<GridComponent
width={4}
height={4}
grid={item}
style={{
transform: [{ rotateZ: `${rot}deg` }],
}}
/>
</Animatable.View>
);
})}
</View>
);
}
const getMainContent = () => { getPostGameContent(stats: GameStatsType) {
const { props } = this;
const width = this.isHighScore ? '50%' : '30%';
const margin = this.isHighScore ? 'auto' : undefined;
const marginLeft = this.isHighScore ? '60%' : '20%';
const color = this.isHighScore
? props.theme.colors.gameGold
: props.theme.colors.primary;
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
{lastGameStats ? ( <Mascot
<PostGameContent emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
stats={lastGameStats} animated={this.isHighScore}
isHighScore={lastGameStats.isHighScore} style={{
/> width: width,
) : ( marginLeft: margin,
<WelcomeGameContent /> marginRight: margin,
)} }}
/>
<SpeechArrow
style={{ marginLeft: marginLeft }}
size={20}
color={props.theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: props.theme.colors.mascotMessageArrow,
...styles.recapCard,
}}
>
<Card.Content>
<Headline
style={{
color: color,
...styles.centertext,
}}
>
{this.isHighScore
? i18n.t('screens.game.newHighScore')
: i18n.t('screens.game.gameOver')}
</Headline>
<Divider />
<View style={styles.recapScoreContainer}>
<Text style={styles.recapScore}>
{i18n.t('screens.game.score', { score: stats.score })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={30}
style={styles.recapScoreIcon}
/>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.level')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name="gamepad-square"
size={20}
color={props.theme.colors.textDisabled}
/>
<Text>{stats.level}</Text>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.time')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name="timer"
size={20}
color={props.theme.colors.textDisabled}
/>
<Text>{stats.time}</Text>
</View>
</Card.Content>
</Card>
</View>
);
}
getWelcomeText() {
const { props } = this;
return (
<View>
<Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
<SpeechArrow
style={styles.speechArrow}
size={20}
color={props.theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: props.theme.colors.mascotMessageArrow,
...styles.welcomeCard,
}}
>
<Card.Content>
<Headline
style={{
color: props.theme.colors.primary,
...styles.centertext,
}}
>
{i18n.t('screens.game.welcomeTitle')}
</Headline>
<Divider />
<Paragraph style={styles.welcomeText}>
{i18n.t('screens.game.welcomeMessage')}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}
getPodiumRender(place: 1 | 2 | 3, score: string) {
const { props } = this;
let icon = 'podium-gold';
let color = props.theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = 'podium-silver';
color = props.theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = 'podium-bronze';
color = props.theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
const marginLeft = place === 2 ? 20 : 'auto';
const marginRight = place === 3 ? 20 : 'auto';
const fontWeight = place === 1 ? 'bold' : undefined;
return (
<View
style={{
marginLeft: marginLeft,
marginRight: marginRight,
...styles.podiumContainer,
}}
>
{this.isHighScore && place === 1 ? (
<Animatable.View
animation="swing"
iterationCount="infinite"
duration={2000}
delay={1000}
useNativeDriver
style={styles.podiumIconContainer}
>
<Animatable.View
animation="pulse"
iterationCount="infinite"
useNativeDriver
>
<MaterialCommunityIcons
name="decagram"
color={props.theme.colors.gameGold}
size={150}
/>
</Animatable.View>
</Animatable.View>
) : null}
<MaterialCommunityIcons
name={icon}
color={this.isHighScore && place === 1 ? '#fff' : color}
size={size}
/>
<Text
style={{
fontWeight: fontWeight,
fontSize,
...styles.centertext,
}}
>
{score}
</Text>
</View>
);
}
getTopScoresRender() {
const gold = this.scores.length > 0 ? this.scores[0] : '-';
const silver = this.scores.length > 1 ? this.scores[1] : '-';
const bronze = this.scores.length > 2 ? this.scores[2] : '-';
return (
<View style={styles.topScoreContainer}>
{this.getPodiumRender(1, gold.toString())}
<View style={styles.topScoreSubcontainer}>
{this.getPodiumRender(3, bronze.toString())}
{this.getPodiumRender(2, silver.toString())}
</View>
</View>
);
}
getMainContent() {
const { props } = this;
return (
<View style={GENERAL_STYLES.flex}>
{this.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()}
<Button <Button
icon={'play'} icon="play"
mode={'contained'} mode="contained"
onPress={() => { onPress={() => {
navigation.replace('game-main'); props.navigation.replace('game-main', {
highScore: this.scores.length > 0 ? this.scores[0] : null,
});
}} }}
style={styles.playButton} style={styles.playButton}
> >
{i18n.t('screens.game.play')} {i18n.t('screens.game.play')}
</Button> </Button>
<FullGamePodium {this.getTopScoresRender()}
scores={scores}
isHighScore={lastGameStats?.isHighScore === true}
/>
</View> </View>
); );
}; }
return ( keyExtractor = (item: number): string => item.toString();
<View style={GENERAL_STYLES.flex}>
<GameBackground /> recoverGameScore() {
<LinearGradient const { route } = this.props;
style={GENERAL_STYLES.flex} this.gameStats = route.params;
colors={[`${theme.colors.background}00`, theme.colors.background]} if (this.gameStats.score != null) {
start={{ x: 0, y: 0 }} this.isHighScore =
end={{ x: 0, y: 1 }} this.scores.length === 0 || this.gameStats.score > this.scores[0];
> for (let i = 0; i < 3; i += 1) {
<CollapsibleScrollView headerColors={'transparent'}> if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
{getMainContent()} this.scores.splice(i, 0, this.gameStats.score);
<MascotPopup break;
title={i18n.t('screens.game.mascotDialog.title')} } else if (this.scores.length <= i) {
message={i18n.t('screens.game.mascotDialog.message')} this.scores.push(this.gameStats.score);
icon="gamepad-variant" break;
buttons={{ }
cancel: { }
message: i18n.t('screens.game.mascotDialog.button'), if (this.scores.length > 3) {
icon: 'check', this.scores.splice(3, 1);
}, }
}} AsyncStorageManager.set(
emotion={MASCOT_STYLE.COOL} AsyncStorageManager.PREFERENCES.gameScores.key,
/> this.scores
</CollapsibleScrollView> );
</LinearGradient> }
</View> }
);
render() {
const { props } = this;
return (
<View style={GENERAL_STYLES.flex}>
{this.getPiecesBackground()}
<LinearGradient
style={GENERAL_STYLES.flex}
colors={[
`${props.theme.colors.background}00`,
props.theme.colors.background,
]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<CollapsibleScrollView headerColors={'transparent'}>
{this.getMainContent()}
<MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.gameStartMascot.key}
title={i18n.t('screens.game.mascotDialog.title')}
message={i18n.t('screens.game.mascotDialog.message')}
icon="gamepad-variant"
buttons={{
cancel: {
message: i18n.t('screens.game.mascotDialog.button'),
icon: 'check',
},
}}
emotion={MASCOT_STYLE.COOL}
/>
</CollapsibleScrollView>
</LinearGradient>
</View>
);
}
} }
export default withTheme(GameStartScreen);

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useLayoutEffect, useRef, useState } from 'react'; import * as React from 'react';
import { import {
FlatList, FlatList,
NativeScrollEvent, NativeScrollEvent,
@ -26,13 +26,9 @@ import {
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Headline, useTheme } from 'react-native-paper'; import { Headline, withTheme } from 'react-native-paper';
import { import { CommonActions } from '@react-navigation/native';
CommonActions, import { StackNavigationProp } from '@react-navigation/stack';
useFocusEffect,
useNavigation,
} from '@react-navigation/native';
import { StackScreenProps } from '@react-navigation/stack';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
@ -48,17 +44,16 @@ import MaterialHeaderButtons, {
import AnimatedFAB from '../../components/Animations/AnimatedFAB'; import AnimatedFAB from '../../components/Animations/AnimatedFAB';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import LogoutDialog from '../../components/Amicale/LogoutDialog';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import DashboardManager from '../../managers/DashboardManager';
import type { ServiceItemType } from '../../managers/ServicesManager';
import { getDisplayEvent, getFutureEvents } from '../../utils/Home'; import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
import type { PlanningEventType } from '../../utils/Planning'; import type { PlanningEventType } from '../../utils/Planning';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
import { ServiceItemType } from '../../utils/Services';
import { useCurrentDashboard } from '../../context/preferencesContext';
import { MainRoutes } from '../../navigation/MainNavigator';
const FEED_ITEM_HEIGHT = 500; const FEED_ITEM_HEIGHT = 500;
@ -93,7 +88,15 @@ type RawDashboardType = {
dashboard: FullDashboardType; dashboard: FullDashboardType;
}; };
type Props = StackScreenProps<TabStackParamsList, TabRoutes.Home>; type PropsType = {
navigation: StackNavigationProp<any>;
route: { params: { nextScreen: string; data: object } };
theme: ReactNativePaper.Theme;
};
type StateType = {
dialogVisible: boolean;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dashboardRow: { dashboardRow: {
@ -124,91 +127,106 @@ const styles = StyleSheet.create({
}, },
}); });
const sortFeedTime = (a: FeedItemType, b: FeedItemType): number => /**
b.time - a.time; * Class defining the app's home screen
*/
class HomeScreen extends React.Component<PropsType, StateType> {
static sortFeedTime = (a: FeedItemType, b: FeedItemType): number =>
b.time - a.time;
const generateNewsFeed = (rawFeed: RawNewsFeedType): Array<FeedItemType> => { static generateNewsFeed(rawFeed: RawNewsFeedType): Array<FeedItemType> {
const finalFeed: Array<FeedItemType> = []; const finalFeed: Array<FeedItemType> = [];
Object.keys(rawFeed).forEach((key: string) => { Object.keys(rawFeed).forEach((key: string) => {
const category: Array<FeedItemType> | null = rawFeed[key]; const category: Array<FeedItemType> | null = rawFeed[key];
if (category != null && category.length > 0) { if (category != null && category.length > 0) {
finalFeed.push(...category); finalFeed.push(...category);
}
});
finalFeed.sort(sortFeedTime);
return finalFeed;
};
function HomeScreen(props: Props) {
const theme = useTheme();
const navigation = useNavigation();
const [dialogVisible, setDialogVisible] = useState(false);
const fabRef = useRef<AnimatedFAB>(null);
const [isLoggedIn, setIsLoggedIn] = useState(
ConnectionManager.getInstance().isLoggedIn()
);
const { currentDashboard } = useCurrentDashboard();
let homeDashboard: FullDashboardType | null = null;
useLayoutEffect(() => {
const getHeaderButton = () => {
let onPressLog = () =>
navigation.navigate('login', { nextScreen: 'profile' });
let logIcon = 'login';
let logColor = theme.colors.primary;
if (isLoggedIn) {
onPressLog = () => showDisconnectDialog();
logIcon = 'logout';
logColor = theme.colors.text;
} }
return (
<MaterialHeaderButtons>
<Item
title={'log'}
iconName={logIcon}
color={logColor}
onPress={onPressLog}
/>
<Item
title={i18n.t('screens.settings.title')}
iconName={'cog'}
onPress={() => navigation.navigate(MainRoutes.Settings)}
/>
</MaterialHeaderButtons>
);
};
navigation.setOptions({
headerRight: getHeaderButton,
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps finalFeed.sort(HomeScreen.sortFeedTime);
}, [navigation, isLoggedIn]); return finalFeed;
}
useFocusEffect( isLoggedIn: boolean | null;
React.useCallback(() => {
const handleNavigationParams = () => {
const { route } = props;
if (route.params != null) {
if (route.params.nextScreen != null) {
navigation.navigate(route.params.nextScreen, route.params.data);
// reset params to prevent infinite loop
navigation.dispatch(CommonActions.setParams({ nextScreen: null }));
}
}
};
if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) { fabRef: { current: null | AnimatedFAB };
setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
} currentNewFeed: Array<FeedItemType>;
// handle link open when home is not focused or created
handleNavigationParams(); currentDashboard: FullDashboardType | null;
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps dashboardManager: DashboardManager;
}, [isLoggedIn])
); constructor(props: PropsType) {
super(props);
this.fabRef = React.createRef();
this.dashboardManager = new DashboardManager(props.navigation);
this.currentNewFeed = [];
this.currentDashboard = null;
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
this.state = {
dialogVisible: false,
};
}
componentDidMount() {
const { props } = this;
props.navigation.addListener('focus', this.onScreenFocus);
// Handle link open when home is focused
props.navigation.addListener('state', this.handleNavigationParams);
}
/**
* Updates login state and navigation parameters on screen focus
*/
onScreenFocus = () => {
const { props } = this;
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
// handle link open when home is not focused or created
this.handleNavigationParams();
};
/**
* Gets header buttons based on login state
*
* @returns {*}
*/
getHeaderButton = () => {
const { props } = this;
let onPressLog = (): void =>
props.navigation.navigate('login', { nextScreen: 'profile' });
let logIcon = 'login';
let logColor = props.theme.colors.primary;
if (this.isLoggedIn) {
onPressLog = (): void => this.showDisconnectDialog();
logIcon = 'logout';
logColor = props.theme.colors.text;
}
const onPressSettings = (): void => props.navigation.navigate('settings');
return (
<MaterialHeaderButtons>
<Item
title="log"
iconName={logIcon}
color={logColor}
onPress={onPressLog}
/>
<Item
title={i18n.t('screens.settings.title')}
iconName="cog"
onPress={onPressSettings}
/>
</MaterialHeaderButtons>
);
};
/** /**
* Gets the event dashboard render item. * Gets the event dashboard render item.
@ -217,7 +235,7 @@ function HomeScreen(props: Props) {
* @param content * @param content
* @return {*} * @return {*}
*/ */
const getDashboardEvent = (content: Array<PlanningEventType>) => { getDashboardEvent(content: Array<PlanningEventType>) {
const futureEvents = getFutureEvents(content); const futureEvents = getFutureEvents(content);
const displayEvent = getDisplayEvent(futureEvents); const displayEvent = getDisplayEvent(futureEvents);
// const clickPreviewAction = () => // const clickPreviewAction = () =>
@ -228,15 +246,15 @@ function HomeScreen(props: Props) {
return ( return (
<DashboardItem <DashboardItem
eventNumber={futureEvents.length} eventNumber={futureEvents.length}
clickAction={onEventContainerClick} clickAction={this.onEventContainerClick}
> >
<PreviewEventDashboardItem <PreviewEventDashboardItem
event={displayEvent} event={displayEvent}
clickAction={onEventContainerClick} clickAction={this.onEventContainerClick}
/> />
</DashboardItem> </DashboardItem>
); );
}; }
/** /**
* Gets a dashboard item with a row of shortcut buttons. * Gets a dashboard item with a row of shortcut buttons.
@ -244,16 +262,16 @@ function HomeScreen(props: Props) {
* @param content * @param content
* @return {*} * @return {*}
*/ */
const getDashboardRow = (content: Array<ServiceItemType | null>) => { getDashboardRow(content: Array<ServiceItemType | null>) {
return ( return (
<FlatList <FlatList
data={content} data={content}
renderItem={getDashboardRowRenderItem} renderItem={this.getDashboardRowRenderItem}
horizontal horizontal
contentContainerStyle={styles.dashboardRow} contentContainerStyle={styles.dashboardRow}
/> />
); );
}; }
/** /**
* Gets a dashboard shortcut item * Gets a dashboard shortcut item
@ -261,19 +279,15 @@ function HomeScreen(props: Props) {
* @param item * @param item
* @returns {*} * @returns {*}
*/ */
const getDashboardRowRenderItem = ({ getDashboardRowRenderItem = ({ item }: { item: ServiceItemType | null }) => {
item,
}: {
item: ServiceItemType | null;
}) => {
if (item != null) { if (item != null) {
return ( return (
<SmallDashboardItem <SmallDashboardItem
image={item.image} image={item.image}
onPress={item.onPress} onPress={item.onPress}
badgeCount={ badgeCount={
homeDashboard != null && item.badgeFunction != null this.currentDashboard != null && item.badgeFunction != null
? item.badgeFunction(homeDashboard) ? item.badgeFunction(this.currentDashboard)
: undefined : undefined
} }
/> />
@ -282,13 +296,29 @@ function HomeScreen(props: Props) {
return <SmallDashboardItem />; return <SmallDashboardItem />;
}; };
const getRenderItem = ({ item }: { item: FeedItemType }) => ( /**
<FeedItem item={item} height={FEED_ITEM_HEIGHT} /> * Gets a render item for the given feed object
); *
* @param item The feed item to display
* @return {*}
*/
getFeedItem(item: FeedItemType) {
return <FeedItem item={item} height={FEED_ITEM_HEIGHT} />;
}
const getRenderSectionHeader = (data: { /**
* Gets a FlatList render item
*
* @param item The item to display
* @param section The current section
* @return {*}
*/
getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item);
getRenderSectionHeader = (data: {
section: SectionListData<FeedItemType>; section: SectionListData<FeedItemType>;
}) => { }) => {
const { props } = this;
const icon = data.section.icon; const icon = data.section.icon;
if (data.section.data.length > 0) { if (data.section.data.length > 0) {
return ( return (
@ -300,7 +330,7 @@ function HomeScreen(props: Props) {
<Headline <Headline
style={{ style={{
...styles.sectionHeaderEmpty, ...styles.sectionHeaderEmpty,
color: theme.colors.textDisabled, color: props.theme.colors.textDisabled,
}} }}
> >
{data.section.title} {data.section.title}
@ -309,7 +339,7 @@ function HomeScreen(props: Props) {
<MaterialCommunityIcons <MaterialCommunityIcons
name={icon} name={icon}
size={100} size={100}
color={theme.colors.textDisabled} color={props.theme.colors.textDisabled}
style={GENERAL_STYLES.center} style={GENERAL_STYLES.center}
/> />
) : null} ) : null}
@ -317,7 +347,7 @@ function HomeScreen(props: Props) {
); );
}; };
const getListHeader = (fetchedData: RawDashboardType | undefined) => { getListHeader = (fetchedData: RawDashboardType | undefined) => {
let dashboard = null; let dashboard = null;
if (fetchedData != null) { if (fetchedData != null) {
dashboard = fetchedData.dashboard; dashboard = fetchedData.dashboard;
@ -325,17 +355,41 @@ function HomeScreen(props: Props) {
return ( return (
<Animatable.View animation="fadeInDown" duration={500} useNativeDriver> <Animatable.View animation="fadeInDown" duration={500} useNativeDriver>
<ActionsDashBoardItem /> <ActionsDashBoardItem />
{getDashboardRow(currentDashboard)} {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())}
{getDashboardEvent(dashboard == null ? [] : dashboard.today_events)} {this.getDashboardEvent(
dashboard == null ? [] : dashboard.today_events
)}
</Animatable.View> </Animatable.View>
); );
}; };
const showDisconnectDialog = () => setDialogVisible(true); /**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => {
const { props } = this;
if (props.route.params != null) {
if (props.route.params.nextScreen != null) {
props.navigation.navigate(
props.route.params.nextScreen,
props.route.params.data
);
// reset params to prevent infinite loop
props.navigation.dispatch(
CommonActions.setParams({ nextScreen: null })
);
}
}
};
const hideDisconnectDialog = () => setDialogVisible(false); showDisconnectDialog = (): void => this.setState({ dialogVisible: true });
const openScanner = () => navigation.navigate('scanner'); hideDisconnectDialog = (): void => this.setState({ dialogVisible: false });
openScanner = () => {
const { props } = this;
props.navigation.navigate('scanner');
};
/** /**
* Creates the dataset to be used in the FlatList * Creates the dataset to be used in the FlatList
@ -344,7 +398,7 @@ function HomeScreen(props: Props) {
* @param isLoading * @param isLoading
* @return {*} * @return {*}
*/ */
const createDataset = ( createDataset = (
fetchedData: RawDashboardType | undefined, fetchedData: RawDashboardType | undefined,
isLoading: boolean isLoading: boolean
): Array<{ ): Array<{
@ -353,20 +407,21 @@ function HomeScreen(props: Props) {
icon?: string; icon?: string;
id: string; id: string;
}> => { }> => {
let currentNewFeed: Array<FeedItemType> = [];
if (fetchedData) { if (fetchedData) {
if (fetchedData.news_feed) { if (fetchedData.news_feed) {
currentNewFeed = generateNewsFeed(fetchedData.news_feed); this.currentNewFeed = HomeScreen.generateNewsFeed(
fetchedData.news_feed
);
} }
if (fetchedData.dashboard) { if (fetchedData.dashboard) {
homeDashboard = fetchedData.dashboard; this.currentDashboard = fetchedData.dashboard;
} }
} }
if (currentNewFeed.length > 0) { if (this.currentNewFeed.length > 0) {
return [ return [
{ {
title: i18n.t('screens.home.feedTitle'), title: i18n.t('screens.home.feedTitle'),
data: currentNewFeed, data: this.currentNewFeed,
id: SECTIONS_ID[1], id: SECTIONS_ID[1],
}, },
]; ];
@ -383,11 +438,14 @@ function HomeScreen(props: Props) {
]; ];
}; };
const onEventContainerClick = () => navigation.navigate(TabRoutes.Planning); onEventContainerClick = () => {
const { props } = this;
props.navigation.navigate('planning');
};
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (fabRef.current) { if (this.fabRef.current) {
fabRef.current.onScroll(event); this.fabRef.current.onScroll(event);
} }
}; };
@ -395,50 +453,63 @@ function HomeScreen(props: Props) {
* Callback when pressing the login button on the banner. * Callback when pressing the login button on the banner.
* This hides the banner and takes the user to the login page. * This hides the banner and takes the user to the login page.
*/ */
const onLogin = () => onLogin = () => {
navigation.navigate(MainRoutes.Login, { const { props } = this;
props.navigation.navigate('login', {
nextScreen: 'profile', nextScreen: 'profile',
}); });
};
return ( render() {
<View style={GENERAL_STYLES.flex}> const { props, state } = this;
<View style={styles.content}> return (
<WebSectionList <View style={GENERAL_STYLES.flex}>
request={() => readData<RawDashboardType>(Urls.app.dashboard)} <View style={styles.content}>
createDataset={createDataset} <WebSectionList
autoRefreshTime={REFRESH_TIME} request={() => readData<RawDashboardType>(Urls.app.dashboard)}
refreshOnFocus={true} createDataset={this.createDataset}
renderItem={getRenderItem} autoRefreshTime={REFRESH_TIME}
itemHeight={FEED_ITEM_HEIGHT} refreshOnFocus={true}
onScroll={onScroll} renderItem={this.getRenderItem}
renderSectionHeader={getRenderSectionHeader} itemHeight={FEED_ITEM_HEIGHT}
renderListHeaderComponent={getListHeader} onScroll={this.onScroll}
renderSectionHeader={this.getRenderSectionHeader}
renderListHeaderComponent={this.getListHeader}
/>
</View>
{!this.isLoggedIn ? (
<MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.homeShowMascot.key}
title={i18n.t('screens.home.mascotDialog.title')}
message={i18n.t('screens.home.mascotDialog.message')}
icon="human-greeting"
buttons={{
action: {
message: i18n.t('screens.home.mascotDialog.login'),
icon: 'login',
onPress: this.onLogin,
},
cancel: {
message: i18n.t('screens.home.mascotDialog.later'),
icon: 'close',
color: props.theme.colors.warning,
},
}}
emotion={MASCOT_STYLE.CUTE}
/>
) : null}
<AnimatedFAB
ref={this.fabRef}
icon="qrcode-scan"
onPress={this.openScanner}
/>
<LogoutDialog
visible={state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/> />
</View> </View>
{!isLoggedIn ? ( );
<MascotPopup }
title={i18n.t('screens.home.mascotDialog.title')}
message={i18n.t('screens.home.mascotDialog.message')}
icon="human-greeting"
buttons={{
action: {
message: i18n.t('screens.home.mascotDialog.login'),
icon: 'login',
onPress: onLogin,
},
cancel: {
message: i18n.t('screens.home.mascotDialog.later'),
icon: 'close',
color: theme.colors.warning,
},
}}
emotion={MASCOT_STYLE.CUTE}
/>
) : null}
<AnimatedFAB ref={fabRef} icon="qrcode-scan" onPress={openScanner} />
<LogoutDialog visible={dialogVisible} onDismiss={hideDisconnectDialog} />
</View>
);
} }
export default HomeScreen; export default withTheme(HomeScreen);

View file

@ -1,43 +0,0 @@
import React from 'react';
import CustomIntroSlider from '../../components/Overrides/CustomIntroSlider';
import Update from '../../constants/Update';
import { usePreferences } from '../../context/preferencesContext';
import AprilFoolsManager from '../../managers/AprilFoolsManager';
import {
getPreferenceBool,
getPreferenceNumber,
GeneralPreferenceKeys,
} from '../../utils/asyncStorage';
export default function IntroScreen() {
const { preferences, updatePreferences } = usePreferences();
const onDone = () => {
updatePreferences(GeneralPreferenceKeys.showIntro, false);
updatePreferences(GeneralPreferenceKeys.updateNumber, Update.number);
updatePreferences(GeneralPreferenceKeys.showAprilFoolsStart, false);
};
const showIntro =
getPreferenceBool(GeneralPreferenceKeys.showIntro, preferences) !== false;
const isUpdate =
getPreferenceNumber(GeneralPreferenceKeys.updateNumber, preferences) !==
Update.number && !showIntro;
const isAprilFools =
AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
getPreferenceBool(
GeneralPreferenceKeys.showAprilFoolsStart,
preferences
) !== false &&
!showIntro;
return (
<CustomIntroSlider
onDone={onDone}
isUpdate={isUpdate}
isAprilFools={isAprilFools}
/>
);
}

View file

@ -1,61 +0,0 @@
import React, { Ref, useEffect } from 'react';
import {
NavigationContainer,
NavigationContainerRef,
} from '@react-navigation/native';
import { Provider as PaperProvider } from 'react-native-paper';
import GENERAL_STYLES from '../constants/Styles';
import CollapsibleProvider from '../components/providers/CollapsibleProvider';
import CacheProvider from '../components/providers/CacheProvider';
import { OverflowMenuProvider } from 'react-navigation-header-buttons';
import MainNavigator from '../navigation/MainNavigator';
import { Platform, SafeAreaView, View } from 'react-native';
import { useDarkTheme } from '../context/preferencesContext';
import { CustomDarkTheme, CustomWhiteTheme } from '../utils/Themes';
import { setupStatusBar } from '../utils/Utils';
type Props = {
defaultHomeRoute?: string;
defaultHomeData?: { [key: string]: string };
};
function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
const darkTheme = useDarkTheme();
const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
useEffect(() => {
if (Platform.OS === 'ios') {
setTimeout(setupStatusBar, 1000);
} else {
setupStatusBar(theme);
}
}, [theme]);
return (
<PaperProvider theme={theme}>
<CollapsibleProvider>
<CacheProvider>
<OverflowMenuProvider>
<View
style={{
backgroundColor: theme.colors.background,
...GENERAL_STYLES.flex,
}}
>
<SafeAreaView style={GENERAL_STYLES.flex}>
<NavigationContainer theme={theme} ref={ref}>
<MainNavigator
defaultHomeRoute={props.defaultHomeRoute}
defaultHomeData={props.defaultHomeData}
/>
</NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
</CacheProvider>
</CollapsibleProvider>
</PaperProvider>
);
}
export default React.forwardRef(MainApp);

View file

@ -17,21 +17,31 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useRef, useState } from 'react'; import * as React from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import { Button, Card, Paragraph } from 'react-native-paper'; import { Button, Card, Paragraph } from 'react-native-paper';
import { FlatList, StyleSheet } from 'react-native'; import { FlatList, StyleSheet } from 'react-native';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import DashboardEditAccordion from '../../../components/Lists/DashboardEdit/DashboardEditAccordion'; import type {
import DashboardEditPreviewItem from '../../../components/Lists/DashboardEdit/DashboardEditPreviewItem';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import {
getCategories,
ServiceCategoryType, ServiceCategoryType,
ServiceItemType, ServiceItemType,
} from '../../../utils/Services'; } from '../../../managers/ServicesManager';
import { useNavigation } from '@react-navigation/core'; import DashboardManager from '../../../managers/DashboardManager';
import { useCurrentDashboard } from '../../../context/preferencesContext'; import DashboardEditAccordion from '../../../components/Lists/DashboardEdit/DashboardEditAccordion';
import DashboardEditPreviewItem from '../../../components/Lists/DashboardEdit/DashboardEditPreviewItem';
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
type PropsType = {
navigation: StackNavigationProp<any>;
};
type StateType = {
currentDashboard: Array<ServiceItemType | null>;
currentDashboardIdList: Array<string>;
activeItem: number;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dashboardContainer: { dashboardContainer: {
@ -61,71 +71,85 @@ const styles = StyleSheet.create({
/** /**
* Class defining the Settings screen. This screen shows controls to modify app preferences. * Class defining the Settings screen. This screen shows controls to modify app preferences.
*/ */
function DashboardEditScreen() { class DashboardEditScreen extends React.Component<PropsType, StateType> {
const navigation = useNavigation(); content: Array<ServiceCategoryType>;
const { initialDashboard: Array<ServiceItemType | null>;
currentDashboard,
currentDashboardIdList,
updateCurrentDashboard,
} = useCurrentDashboard();
const initialDashboard = useRef(currentDashboardIdList);
const [activeItem, setActiveItem] = useState(0);
const getDashboardRowRenderItem = ({ initialDashboardIdList: Array<string>;
constructor(props: PropsType) {
super(props);
const dashboardManager = new DashboardManager(props.navigation);
this.initialDashboardIdList = AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.dashboardItems.key
);
this.initialDashboard = dashboardManager.getCurrentDashboard();
this.state = {
currentDashboard: [...this.initialDashboard],
currentDashboardIdList: [...this.initialDashboardIdList],
activeItem: 0,
};
this.content = dashboardManager.getCategories();
}
getDashboardRowRenderItem = ({
item, item,
index, index,
}: { }: {
item: ServiceItemType | null; item: ServiceItemType | null;
index: number; index: number;
}) => { }) => {
const { activeItem } = this.state;
return ( return (
<DashboardEditPreviewItem <DashboardEditPreviewItem
image={item?.image} image={item?.image}
onPress={() => { onPress={() => {
setActiveItem(index); this.setState({ activeItem: index });
}} }}
isActive={activeItem === index} isActive={activeItem === index}
/> />
); );
}; };
const getDashboard = (content: Array<ServiceItemType | null>) => { getDashboard(content: Array<ServiceItemType | null>) {
return ( return (
<FlatList <FlatList
data={content} data={content}
extraData={activeItem} extraData={this.state}
renderItem={getDashboardRowRenderItem} renderItem={this.getDashboardRowRenderItem}
horizontal horizontal
contentContainerStyle={styles.dashboard} contentContainerStyle={styles.dashboard}
/> />
); );
}; }
const getRenderItem = ({ item }: { item: ServiceCategoryType }) => { getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
const { currentDashboardIdList } = this.state;
return ( return (
<DashboardEditAccordion <DashboardEditAccordion
item={item} item={item}
onPress={updateDashboard} onPress={this.updateDashboard}
activeDashboard={currentDashboardIdList} activeDashboard={currentDashboardIdList}
/> />
); );
}; };
const getListHeader = () => { getListHeader() {
const { currentDashboard } = this.state;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Content> <Card.Content>
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
mode={'contained'} mode="contained"
onPress={undoDashboard} onPress={this.undoDashboard}
style={styles.button} style={styles.button}
> >
{i18n.t('screens.settings.dashboardEdit.undo')} {i18n.t('screens.settings.dashboardEdit.undo')}
</Button> </Button>
<View style={styles.dashboardContainer}> <View style={styles.dashboardContainer}>
{getDashboard(currentDashboard)} {this.getDashboard(currentDashboard)}
</View> </View>
</View> </View>
<Paragraph style={styles.text}> <Paragraph style={styles.text}>
@ -134,28 +158,43 @@ function DashboardEditScreen() {
</Card.Content> </Card.Content>
</Card> </Card>
); );
}; }
const updateDashboard = (service: ServiceItemType) => { updateDashboard = (service: ServiceItemType) => {
updateCurrentDashboard( const { currentDashboard, currentDashboardIdList, activeItem } = this.state;
currentDashboardIdList.map((id, index) => currentDashboard[activeItem] = service;
index === activeItem ? service.key : id currentDashboardIdList[activeItem] = service.key;
) this.setState({
currentDashboard,
currentDashboardIdList,
});
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.dashboardItems.key,
currentDashboardIdList
); );
}; };
const undoDashboard = () => { undoDashboard = () => {
updateCurrentDashboard(initialDashboard.current); this.setState({
currentDashboard: [...this.initialDashboard],
currentDashboardIdList: [...this.initialDashboardIdList],
});
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.dashboardItems.key,
this.initialDashboardIdList
);
}; };
return ( render() {
<CollapsibleFlatList return (
data={getCategories(navigation.navigate)} <CollapsibleFlatList
renderItem={getRenderItem} data={this.content}
ListHeaderComponent={getListHeader()} renderItem={this.getRenderItem}
style={{}} ListHeaderComponent={this.getListHeader()}
/> style={{}}
); />
);
}
} }
export default DashboardEditScreen; export default DashboardEditScreen;

View file

@ -26,24 +26,28 @@ import {
List, List,
Switch, Switch,
ToggleButton, ToggleButton,
useTheme, withTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { Appearance } from 'react-native-appearance'; 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 CustomSlider from '../../../components/Overrides/CustomSlider';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import {
usePreferences, type PropsType = {
useProxiwashPreferences, navigation: StackNavigationProp<any>;
} from '../../../context/preferencesContext'; theme: ReactNativePaper.Theme;
import { useNavigation } from '@react-navigation/core'; };
import {
getPreferenceBool, type StateType = {
getPreferenceNumber, nightMode: boolean;
getPreferenceString, nightModeFollowSystem: boolean;
GeneralPreferenceKeys, startScreenPickerSelected: string;
ProxiwashPreferenceKeys, selectedWash: string;
} from '../../../utils/asyncStorage'; isDebugUnlocked: boolean;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
slider: { slider: {
@ -62,74 +66,98 @@ const styles = StyleSheet.create({
/** /**
* Class defining the Settings screen. This screen shows controls to modify app preferences. * Class defining the Settings screen. This screen shows controls to modify app preferences.
*/ */
function SettingsScreen() { class SettingsScreen extends React.Component<PropsType, StateType> {
const navigation = useNavigation(); savedNotificationReminder: number;
const theme = useTheme();
const generalPreferences = usePreferences();
const proxiwashPreferences = useProxiwashPreferences();
const nightMode = getPreferenceBool( /**
GeneralPreferenceKeys.nightMode, * Loads user preferences into state
generalPreferences.preferences */
) as boolean; constructor(props: PropsType) {
const nightModeFollowSystem = super(props);
(getPreferenceBool( const notifReminder = AsyncStorageManager.getString(
GeneralPreferenceKeys.nightModeFollowSystem, AsyncStorageManager.PREFERENCES.proxiwashNotifications.key
generalPreferences.preferences );
) as boolean) && Appearance.getColorScheme() !== 'no-preference'; this.savedNotificationReminder = parseInt(notifReminder, 10);
const startScreenPickerSelected = getPreferenceString( if (Number.isNaN(this.savedNotificationReminder)) {
GeneralPreferenceKeys.defaultStartScreen, this.savedNotificationReminder = 0;
generalPreferences.preferences }
) as string;
const selectedWash = getPreferenceString(
ProxiwashPreferenceKeys.selectedWash,
proxiwashPreferences.preferences
) as string;
const isDebugUnlocked = getPreferenceBool(
GeneralPreferenceKeys.debugUnlocked,
generalPreferences.preferences
) as boolean;
const notif = getPreferenceNumber(
ProxiwashPreferenceKeys.proxiwashNotifications,
proxiwashPreferences.preferences
);
const savedNotificationReminder = !notif || Number.isNaN(notif) ? 0 : notif;
const onProxiwashNotifPickerValueChange = (value: number) => { this.state = {
proxiwashPreferences.updatePreferences( nightMode: ThemeManager.getNightMode(),
ProxiwashPreferenceKeys.proxiwashNotifications, 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 value
); );
}; };
const onStartScreenPickerValueChange = (value: string) => { /**
* Saves the value for the proxiwash reminder notification time
*
* @param value The value to store
*/
onStartScreenPickerValueChange = (value: string) => {
if (value != null) { if (value != null) {
generalPreferences.updatePreferences( this.setState({ startScreenPickerSelected: value });
GeneralPreferenceKeys.defaultStartScreen, AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
value value
); );
} }
}; };
const getProxiwashNotifPicker = () => { /**
* Returns a picker allowing the user to select the proxiwash reminder notification time
*
* @returns {React.Node}
*/
getProxiwashNotifPicker() {
const { theme } = this.props;
return ( return (
<CustomSlider <CustomSlider
style={styles.slider} style={styles.slider}
minimumValue={0} minimumValue={0}
maximumValue={10} maximumValue={10}
step={1} step={1}
value={savedNotificationReminder} value={this.savedNotificationReminder}
onValueChange={onProxiwashNotifPickerValueChange} onValueChange={this.onProxiwashNotifPickerValueChange}
thumbTintColor={theme.colors.primary} thumbTintColor={theme.colors.primary}
minimumTrackTintColor={theme.colors.primary} minimumTrackTintColor={theme.colors.primary}
/> />
); );
}; }
const getProxiwashChangePicker = () => { /**
* Returns a radio picker allowing the user to select the proxiwash
*
* @returns {React.Node}
*/
getProxiwashChangePicker() {
const { selectedWash } = this.state;
return ( return (
<RadioButton.Group <RadioButton.Group
onValueChange={onSelectWashValueChange} onValueChange={this.onSelectWashValueChange}
value={selectedWash} value={selectedWash}
> >
<RadioButton.Item <RadioButton.Item
@ -142,12 +170,18 @@ function SettingsScreen() {
/> />
</RadioButton.Group> </RadioButton.Group>
); );
}; }
const getStartScreenPicker = () => { /**
* Returns a picker allowing the user to select the start screen
*
* @returns {React.Node}
*/
getStartScreenPicker() {
const { startScreenPickerSelected } = this.state;
return ( return (
<ToggleButton.Row <ToggleButton.Row
onValueChange={onStartScreenPickerValueChange} onValueChange={this.onStartScreenPickerValueChange}
value={startScreenPickerSelected} value={startScreenPickerSelected}
style={GENERAL_STYLES.centerHorizontal} style={GENERAL_STYLES.centerHorizontal}
> >
@ -158,20 +192,30 @@ function SettingsScreen() {
<ToggleButton icon="clock" value="planex" /> <ToggleButton icon="clock" value="planex" />
</ToggleButton.Row> </ToggleButton.Row>
); );
}
/**
* Toggles night mode and saves it to preferences
*/
onToggleNightMode = () => {
const { nightMode } = this.state;
ThemeManager.getInstance().setNightMode(!nightMode);
this.setState({ nightMode: !nightMode });
}; };
const onToggleNightMode = () => { onToggleNightModeFollowSystem = () => {
generalPreferences.updatePreferences( const { nightModeFollowSystem } = this.state;
GeneralPreferenceKeys.nightMode, const value = !nightModeFollowSystem;
!nightMode this.setState({ nightModeFollowSystem: value });
); AsyncStorageManager.set(
}; AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
value
const onToggleNightModeFollowSystem = () => {
generalPreferences.updatePreferences(
GeneralPreferenceKeys.nightModeFollowSystem,
!nightModeFollowSystem
); );
if (value) {
const nightMode = Appearance.getColorScheme() === 'dark';
ThemeManager.getInstance().setNightMode(nightMode);
this.setState({ nightMode });
}
}; };
/** /**
@ -184,13 +228,13 @@ function SettingsScreen() {
* @param state The current state of the switch * @param state The current state of the switch
* @returns {React.Node} * @returns {React.Node}
*/ */
const getToggleItem = ( static getToggleItem(
onPressCallback: () => void, onPressCallback: () => void,
icon: string, icon: string,
title: string, title: string,
subtitle: string, subtitle: string,
state: boolean state: boolean
) => { ) {
return ( return (
<List.Item <List.Item
title={title} title={title}
@ -201,15 +245,16 @@ function SettingsScreen() {
right={() => <Switch value={state} onValueChange={onPressCallback} />} right={() => <Switch value={state} onValueChange={onPressCallback} />}
/> />
); );
}; }
const getNavigateItem = ( getNavigateItem(
route: string, route: string,
icon: string, icon: string,
title: string, title: string,
subtitle: string, subtitle: string,
onLongPress?: () => void onLongPress?: () => void
) => { ) {
const { navigation } = this.props;
return ( return (
<List.Item <List.Item
title={title} title={title}
@ -230,127 +275,144 @@ function SettingsScreen() {
onLongPress={onLongPress} onLongPress={onLongPress}
/> />
); );
}; }
const onSelectWashValueChange = (value: string) => { /**
* Saves the value for the proxiwash selected wash
*
* @param value The value to store
*/
onSelectWashValueChange = (value: string) => {
if (value != null) { if (value != null) {
proxiwashPreferences.updatePreferences( this.setState({ selectedWash: value });
ProxiwashPreferenceKeys.selectedWash, AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.selectedWash.key,
value value
); );
} }
}; };
const unlockDebugMode = () => { /**
generalPreferences.updatePreferences( * Unlocks debug mode and saves its state to user preferences
GeneralPreferenceKeys.debugUnlocked, */
unlockDebugMode = () => {
this.setState({ isDebugUnlocked: true });
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.debugUnlocked.key,
true true
); );
}; };
return ( render() {
<CollapsibleScrollView> const { nightModeFollowSystem, nightMode, isDebugUnlocked } = this.state;
<Card style={styles.card}> return (
<Card.Title title={i18n.t('screens.settings.generalCard')} /> <CollapsibleScrollView>
<List.Section> <Card style={styles.card}>
{Appearance.getColorScheme() !== 'no-preference' <Card.Title title={i18n.t('screens.settings.generalCard')} />
? getToggleItem( <List.Section>
onToggleNightModeFollowSystem, {Appearance.getColorScheme() !== 'no-preference'
'theme-light-dark', ? SettingsScreen.getToggleItem(
i18n.t('screens.settings.nightModeAuto'), this.onToggleNightModeFollowSystem,
i18n.t('screens.settings.nightModeAutoSub'), 'theme-light-dark',
nightModeFollowSystem i18n.t('screens.settings.nightModeAuto'),
) i18n.t('screens.settings.nightModeAutoSub'),
: null} nightModeFollowSystem
{Appearance.getColorScheme() === 'no-preference' || )
!nightModeFollowSystem : null}
? getToggleItem( {Appearance.getColorScheme() === 'no-preference' ||
onToggleNightMode, !nightModeFollowSystem
'theme-light-dark', ? SettingsScreen.getToggleItem(
i18n.t('screens.settings.nightMode'), this.onToggleNightMode,
nightMode 'theme-light-dark',
? i18n.t('screens.settings.nightModeSubOn') i18n.t('screens.settings.nightMode'),
: i18n.t('screens.settings.nightModeSubOff'), nightMode
nightMode ? i18n.t('screens.settings.nightModeSubOn')
) : i18n.t('screens.settings.nightModeSubOff'),
: null} nightMode
<List.Item )
title={i18n.t('screens.settings.startScreen')} : null}
description={i18n.t('screens.settings.startScreenSub')} <List.Item
left={(props) => ( title={i18n.t('screens.settings.startScreen')}
<List.Icon color={props.color} style={props.style} icon="power" /> description={i18n.t('screens.settings.startScreenSub')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="power"
/>
)}
/>
{this.getStartScreenPicker()}
{this.getNavigateItem(
'dashboard-edit',
'view-dashboard',
i18n.t('screens.settings.dashboard'),
i18n.t('screens.settings.dashboardSub')
)} )}
/> </List.Section>
{getStartScreenPicker()} </Card>
{getNavigateItem( <Card style={styles.card}>
'dashboard-edit', <Card.Title title="Proxiwash" />
'view-dashboard', <List.Section>
i18n.t('screens.settings.dashboard'), <List.Item
i18n.t('screens.settings.dashboardSub') title={i18n.t('screens.settings.proxiwashNotifReminder')}
)} description={i18n.t('screens.settings.proxiwashNotifReminderSub')}
</List.Section> left={(props) => (
</Card> <List.Icon
<Card style={styles.card}> color={props.color}
<Card.Title title="Proxiwash" /> style={props.style}
<List.Section> icon="washing-machine"
<List.Item />
title={i18n.t('screens.settings.proxiwashNotifReminder')} )}
description={i18n.t('screens.settings.proxiwashNotifReminderSub')} />
left={(props) => ( <View style={styles.pickerContainer}>
<List.Icon {this.getProxiwashNotifPicker()}
color={props.color} </View>
style={props.style} <List.Item
icon="washing-machine" title={i18n.t('screens.settings.proxiwashChangeWash')}
/> description={i18n.t('screens.settings.proxiwashChangeWashSub')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="washing-machine"
/>
)}
/>
<View style={styles.pickerContainer}>
{this.getProxiwashChangePicker()}
</View>
</List.Section>
</Card>
<Card style={styles.card}>
<Card.Title title={i18n.t('screens.settings.information')} />
<List.Section>
{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
)} )}
/> {this.getNavigateItem(
<View style={styles.pickerContainer}> 'feedback',
{getProxiwashNotifPicker()} 'comment-quote',
</View> i18n.t('screens.feedback.homeButtonTitle'),
<List.Item i18n.t('screens.feedback.homeButtonSubtitle')
title={i18n.t('screens.settings.proxiwashChangeWash')}
description={i18n.t('screens.settings.proxiwashChangeWashSub')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="washing-machine"
/>
)} )}
/> </List.Section>
<View style={styles.pickerContainer}> </Card>
{getProxiwashChangePicker()} </CollapsibleScrollView>
</View> );
</List.Section> }
</Card>
<Card style={styles.card}>
<Card.Title title={i18n.t('screens.settings.information')} />
<List.Section>
{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')
)}
</List.Section>
</Card>
</CollapsibleScrollView>
);
} }
export default SettingsScreen; export default withTheme(SettingsScreen);

View file

@ -17,22 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useLayoutEffect, useState } from 'react'; import React, {
useCallback,
useEffect,
useLayoutEffect,
useState,
} from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Searchbar } from 'react-native-paper'; import { Searchbar } from 'react-native-paper';
import { stringMatchQuery } from '../../utils/Search'; import { stringMatchQuery } from '../../utils/Search';
import WebSectionList from '../../components/Screens/WebSectionList'; import WebSectionList from '../../components/Screens/WebSectionList';
import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion'; import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { useCachedPlanexGroups } from '../../context/cacheContext'; import { useCachedPlanexGroups } from '../../context/cacheContext';
import { usePlanexPreferences } from '../../context/preferencesContext';
import {
getPreferenceObject,
PlanexPreferenceKeys,
} from '../../utils/asyncStorage';
export type PlanexGroupType = { export type PlanexGroupType = {
name: string; name: string;
@ -62,23 +63,13 @@ function sortName(
function GroupSelectionScreen() { function GroupSelectionScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const { preferences, updatePreferences } = usePlanexPreferences();
const { groups, setGroups } = useCachedPlanexGroups(); const { groups, setGroups } = useCachedPlanexGroups();
const [currentSearchString, setCurrentSearchString] = useState(''); const [currentSearchString, setCurrentSearchString] = useState('');
const [favoriteGroups, setFavoriteGroups] = useState<Array<PlanexGroupType>>(
const getFavoriteGroups = (): Array<PlanexGroupType> => { AsyncStorageManager.getObject(
const data = getPreferenceObject( AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key
PlanexPreferenceKeys.planexFavoriteGroups, )
preferences );
);
if (data) {
return data as Array<PlanexGroupType>;
} else {
return [];
}
};
const favoriteGroups = getFavoriteGroups();
useLayoutEffect(() => { useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
@ -149,8 +140,10 @@ function GroupSelectionScreen() {
* @param item The article pressed * @param item The article pressed
*/ */
const onListItemPress = (item: PlanexGroupType) => { const onListItemPress = (item: PlanexGroupType) => {
updatePreferences(PlanexPreferenceKeys.planexCurrentGroup, item); navigation.navigate('planex', {
navigation.goBack(); screen: 'index',
params: { group: item },
});
}; };
/** /**
@ -160,16 +153,12 @@ function GroupSelectionScreen() {
*/ */
const onListFavoritePress = useCallback( const onListFavoritePress = useCallback(
(group: PlanexGroupType) => { (group: PlanexGroupType) => {
const updateFavorites = (newValue: Array<PlanexGroupType>) => {
updatePreferences(PlanexPreferenceKeys.planexFavoriteGroups, newValue);
};
const removeGroupFromFavorites = (g: PlanexGroupType) => { const removeGroupFromFavorites = (g: PlanexGroupType) => {
updateFavorites(favoriteGroups.filter((f) => f.id !== g.id)); setFavoriteGroups(favoriteGroups.filter((f) => f.id !== g.id));
}; };
const addGroupToFavorites = (g: PlanexGroupType) => { const addGroupToFavorites = (g: PlanexGroupType) => {
updateFavorites([...favoriteGroups, g].sort(sortName)); setFavoriteGroups([...favoriteGroups, g].sort(sortName));
}; };
if (favoriteGroups.some((f) => f.id === group.id)) { if (favoriteGroups.some((f) => f.id === group.id)) {
@ -178,9 +167,16 @@ function GroupSelectionScreen() {
addGroupToFavorites(group); addGroupToFavorites(group);
} }
}, },
[favoriteGroups, updatePreferences] [favoriteGroups]
); );
useEffect(() => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key,
favoriteGroups
);
}, [favoriteGroups]);
/** /**
* Generates the dataset to be used in the FlatList. * 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. * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top.

View file

@ -17,12 +17,17 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Title, useTheme } from 'react-native-paper'; import { Title, useTheme } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import {
CommonActions,
useFocusEffect,
useNavigation,
} from '@react-navigation/native';
import Autolink from 'react-native-autolink'; import Autolink from 'react-native-autolink';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import AlertDialog from '../../components/Dialogs/AlertDialog'; import AlertDialog from '../../components/Dialogs/AlertDialog';
import { dateToString, getTimeOnlyString } from '../../utils/Planning'; import { dateToString, getTimeOnlyString } from '../../utils/Planning';
import DateManager from '../../managers/DateManager'; import DateManager from '../../managers/DateManager';
@ -31,17 +36,8 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import { getPrettierPlanexGroupName } from '../../utils/Utils'; import { getPrettierPlanexGroupName } from '../../utils/Utils';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import PlanexWebview, { import PlanexWebview from '../../components/Screens/PlanexWebview';
JS_LOADED_MESSAGE,
} from '../../components/Screens/PlanexWebview';
import PlanexBottomBar from '../../components/Animations/PlanexBottomBar'; import PlanexBottomBar from '../../components/Animations/PlanexBottomBar';
import {
getPreferenceString,
GeneralPreferenceKeys,
PlanexPreferenceKeys,
} from '../../utils/asyncStorage';
import { usePlanexPreferences } from '../../context/preferencesContext';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -54,10 +50,17 @@ const styles = StyleSheet.create({
}, },
}); });
function PlanexScreen() { type Props = {
route: {
params: {
group?: PlanexGroupType;
};
};
};
function PlanexScreen(props: Props) {
const navigation = useNavigation(); const navigation = useNavigation();
const theme = useTheme(); const theme = useTheme();
const { preferences } = usePlanexPreferences();
const [dialogContent, setDialogContent] = useState< const [dialogContent, setDialogContent] = useState<
| undefined | undefined
@ -68,15 +71,13 @@ function PlanexScreen() {
} }
>(); >();
const [injectJS, setInjectJS] = useState(''); const [injectJS, setInjectJS] = useState('');
const [loading, setLoading] = useState(true);
const getCurrentGroup: () => PlanexGroupType | undefined = useCallback(() => { const getCurrentGroup = (): PlanexGroupType | undefined => {
let currentGroupString = getPreferenceString( let currentGroupString = AsyncStorageManager.getString(
PlanexPreferenceKeys.planexCurrentGroup, AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
preferences
); );
let group: PlanexGroupType; let group: PlanexGroupType;
if (currentGroupString) { if (currentGroupString !== '') {
group = JSON.parse(currentGroupString); group = JSON.parse(currentGroupString);
navigation.setOptions({ navigation.setOptions({
title: getPrettierPlanexGroupName(group.name), title: getPrettierPlanexGroupName(group.name),
@ -84,10 +85,22 @@ function PlanexScreen() {
return group; return group;
} }
return undefined; return undefined;
}, [navigation, preferences]); };
const currentGroup = getCurrentGroup(); const [currentGroup, setCurrentGroup] = useState<PlanexGroupType | undefined>(
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. * Gets the Webview, with an error view on top if no group is selected.
* *
@ -136,26 +149,22 @@ function PlanexScreen() {
* @param event * @param event
*/ */
const onMessage = (event: { nativeEvent: { data: string } }) => { const onMessage = (event: { nativeEvent: { data: string } }) => {
if (event.nativeEvent.data === JS_LOADED_MESSAGE) { const data: {
setLoading(false); start: string;
} else { end: string;
const data: { title: string;
start: string; color: string;
end: string; } = JSON.parse(event.nativeEvent.data);
title: string; const startDate = dateToString(new Date(data.start), true);
color: string; const endDate = dateToString(new Date(data.end), true);
} = JSON.parse(event.nativeEvent.data); const startString = getTimeOnlyString(startDate);
const startDate = dateToString(new Date(data.start), true); const endString = getTimeOnlyString(endDate);
const endDate = dateToString(new Date(data.end), true);
const startString = getTimeOnlyString(startDate);
const endString = getTimeOnlyString(endDate);
let msg = `${DateManager.getInstance().getTranslatedDate(startDate)}\n`; let msg = `${DateManager.getInstance().getTranslatedDate(startDate)}\n`;
if (startString != null && endString != null) { if (startString != null && endString != null) {
msg += `${startString} - ${endString}`; msg += `${startString} - ${endString}`;
}
showDialog(data.title, msg, data.color);
} }
showDialog(data.title, msg, data.color);
}; };
/** /**
@ -185,20 +194,21 @@ function PlanexScreen() {
const hideDialog = () => setDialogContent(undefined); const hideDialog = () => setDialogContent(undefined);
useEffect(() => { /**
const group = getCurrentGroup(); * Sends the webpage a message with the new group to select and save it to preferences
if (group) { *
sendMessage('setGroup', group.id.toString()); * @param group The group object selected
navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); */
} const selectNewGroup = (group: PlanexGroupType) => {
// eslint-disable-next-line react-hooks/exhaustive-deps sendMessage('setGroup', group.id.toString());
}, [getCurrentGroup, navigation]); setCurrentGroup(group);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.planexCurrentGroup.key,
group
);
const showMascot = navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
getPreferenceString( };
GeneralPreferenceKeys.defaultStartScreen,
preferences
)?.toLowerCase() !== 'planex';
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
@ -210,13 +220,11 @@ function PlanexScreen() {
<View style={GENERAL_STYLES.flex}>{getWebView()}</View> <View style={GENERAL_STYLES.flex}>{getWebView()}</View>
)} )}
</View> </View>
{loading ? ( {AsyncStorageManager.getString(
<View style={styles.container}> AsyncStorageManager.PREFERENCES.defaultStartScreen.key
<BasicLoadingScreen /> ).toLowerCase() !== 'planex' ? (
</View>
) : null}
{showMascot ? (
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.planexShowMascot.key}
title={i18n.t('screens.planex.mascotDialog.title')} title={i18n.t('screens.planex.mascotDialog.title')}
message={i18n.t('screens.planex.mascotDialog.message')} message={i18n.t('screens.planex.mascotDialog.message')}
icon="emoticon-kiss" icon="emoticon-kiss"

View file

@ -34,6 +34,7 @@ import {
import CustomAgenda from '../../components/Overrides/CustomAgenda'; import CustomAgenda from '../../components/Overrides/CustomAgenda';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
@ -290,6 +291,7 @@ class PlanningScreen extends React.Component<PropsType, StateType> {
} }
/> />
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.eventsShowMascot.key}
title={i18n.t('screens.planning.mascotDialog.title')} title={i18n.t('screens.planning.mascotDialog.title')}
message={i18n.t('screens.planning.mascotDialog.message')} message={i18n.t('screens.planning.mascotDialog.message')}
icon="party-popper" icon="party-popper"

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { useLayoutEffect, useRef, useState } from 'react'; import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { import {
SectionListData, SectionListData,
SectionListRenderItemInfo, SectionListRenderItemInfo,
@ -28,6 +28,7 @@ import i18n from 'i18n-js';
import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper'; import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import WebSectionList from '../../components/Screens/WebSectionList'; import WebSectionList from '../../components/Screens/WebSectionList';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem'; import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
import ProxiwashConstants, { import ProxiwashConstants, {
MachineStates, MachineStates,
@ -49,17 +50,9 @@ import type { SectionListDataType } from '../../components/Screens/WebSectionLis
import type { LaundromatType } from './ProxiwashAboutScreen'; import type { LaundromatType } from './ProxiwashAboutScreen';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { useNavigation } from '@react-navigation/core'; import { useFocusEffect, useNavigation } from '@react-navigation/core';
import { setupMachineNotification } from '../../utils/Notifications'; import { setupMachineNotification } from '../../utils/Notifications';
import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader'; import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader';
import {
getPreferenceNumber,
getPreferenceObject,
getPreferenceString,
ProxiwashPreferenceKeys,
} from '../../utils/asyncStorage';
import { useProxiwashPreferences } from '../../context/preferencesContext';
import { useSubsequentEffect } from '../../utils/customHooks';
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -98,44 +91,23 @@ const styles = StyleSheet.create({
function ProxiwashScreen() { function ProxiwashScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const theme = useTheme(); const theme = useTheme();
const { preferences, updatePreferences } = useProxiwashPreferences();
const [ const [
modalCurrentDisplayItem, modalCurrentDisplayItem,
setModalCurrentDisplayItem, setModalCurrentDisplayItem,
] = useState<React.ReactElement | null>(null); ] = useState<React.ReactElement | null>(null);
const reminder = getPreferenceNumber( const [machinesWatched, setMachinesWatched] = useState<
ProxiwashPreferenceKeys.proxiwashNotifications, Array<ProxiwashMachineType>
preferences >(
AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
)
); );
const [refresh, setRefresh] = useState(false);
const getMachinesWatched = () => { const [selectedWash, setSelectedWash] = useState(
const data = getPreferenceObject( AsyncStorageManager.getString(
ProxiwashPreferenceKeys.proxiwashWatchedMachines, AsyncStorageManager.PREFERENCES.selectedWash.key
preferences ) as 'tripodeB' | 'washinsa'
) as Array<ProxiwashMachineType>; );
return data ? (data as Array<ProxiwashMachineType>) : [];
};
const getSelectedWash = () => {
const data = getPreferenceString(
ProxiwashPreferenceKeys.selectedWash,
preferences
);
if (data !== 'washinsa' && data !== 'tripodeB') {
return 'washinsa';
} else {
return data;
}
};
const machinesWatched: Array<ProxiwashMachineType> = getMachinesWatched();
const selectedWash: 'washinsa' | 'tripodeB' = getSelectedWash();
useSubsequentEffect(() => {
// Refresh the list when the selected wash changes
setRefresh(true);
}, [selectedWash]);
const modalStateStrings: { [key in MachineStates]: string } = { const modalStateStrings: { [key in MachineStates]: string } = {
[MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'), [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'),
@ -165,6 +137,17 @@ function ProxiwashScreen() {
}); });
}, [navigation]); }, [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 * Callback used when the user clicks on enable notifications for a machine
* *
@ -310,7 +293,6 @@ function ProxiwashScreen() {
setupMachineNotification( setupMachineNotification(
machine.number, machine.number,
true, true,
reminder,
getMachineEndDate(machine) getMachineEndDate(machine)
); );
saveNotificationToState(machine); saveNotificationToState(machine);
@ -359,11 +341,8 @@ function ProxiwashScreen() {
...data.dryers, ...data.dryers,
...data.washers, ...data.washers,
]); ]);
if (cleanedList.length !== machinesWatched.length) { if (cleanedList !== machinesWatched) {
updatePreferences( setMachinesWatched(machinesWatched);
ProxiwashPreferenceKeys.proxiwashWatchedMachines,
cleanedList
);
} }
return [ return [
{ {
@ -428,7 +407,11 @@ function ProxiwashScreen() {
}; };
const saveNewWatchedList = (list: Array<ProxiwashMachineType>) => { const saveNewWatchedList = (list: Array<ProxiwashMachineType>) => {
updatePreferences(ProxiwashPreferenceKeys.proxiwashWatchedMachines, list); setMachinesWatched(list);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
list
);
}; };
const renderListHeaderComponent = ( const renderListHeaderComponent = (
@ -453,7 +436,6 @@ function ProxiwashScreen() {
default: default:
data = ProxiwashConstants.washinsa; data = ProxiwashConstants.washinsa;
} }
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<View style={styles.container}> <View style={styles.container}>
@ -466,11 +448,10 @@ function ProxiwashScreen() {
refreshOnFocus={true} refreshOnFocus={true}
extraData={machinesWatched.length} extraData={machinesWatched.length}
renderListHeaderComponent={renderListHeaderComponent} renderListHeaderComponent={renderListHeaderComponent}
refresh={refresh}
onFinish={() => setRefresh(false)}
/> />
</View> </View>
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowMascot.key}
title={i18n.t('screens.proxiwash.mascotDialog.title')} title={i18n.t('screens.proxiwash.mascotDialog.title')}
message={i18n.t('screens.proxiwash.mascotDialog.message')} message={i18n.t('screens.proxiwash.mascotDialog.message')}
icon="information" icon="information"

View file

@ -35,12 +35,12 @@ import MaterialHeaderButtons, {
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import { import ServicesManager, {
getCategories,
ServiceCategoryType,
SERVICES_CATEGORIES_KEY, SERVICES_CATEGORIES_KEY,
} from '../../utils/Services'; } from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type { ServiceCategoryType } from '../../managers/ServicesManager';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -66,7 +66,8 @@ class ServicesScreen extends React.Component<PropsType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.finalDataset = getCategories(props.navigation.navigate, [ const services = new ServicesManager(props.navigation);
this.finalDataset = services.getCategories([
SERVICES_CATEGORIES_KEY.SPECIAL, SERVICES_CATEGORIES_KEY.SPECIAL,
]); ]);
} }
@ -158,6 +159,7 @@ class ServicesScreen extends React.Component<PropsType> {
hasTab hasTab
/> />
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.servicesShowMascot.key}
title={i18n.t('screens.services.mascotDialog.title')} title={i18n.t('screens.services.mascotDialog.title')}
message={i18n.t('screens.services.mascotDialog.message')} message={i18n.t('screens.services.mascotDialog.message')}
icon="cloud-question" icon="cloud-question"

View file

@ -22,7 +22,7 @@ import { Collapsible } from 'react-navigation-collapsible';
import { CommonActions } from '@react-navigation/native'; import { CommonActions } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import { ServiceCategoryType } from '../../utils/Services'; import type { ServiceCategoryType } from '../../managers/ServicesManager';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;

View file

@ -18,6 +18,7 @@
*/ */
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AsyncStorageManager from '../managers/AsyncStorageManager';
import PushNotificationIOS from '@react-native-community/push-notification-ios'; import PushNotificationIOS from '@react-native-community/push-notification-ios';
import PushNotification from 'react-native-push-notification'; import PushNotification from 'react-native-push-notification';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
@ -78,8 +79,11 @@ PushNotification.configure({
* @param machineID The machine id to schedule notifications for. This is used as id and in the notification string. * @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 * @param date The date to trigger the notification at
*/ */
function createNotifications(machineID: string, date: Date, reminder?: number) { function createNotifications(machineID: string, date: Date) {
if (reminder && !Number.isNaN(reminder) && reminder > 0) { const reminder = AsyncStorageManager.getNumber(
AsyncStorageManager.PREFERENCES.proxiwashNotifications.key
);
if (!Number.isNaN(reminder) && reminder > 0) {
const id = reminderIdFactor * parseInt(machineID, 10); const id = reminderIdFactor * parseInt(machineID, 10);
const reminderDate = new Date(date); const reminderDate = new Date(date);
reminderDate.setMinutes(reminderDate.getMinutes() - reminder); reminderDate.setMinutes(reminderDate.getMinutes() - reminder);
@ -118,11 +122,10 @@ function createNotifications(machineID: string, date: Date, reminder?: number) {
export function setupMachineNotification( export function setupMachineNotification(
machineID: string, machineID: string,
isEnabled: boolean, isEnabled: boolean,
reminder?: number,
endDate?: Date | null endDate?: Date | null
) { ) {
if (isEnabled && endDate) { if (isEnabled && endDate) {
createNotifications(machineID, endDate, reminder); createNotifications(machineID, endDate);
} else { } else {
PushNotification.cancelLocalNotifications({ id: machineID }); PushNotification.cancelLocalNotifications({ id: machineID });
const reminderId = reminderIdFactor * parseInt(machineID, 10); const reminderId = reminderIdFactor * parseInt(machineID, 10);

View file

@ -53,10 +53,6 @@ export function getErrorMessage(
fullMessage.message = i18n.t('errors.tokenSave'); fullMessage.message = i18n.t('errors.tokenSave');
fullMessage.icon = 'alert-circle-outline'; fullMessage.icon = 'alert-circle-outline';
break; break;
case REQUEST_STATUS.TOKEN_RETRIEVE:
fullMessage.message = i18n.t('errors.tokenRetrieve');
fullMessage.icon = 'alert-circle-outline';
break;
default: default:
fullMessage.message = i18n.t('errors.unknown'); fullMessage.message = i18n.t('errors.unknown');
fullMessage.icon = 'alert-circle-outline'; fullMessage.icon = 'alert-circle-outline';

View file

@ -16,11 +16,6 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
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 * Gets the given services list without items of the given ids
@ -30,296 +25,45 @@ import { TabRoutes } from '../navigation/TabNavigator';
* @returns {[]} * @returns {[]}
*/ */
export default function getStrippedServicesList<T extends { key: string }>( export default function getStrippedServicesList<T extends { key: string }>(
sourceList: Array<T>, idList: Array<string>,
idList?: Array<string> sourceList: Array<T>
) { ) {
if (idList) { const newArray: Array<T> = [];
return sourceList.filter((item) => !idList.includes(item.key)); sourceList.forEach((item: T) => {
} else { if (!idList.includes(item.key)) {
return sourceList; newArray.push(item);
}
});
return newArray;
}
/**
* 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<T extends { key: string }>(
idList: Array<string>,
originalList: Array<T>
) {
const subList: Array<T | null> = [];
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 AMICALE_LOGO = require('../../assets/amicale.png'); const item = originalList[i];
if (idList.includes(item.key)) {
export const SERVICES_KEY = { subList[idList.indexOf(item.key)] = item;
CLUBS: 'clubs', itemsAdded += 1;
PROFILE: 'profile', if (itemsAdded === idList.length) {
EQUIPMENT: 'equipment', break;
AMICALE_WEBSITE: 'amicale_website', }
VOTE: 'vote', }
PROXIMO: 'proximo', }
WIKETUD: 'wiketud', return subList;
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<ServiceItemType>;
};
export function getAmicaleServices(
onPress: (route: string, params?: { [key: string]: any }) => void,
excludedItems?: Array<string>
): Array<ServiceItemType> {
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<string>
): Array<ServiceItemType> {
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<string>
): Array<ServiceItemType> {
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<string>
): Array<ServiceItemType> {
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<string>
): Array<ServiceCategoryType> {
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);
} }

View file

@ -18,21 +18,23 @@
*/ */
import { Platform, StatusBar } from 'react-native'; import { Platform, StatusBar } from 'react-native';
import ThemeManager from '../managers/ThemeManager';
/** /**
* Updates status bar content color if on iOS only, * Updates status bar content color if on iOS only,
* as the android status bar is always set to black. * as the android status bar is always set to black.
*/ */
export function setupStatusBar(theme?: ReactNativePaper.Theme) { export function setupStatusBar() {
if (theme) { if (ThemeManager.getNightMode()) {
if (theme.dark) { StatusBar.setBarStyle('light-content', true);
StatusBar.setBarStyle('light-content', true); } else {
} else { StatusBar.setBarStyle('dark-content', true);
StatusBar.setBarStyle('dark-content', true); }
} if (Platform.OS === 'android') {
if (Platform.OS === 'android') { StatusBar.setBackgroundColor(
StatusBar.setBackgroundColor(theme.colors.surface, true); ThemeManager.getCurrentTheme().colors.surface,
} true
);
} }
} }

View file

@ -1,30 +1,14 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { SERVICES_KEY } from './Services'; import { SERVICES_KEY } from '../managers/ServicesManager';
export enum GeneralPreferenceKeys { export enum PreferenceKeys {
debugUnlocked = 'debugUnlocked', debugUnlocked = 'debugUnlocked',
showIntro = 'showIntro', showIntro = 'showIntro',
updateNumber = 'updateNumber', updateNumber = 'updateNumber',
proxiwashNotifications = 'proxiwashNotifications',
nightModeFollowSystem = 'nightModeFollowSystem', nightModeFollowSystem = 'nightModeFollowSystem',
nightMode = 'nightMode', nightMode = 'nightMode',
defaultStartScreen = 'defaultStartScreen', defaultStartScreen = 'defaultStartScreen',
showAprilFoolsStart = 'showAprilFoolsStart',
dashboardItems = 'dashboardItems',
gameScores = 'gameScores',
}
export enum PlanexPreferenceKeys {
planexCurrentGroup = 'planexCurrentGroup',
planexFavoriteGroups = 'planexFavoriteGroups',
}
export enum ProxiwashPreferenceKeys {
proxiwashNotifications = 'proxiwashNotifications',
proxiwashWatchedMachines = 'proxiwashWatchedMachines',
selectedWash = 'selectedWash',
}
export enum MascotPreferenceKeys {
servicesShowMascot = 'servicesShowMascot', servicesShowMascot = 'servicesShowMascot',
proxiwashShowMascot = 'proxiwashShowMascot', proxiwashShowMascot = 'proxiwashShowMascot',
homeShowMascot = 'homeShowMascot', homeShowMascot = 'homeShowMascot',
@ -33,110 +17,68 @@ export enum MascotPreferenceKeys {
loginShowMascot = 'loginShowMascot', loginShowMascot = 'loginShowMascot',
voteShowMascot = 'voteShowMascot', voteShowMascot = 'voteShowMascot',
equipmentShowMascot = 'equipmentShowMascot', equipmentShowMascot = 'equipmentShowMascot',
gameShowMascot = 'gameShowMascot', gameStartMascot = 'gameStartMascot',
proxiwashWatchedMachines = 'proxiwashWatchedMachines',
showAprilFoolsStart = 'showAprilFoolsStart',
planexCurrentGroup = 'planexCurrentGroup',
planexFavoriteGroups = 'planexFavoriteGroups',
dashboardItems = 'dashboardItems',
gameScores = 'gameScores',
selectedWash = 'selectedWash',
} }
export const PreferenceKeys = {
...GeneralPreferenceKeys,
...PlanexPreferenceKeys,
...ProxiwashPreferenceKeys,
...MascotPreferenceKeys,
};
export type PreferenceKeys =
| GeneralPreferenceKeys
| PlanexPreferenceKeys
| ProxiwashPreferenceKeys
| MascotPreferenceKeys;
export type PreferencesType = { [key in PreferenceKeys]: string }; export type PreferencesType = { [key in PreferenceKeys]: string };
export type GeneralPreferencesType = { [key in GeneralPreferenceKeys]: string };
export type PlanexPreferencesType = {
[key in PlanexPreferenceKeys]: string;
};
export type ProxiwashPreferencesType = {
[key in ProxiwashPreferenceKeys]: string;
};
export type MascotPreferencesType = { [key in MascotPreferenceKeys]: string };
export const defaultPlanexPreferences: { export const defaultPreferences: { [key in PreferenceKeys]: string } = {
[key in PlanexPreferenceKeys]: string; [PreferenceKeys.debugUnlocked]: '0',
} = { [PreferenceKeys.showIntro]: '1',
[PlanexPreferenceKeys.planexCurrentGroup]: '', [PreferenceKeys.updateNumber]: '0',
[PlanexPreferenceKeys.planexFavoriteGroups]: '[]', [PreferenceKeys.proxiwashNotifications]: '5',
}; [PreferenceKeys.nightModeFollowSystem]: '1',
[PreferenceKeys.nightMode]: '1',
export const defaultProxiwashPreferences: { [PreferenceKeys.defaultStartScreen]: 'home',
[key in ProxiwashPreferenceKeys]: string; [PreferenceKeys.servicesShowMascot]: '1',
} = { [PreferenceKeys.proxiwashShowMascot]: '1',
[ProxiwashPreferenceKeys.proxiwashNotifications]: '5', [PreferenceKeys.homeShowMascot]: '1',
[ProxiwashPreferenceKeys.proxiwashWatchedMachines]: '[]', [PreferenceKeys.eventsShowMascot]: '1',
[ProxiwashPreferenceKeys.selectedWash]: 'washinsa', [PreferenceKeys.planexShowMascot]: '1',
}; [PreferenceKeys.loginShowMascot]: '1',
[PreferenceKeys.voteShowMascot]: '1',
export const defaultMascotPreferences: { [PreferenceKeys.equipmentShowMascot]: '1',
[key in MascotPreferenceKeys]: string; [PreferenceKeys.gameStartMascot]: '1',
} = { [PreferenceKeys.proxiwashWatchedMachines]: '[]',
[MascotPreferenceKeys.servicesShowMascot]: '1', [PreferenceKeys.showAprilFoolsStart]: '1',
[MascotPreferenceKeys.proxiwashShowMascot]: '1', [PreferenceKeys.planexCurrentGroup]: '',
[MascotPreferenceKeys.homeShowMascot]: '1', [PreferenceKeys.planexFavoriteGroups]: '[]',
[MascotPreferenceKeys.eventsShowMascot]: '1', [PreferenceKeys.dashboardItems]: JSON.stringify([
[MascotPreferenceKeys.planexShowMascot]: '1',
[MascotPreferenceKeys.loginShowMascot]: '1',
[MascotPreferenceKeys.voteShowMascot]: '1',
[MascotPreferenceKeys.equipmentShowMascot]: '1',
[MascotPreferenceKeys.gameShowMascot]: '1',
};
export const defaultPreferences: { [key in GeneralPreferenceKeys]: string } = {
[GeneralPreferenceKeys.debugUnlocked]: '0',
[GeneralPreferenceKeys.showIntro]: '1',
[GeneralPreferenceKeys.updateNumber]: '0',
[GeneralPreferenceKeys.nightModeFollowSystem]: '1',
[GeneralPreferenceKeys.nightMode]: '1',
[GeneralPreferenceKeys.defaultStartScreen]: 'home',
[GeneralPreferenceKeys.showAprilFoolsStart]: '1',
[GeneralPreferenceKeys.dashboardItems]: JSON.stringify([
SERVICES_KEY.EMAIL, SERVICES_KEY.EMAIL,
SERVICES_KEY.WASHERS, SERVICES_KEY.WASHERS,
SERVICES_KEY.PROXIMO, SERVICES_KEY.PROXIMO,
SERVICES_KEY.TUTOR_INSA, SERVICES_KEY.TUTOR_INSA,
SERVICES_KEY.RU, SERVICES_KEY.RU,
]), ]),
[PreferenceKeys.gameScores]: '[]',
[GeneralPreferenceKeys.gameScores]: '[]', [PreferenceKeys.selectedWash]: 'washinsa',
}; };
export function isValidGeneralPreferenceKey(
key: string
): key is GeneralPreferenceKeys {
return key in Object.values(GeneralPreferenceKeys);
}
export function isValidMascotPreferenceKey(
key: string
): key is MascotPreferenceKeys {
return key in Object.values(MascotPreferenceKeys);
}
/** /**
* Set preferences object current values from AsyncStorage. * Set preferences object current values from AsyncStorage.
* This function should be called once on start. * This function should be called once on start.
* *
* @return {Promise<PreferencesType>} * @return {Promise<PreferencesType>}
*/ */
export function retrievePreferences< export function retrievePreferences(
Keys extends PreferenceKeys, keys: Array<PreferenceKeys>,
T extends Partial<PreferencesType> defaults: PreferencesType
>(keys: Array<Keys>, defaults: T): Promise<T> { ): Promise<PreferencesType> {
return new Promise((resolve: (preferences: T) => void) => { return new Promise((resolve: (preferences: PreferencesType) => void) => {
AsyncStorage.multiGet(keys) AsyncStorage.multiGet(Object.values(keys))
.then((result) => { .then((result) => {
const preferences = { ...defaults }; const preferences = { ...defaults };
result.forEach((item) => { result.forEach((item) => {
let [key, value] = item; let [key, value] = item;
if (value !== null) { if (value !== null) {
preferences[key as Keys] = value; preferences[key as PreferenceKeys] = value;
} }
}); });
resolve(preferences); resolve(preferences);
@ -152,14 +94,11 @@ export function retrievePreferences<
* @param key * @param key
* @param value * @param value
*/ */
export function setPreference< export function setPreference(
Keys extends PreferenceKeys, key: PreferenceKeys,
T extends Partial<PreferencesType> value: number | string | boolean | object | Array<any>,
>( prevPreferences: PreferencesType
key: Keys, ): PreferencesType {
value: number | string | boolean | object | Array<any> | undefined,
prevPreferences: T
): T {
let convertedValue: string; let convertedValue: string;
if (typeof value === 'string') { if (typeof value === 'string') {
convertedValue = value; convertedValue = value;
@ -181,10 +120,10 @@ export function setPreference<
* @param key * @param key
* @returns {boolean} * @returns {boolean}
*/ */
export function getPreferenceString< export function getPreferenceString(
Keys extends PreferenceKeys, key: PreferenceKeys,
T extends Partial<PreferencesType> preferences: PreferencesType
>(key: Keys, preferences: T): string | undefined { ): string | undefined {
return preferences[key]; return preferences[key];
} }
@ -194,10 +133,10 @@ export function getPreferenceString<
* @param key * @param key
* @returns {boolean} * @returns {boolean}
*/ */
export function getPreferenceBool< export function getPreferenceBool(
Keys extends PreferenceKeys, key: PreferenceKeys,
T extends Partial<PreferencesType> preferences: PreferencesType
>(key: Keys, preferences: T): boolean | undefined { ): boolean | undefined {
const value = preferences[key]; const value = preferences[key];
return value ? value === '1' || value === 'true' : undefined; return value ? value === '1' || value === 'true' : undefined;
} }
@ -208,12 +147,12 @@ export function getPreferenceBool<
* @param key * @param key
* @returns {number} * @returns {number}
*/ */
export function getPreferenceNumber< export function getPreferenceNumber(
Keys extends PreferenceKeys, key: PreferenceKeys,
T extends Partial<PreferencesType> preferences: PreferencesType
>(key: Keys, preferences: T): number | undefined { ): number | undefined {
const value = preferences[key] as string | undefined; const value = preferences[key];
return value ? parseFloat(value) : undefined; return value !== undefined ? parseFloat(value) : undefined;
} }
/** /**
@ -222,10 +161,10 @@ export function getPreferenceNumber<
* @param key * @param key
* @returns {{...}} * @returns {{...}}
*/ */
export function getPreferenceObject< export function getPreferenceObject(
Keys extends PreferenceKeys, key: PreferenceKeys,
T extends Partial<PreferencesType> preferences: PreferencesType
>(key: Keys, preferences: T): object | Array<any> | undefined { ): object | Array<any> | undefined {
const value = preferences[key] as string | undefined; const value = preferences[key];
return value ? JSON.parse(value) : undefined; return value ? JSON.parse(value) : undefined;
} }