Started writing documentation and ported app to use Flow

This commit is contained in:
keplyx 2019-06-29 13:37:21 +02:00
parent b16aab8adc
commit 0385bbb882
20 changed files with 351 additions and 243 deletions

11
.flowconfig Normal file
View file

@ -0,0 +1,11 @@
[ignore]
[include]
[libs]
[lints]
[options]
[strict]

41
App.js
View file

@ -1,3 +1,5 @@
// @flow
import React from 'react'; import React from 'react';
import {StyleProvider, Root, View} from 'native-base'; import {StyleProvider, Root, View} from 'native-base';
import AppNavigator from './navigation/AppNavigator'; import AppNavigator from './navigation/AppNavigator';
@ -7,26 +9,37 @@ import * as Font from 'expo-font';
// edited native-base-shoutem-theme according to // edited native-base-shoutem-theme according to
// https://github.com/GeekyAnts/theme/pull/5/files/91f67c55ca6e65fe3af779586b506950c9f331be#diff-4cfc2dd4d5dae7954012899f2268a422 // https://github.com/GeekyAnts/theme/pull/5/files/91f67c55ca6e65fe3af779586b506950c9f331be#diff-4cfc2dd4d5dae7954012899f2268a422
// to allow for dynamic theme switching // to allow for dynamic theme switching
import { clearThemeCache } from 'native-base-shoutem-theme'; import {clearThemeCache} from 'native-base-shoutem-theme';
export default class App extends React.Component { type Props = {};
constructor(props) { type State = {
isLoading: boolean,
currentTheme: ?Object,
};
export default class App extends React.Component<Props, State> {
state = {
isLoading: true,
currentTheme: null,
};
constructor(props: Object) {
super(props); super(props);
LocaleManager.getInstance().initTranslations(); LocaleManager.initTranslations();
this.updateTheme = this.updateTheme.bind(this);
this.state = {
isLoading: true,
currentTheme: undefined,
};
} }
/**
* Loads data before components are mounted, like fonts and themes
* @returns {Promise}
*/
async componentWillMount() { async componentWillMount() {
await Font.loadAsync({ await Font.loadAsync({
'Roboto': require('native-base/Fonts/Roboto.ttf'), 'Roboto': require('native-base/Fonts/Roboto.ttf'),
'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'), 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'),
}); });
ThemeManager.getInstance().setUpdateThemeCallback(this.updateTheme); ThemeManager.getInstance().setUpdateThemeCallback(() => this.updateTheme());
await ThemeManager.getInstance().getDataFromPreferences(); await ThemeManager.getInstance().getDataFromPreferences();
this.setState({ this.setState({
isLoading: false, isLoading: false,
@ -34,6 +47,9 @@ export default class App extends React.Component {
}); });
} }
/**
* Updates the theme and clears the cache to force reloading the app colors
*/
updateTheme() { updateTheme() {
// console.log('update theme called'); // console.log('update theme called');
this.setState({ this.setState({
@ -42,6 +58,11 @@ export default class App extends React.Component {
clearThemeCache(); clearThemeCache();
} }
/**
* Renders the app based on loading state
*
* @returns {*}
*/
render() { render() {
if (this.state.isLoading) { if (this.state.isLoading) {
return <View/>; return <View/>;

View file

@ -1,15 +1,31 @@
import React from "react"; // @flow
import {Body, Button, Header, Icon, Left, Right, Title} from "native-base";
import * as React from "react";
import {Body, Header, Icon, Left, Right, Title} from "native-base";
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import {getStatusBarHeight} from "react-native-status-bar-height"; import {getStatusBarHeight} from "react-native-status-bar-height";
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
type Props = {
backButton: boolean,
rightMenu: React.Node,
title: string,
navigation: Object,
};
export default class CustomHeader extends React.Component<Props> {
static defaultProps = {
backButton: false,
rightMenu: <Right/>,
fontSize: 26,
width: 30,
};
export default class CustomHeader extends React.Component {
render() { render() {
let button; let button;
let rightMenu; if (this.props.backButton)
if (this.props.backButton !== undefined && this.props.backButton === true)
button = button =
<Touchable <Touchable
style={{padding: 6}} style={{padding: 6}}
@ -30,11 +46,6 @@ export default class CustomHeader extends React.Component {
type={'MaterialCommunityIcons'}/> type={'MaterialCommunityIcons'}/>
</Touchable>; </Touchable>;
if (this.props.rightMenu)
rightMenu = this.props.rightMenu;
else
rightMenu = <Right/>;
return ( return (
<Header style={styles.header}> <Header style={styles.header}>
<Left> <Left>
@ -43,7 +54,7 @@ export default class CustomHeader extends React.Component {
<Body> <Body>
<Title>{this.props.title}</Title> <Title>{this.props.title}</Title>
</Body> </Body>
{rightMenu} {this.props.rightMenu}
</Header>); </Header>);
} }
}; };

View file

@ -1,12 +1,25 @@
import React from 'react'; // @flow
import * as React from 'react';
import {Icon} from "native-base"; import {Icon} from "native-base";
import ThemeManager from '../utils/ThemeManager'; import ThemeManager from '../utils/ThemeManager';
export default class CustomMaterialIcon extends React.Component { type Props = {
active: boolean,
icon: string,
color: ?string,
fontSize: number,
width: number,
}
constructor(props) { export default class CustomMaterialIcon extends React.Component<Props> {
super(props);
} static defaultProps = {
active: false,
color: undefined,
fontSize: 26,
width: 30,
};
render() { render() {
return ( return (
@ -21,8 +34,8 @@ export default class CustomMaterialIcon extends React.Component {
this.props.active ? this.props.active ?
ThemeManager.getInstance().getCurrentThemeVariables().brandPrimary : ThemeManager.getInstance().getCurrentThemeVariables().brandPrimary :
ThemeManager.getInstance().getCurrentThemeVariables().customMaterialIconColor, ThemeManager.getInstance().getCurrentThemeVariables().customMaterialIconColor,
fontSize: this.props.fontSize !== undefined ? this.props.fontSize : 26, fontSize: this.props.fontSize,
width: this.props.width !== undefined ? this.props.width : 30 width: this.props.width
}} }}
/> />
); );

View file

@ -1,6 +1,8 @@
import React from 'react'; // @flow
import * as React from 'react';
import {Platform, Dimensions, StyleSheet, Image, FlatList, Linking} from 'react-native'; import {Platform, Dimensions, StyleSheet, Image, FlatList, Linking} from 'react-native';
import {Badge, Text, Container, Content, Icon, Left, ListItem, Right} from "native-base"; import {Badge, Text, Container, Content, Left, ListItem, Right} from "native-base";
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomMaterialIcon from '../components/CustomMaterialIcon'; import CustomMaterialIcon from '../components/CustomMaterialIcon';
@ -10,9 +12,19 @@ const drawerCover = require("../assets/drawer-cover.png");
const WIKETUD_LINK = "https://www.etud.insa-toulouse.fr/wiketud/index.php/Accueil"; const WIKETUD_LINK = "https://www.etud.insa-toulouse.fr/wiketud/index.php/Accueil";
export default class SideBar extends React.Component { type Props = {
navigation: Object,
};
constructor(props) { type State = {
active: string,
};
export default class SideBar extends React.Component<Props, State> {
dataSet: Array<Object>;
constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
active: 'Home', active: 'Home',
@ -70,7 +82,7 @@ export default class SideBar extends React.Component {
]; ];
} }
navigateToScreen(route) { navigateToScreen(route: string) {
this.props.navigation.navigate(route); this.props.navigation.navigate(route);
this.props.navigation.closeDrawer(); this.props.navigation.closeDrawer();
this.setState({active: route}); this.setState({active: route});
@ -88,7 +100,7 @@ export default class SideBar extends React.Component {
<FlatList <FlatList
data={this.dataSet} data={this.dataSet}
extraData={this.state} extraData={this.state}
keyExtractor={(item, index) => item.route} keyExtractor={(item) => item.route}
renderItem={({item}) => renderItem={({item}) =>
<ListItem <ListItem
button button

View file

@ -1,16 +0,0 @@
import React from 'react';
import {Ionicons} from '@expo/vector-icons/build/Icons';
export default class TabBarIcon extends React.Component {
render() {
return (
<Ionicons
name={this.props.name}
size={26}
style={{marginBottom: -3}}
color={this.props.focused ? Colors.tabIconSelected : Colors.tabIconDefault}
/>
);
}
}

View file

@ -1,3 +1,5 @@
// @flow
import {createAppContainer, createStackNavigator} from 'react-navigation'; import {createAppContainer, createStackNavigator} from 'react-navigation';
import MainDrawerNavigator from './MainDrawerNavigator'; import MainDrawerNavigator from './MainDrawerNavigator';

View file

@ -1,4 +1,6 @@
import React from 'react'; // @flow
import * as React from 'react';
import {createDrawerNavigator} from 'react-navigation'; import {createDrawerNavigator} from 'react-navigation';
import HomeScreen from '../screens/HomeScreen'; import HomeScreen from '../screens/HomeScreen';

View file

@ -1,58 +0,0 @@
import React from 'react';
import {Platform} from 'react-native';
import {createStackNavigator} from 'react-navigation';
import {createMaterialBottomTabNavigator} from "react-navigation-material-bottom-tabs";
import TabBarIcon from '../components/TabBarIcon';
import HomeScreen from '../screens/HomeScreen';
import PlanningScreen from '../screens/PlanningScreen';
const HomeStack = createStackNavigator({
Home: HomeScreen,
});
HomeStack.navigationOptions = {
tabBarLabel: 'Home',
tabBarIcon: ({focused}) => (
<TabBarIcon
focused={focused}
name={
Platform.OS === 'ios'
? 'ios-home'
: 'md-home'
}
/>
),
};
const ProfileStack = createStackNavigator({
Profile: PlanningScreen,
});
ProfileStack.navigationOptions = {
tabBarLabel: 'Profile',
tabBarIcon: ({focused}) => (
<TabBarIcon
focused={focused}
name={
Platform.OS === 'ios'
? 'ios-people'
: 'md-people'
}
/>
),
};
export default createMaterialBottomTabNavigator(
{
Home: HomeStack,
Profile: ProfileStack
}, {
initialRouteName: 'Home',
shifting: true,
activeColor: Colors.tabIconSelected,
inactiveColor: Colors.tabIconDefault,
barStyle: {backgroundColor: Colors.mainColor},
}
);

View file

@ -1,5 +1,7 @@
import React from 'react'; // @flow
import {Container, Text, Content, ListItem, Body, Left, Thumbnail, Right, Button, Icon} from 'native-base';
import * as React from 'react';
import {Container, Text, Content, ListItem, Body} from 'native-base';
import CustomHeader from "../../components/CustomHeader"; import CustomHeader from "../../components/CustomHeader";
import {FlatList} from "react-native"; import {FlatList} from "react-native";
import i18n from "i18n-js"; import i18n from "i18n-js";
@ -14,8 +16,11 @@ function generateListFromObject(object) {
return list; return list;
} }
type Props = {
navigation: Object
}
export default class AboutDependenciesScreen extends React.Component { export default class AboutDependenciesScreen extends React.Component<Props> {
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
@ -26,7 +31,7 @@ export default class AboutDependenciesScreen extends React.Component {
<Content> <Content>
<FlatList <FlatList
data={data} data={data}
keyExtractor={(item, index) => item.name} keyExtractor={(item) => item.name}
style={{minHeight: 300, width: '100%'}} style={{minHeight: 300, width: '100%'}}
renderItem={({item}) => renderItem={({item}) =>
<ListItem> <ListItem>

View file

@ -1,6 +1,8 @@
import React from 'react'; // @flow
import * as React from 'react';
import {Platform, StyleSheet, Linking, Alert, FlatList} from 'react-native'; import {Platform, StyleSheet, Linking, Alert, FlatList} from 'react-native';
import {Container, Content, Text, Card, CardItem, Body, Icon, Left, Right, Thumbnail, H1, ListItem} from 'native-base'; import {Container, Content, Text, Card, CardItem, Body, Left, Right, Thumbnail, H1} from 'native-base';
import CustomHeader from "../../components/CustomHeader"; import CustomHeader from "../../components/CustomHeader";
import i18n from "i18n-js"; import i18n from "i18n-js";
import appJson from '../../app'; import appJson from '../../app';
@ -20,13 +22,18 @@ const links = {
react: 'https://facebook.github.io/react-native/', react: 'https://facebook.github.io/react-native/',
}; };
type Props = {
navigation: Object,
};
function openWebLink(link) { function openWebLink(link) {
Linking.openURL(link).catch((err) => console.error('Error opening link', err)); Linking.openURL(link).catch((err) => console.error('Error opening link', err));
} }
export default class AboutScreen extends React.Component { export default class AboutScreen extends React.Component<Props> {
appData = [ appData: Array<Object> = [
{ {
onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore), onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore),
icon: Platform.OS === "ios" ? 'apple' : 'google-play', icon: Platform.OS === "ios" ? 'apple' : 'google-play',
@ -59,7 +66,7 @@ export default class AboutScreen extends React.Component {
}, },
]; ];
authorData = [ authorData: Array<Object> = [
{ {
onPressCallback: () => Alert.alert('Coucou', 'Whaou'), onPressCallback: () => Alert.alert('Coucou', 'Whaou'),
icon: 'account-circle', icon: 'account-circle',
@ -86,7 +93,7 @@ export default class AboutScreen extends React.Component {
}, },
]; ];
technoData = [ technoData: Array<Object> = [
{ {
onPressCallback: () => openWebLink(links.react), onPressCallback: () => openWebLink(links.react),
icon: 'react', icon: 'react',
@ -101,7 +108,7 @@ export default class AboutScreen extends React.Component {
}, },
]; ];
getCardItem(onPressCallback, icon, text, showChevron) { getCardItem(onPressCallback: Function, icon: string, text: string, showChevron: boolean) {
return ( return (
<CardItem button <CardItem button
onPress={onPressCallback}> onPress={onPressCallback}>
@ -178,12 +185,3 @@ export default class AboutScreen extends React.Component {
); );
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -1,12 +1,16 @@
import React from 'react'; // @flow
import * as React from 'react';
import {Container, Content, Text, Button, Icon} from 'native-base'; import {Container, Content, Text, Button, Icon} from 'native-base';
import CustomHeader from '../components/CustomHeader'; import CustomHeader from '../components/CustomHeader';
import i18n from "i18n-js"; import i18n from "i18n-js";
import NotificationsManager from '../utils/NotificationsManager' import NotificationsManager from '../utils/NotificationsManager'
import { Notifications } from 'expo';
type Props = {
navigation: Object,
}
export default class HomeScreen extends React.Component { export default class HomeScreen extends React.Component<Props> {
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
return ( return (
@ -19,7 +23,7 @@ export default class HomeScreen extends React.Component {
name={'bell-ring'} name={'bell-ring'}
type={'MaterialCommunityIcons'} type={'MaterialCommunityIcons'}
/> />
<Text>Notif</Text> <Text>Instant Notification</Text>
</Button> </Button>
</Content> </Content>
</Container> </Container>

View file

@ -1,10 +1,15 @@
import React from 'react'; // @flow
import { StyleSheet, View } from 'react-native';
import * as React from 'react';
import {Container, Text} from 'native-base'; import {Container, Text} from 'native-base';
import CustomHeader from "../components/CustomHeader"; import CustomHeader from "../components/CustomHeader";
import i18n from "i18n-js"; import i18n from "i18n-js";
export default class PlanningScreen extends React.Component { type Props = {
navigation: Object,
}
export default class PlanningScreen extends React.Component<Props> {
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
return ( return (
@ -14,11 +19,4 @@ export default class PlanningScreen extends React.Component {
); );
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -1,9 +1,11 @@
import React from 'react'; // @flow
import * as React from 'react';
import {Container, Text, Content, ListItem, Left, Thumbnail, Right, Body, Icon} from 'native-base'; import {Container, Text, Content, ListItem, Left, Thumbnail, Right, Body, Icon} from 'native-base';
import CustomHeader from "../../components/CustomHeader"; import CustomHeader from "../../components/CustomHeader";
import {AsyncStorage, FlatList, View} from "react-native"; import {FlatList} from "react-native";
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Menu, {MenuItem, MenuDivider} from 'react-native-material-menu'; import Menu, {MenuItem} from 'react-native-material-menu';
import i18n from "i18n-js"; import i18n from "i18n-js";
const IMG_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/img/"; const IMG_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/img/";
@ -39,24 +41,35 @@ function sortNameReverse(a, b) {
return 0; return 0;
} }
type Props = {
navigation: Object
}
export default class ProximoMainScreen extends React.Component { type State = {
constructor(props) { navData: Array<Object>,
super(props); currentSortMode: string,
this.state = { isSortReversed: boolean,
navData: this.props.navigation.getParam('data', []).sort(sortPrice), sortPriceIcon: React.Node,
currentSortMode: sortMode.price, sortNameIcon: React.Node,
isSortReversed: false, };
sortPriceIcon: '',
sortNameIcon: '',
};
}
setMenuRef = ref => { export default class ProximoMainScreen extends React.Component<Props, State> {
state = {
navData: this.props.navigation.getParam('data', []).sort(sortPrice),
currentSortMode: sortMode.price,
isSortReversed: false,
sortPriceIcon: '',
sortNameIcon: '',
};
_menu: Menu;
setMenuRef = (ref: Menu) => {
this._menu = ref; this._menu = ref;
}; };
toggleSortMode(mode) { toggleSortMode(mode: string) {
let isReverse = this.state.isSortReversed; let isReverse = this.state.isSortReversed;
if (mode === this.state.currentSortMode) // reverse mode if (mode === this.state.currentSortMode) // reverse mode
isReverse = !isReverse; // this.state not updating on this function cycle isReverse = !isReverse; // this.state not updating on this function cycle
@ -65,7 +78,7 @@ export default class ProximoMainScreen extends React.Component {
this.setSortMode(mode, isReverse); this.setSortMode(mode, isReverse);
} }
setSortMode(mode, isReverse) { setSortMode(mode: string, isReverse: boolean) {
this.setState({ this.setState({
currentSortMode: mode, currentSortMode: mode,
isSortReversed: isReverse isSortReversed: isReverse
@ -98,7 +111,7 @@ export default class ProximoMainScreen extends React.Component {
this.setSortMode(this.state.currentSortMode, this.state.isSortReversed); this.setSortMode(this.state.currentSortMode, this.state.isSortReversed);
} }
setupSortIcons(mode, isReverse) { setupSortIcons(mode: string, isReverse: boolean) {
const downSortIcon = const downSortIcon =
<Icon <Icon
active active

View file

@ -1,6 +1,8 @@
import React from 'react'; // @flow
import * as React from 'react';
import {RefreshControl, SectionList} from 'react-native'; import {RefreshControl, SectionList} from 'react-native';
import {Container, Text, Content, ListItem, Left, Right, Body, Badge, Icon, Toast, H2} from 'native-base'; import {Container, Text, ListItem, Left, Right, Body, Badge, Toast, H2} from 'native-base';
import CustomHeader from "../../components/CustomHeader"; import CustomHeader from "../../components/CustomHeader";
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon"; import CustomMaterialIcon from "../../components/CustomMaterialIcon";
@ -15,18 +17,25 @@ const typesIcons = {
Default: "information-outline", Default: "information-outline",
}; };
export default class ProximoMainScreen extends React.Component { type Props = {
navigation: Object
}
constructor(props) { type State = {
super(props); refreshing: boolean,
this.state = { firstLoading: boolean,
refreshing: false, data: Object,
firstLoading: true, };
data: {},
};
}
static generateDataset(types, data) { export default class ProximoMainScreen extends React.Component<Props, State> {
state = {
refreshing: false,
firstLoading: true,
data: {},
};
static generateDataset(types: Array<string>, data: Object) {
let finalData = []; let finalData = [];
for (let i = 0; i < types.length; i++) { for (let i = 0; i < types.length; i++) {
finalData.push({ finalData.push({

View file

@ -1,4 +1,6 @@
import React from 'react'; // @flow
import * as React from 'react';
import {SectionList, RefreshControl, View} from 'react-native'; import {SectionList, RefreshControl, View} from 'react-native';
import {Body, Container, Icon, Left, ListItem, Right, Text, Toast, H2, Button} from 'native-base'; import {Body, Container, Icon, Left, ListItem, Right, Text, Toast, H2, Button} from 'native-base';
import CustomHeader from "../components/CustomHeader"; import CustomHeader from "../components/CustomHeader";
@ -25,10 +27,27 @@ let stateStrings = {};
let stateColors = {}; let stateColors = {};
type Props = {
navigation: Object,
};
export default class ProxiwashScreen extends React.Component { type State = {
refreshing: boolean,
firstLoading: boolean,
data: Object,
machinesWatched: Array<Object>
};
constructor(props) { export default class ProxiwashScreen extends React.Component<Props, State> {
state = {
refreshing: false,
firstLoading: true,
data: {},
machinesWatched: [],
};
constructor(props: Props) {
super(props); super(props);
let colors = ThemeManager.getInstance().getCurrentThemeVariables(); let colors = ThemeManager.getInstance().getCurrentThemeVariables();
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor; stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
@ -42,24 +61,12 @@ export default class ProxiwashScreen extends React.Component {
stateStrings[MACHINE_STATES.FONCTIONNE] = i18n.t('proxiwashScreen.states.running'); stateStrings[MACHINE_STATES.FONCTIONNE] = i18n.t('proxiwashScreen.states.running');
stateStrings[MACHINE_STATES.HS] = i18n.t('proxiwashScreen.states.broken'); stateStrings[MACHINE_STATES.HS] = i18n.t('proxiwashScreen.states.broken');
stateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.states.error'); stateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.states.error');
this.state = {
refreshing: false,
firstLoading: true,
data: {},
machinesWatched: [],
};
} }
async readData() { async readData() {
try { try {
let response = await fetch(DATA_URL); let response = await fetch(DATA_URL);
let responseJson = await response.json(); let responseJson = await response.json();
// This prevents end notifications from showing
// let watchList = this.state.machinesWatched;
// for (let i = 0; i < watchList.length; i++) {
// if (responseJson[MACHINE_STATES[watchList[i].machineNumber.state]] !== MACHINE_STATES.FONCTIONNE)
// this.disableNotification(watchList[i].machineNumber);
// }
this.setState({ this.setState({
data: responseJson data: responseJson
}); });
@ -98,21 +105,23 @@ export default class ProxiwashScreen extends React.Component {
}); });
}; };
static getRemainingTime(startString, endString, percentDone) { static getRemainingTime(startString: string, endString: string, percentDone: string): number {
let startArray = startString.split(':'); let startArray = startString.split(':');
let endArray = endString.split(':'); let endArray = endString.split(':');
let startDate = new Date(); let startDate = new Date();
startDate.setHours(parseInt(startArray[0]), parseInt(startArray[1]), 0, 0); startDate.setHours(parseInt(startArray[0]), parseInt(startArray[1]), 0, 0);
let endDate = new Date(); let endDate = new Date();
endDate.setHours(parseInt(endArray[0]), parseInt(endArray[1]), 0, 0); endDate.setHours(parseInt(endArray[0]), parseInt(endArray[1]), 0, 0);
return (((100 - percentDone) / 100) * (endDate - startDate) / (60 * 1000)).toFixed(0); // Convert milliseconds into minutes // Convert milliseconds into minutes
let time: string = (((100 - parseFloat(percentDone)) / 100) * (endDate - startDate) / (60 * 1000)).toFixed(0);
return parseInt(time);
} }
async setupNotifications(number, remainingTime) { async setupNotifications(machineId: string, remainingTime: number) {
if (!this.isMachineWatched(number)) { if (!this.isMachineWatched(machineId)) {
let endNotifID = await NotificationsManager.scheduleNotification( let endNotifID = await NotificationsManager.scheduleNotification(
i18n.t('proxiwashScreen.notifications.machineFinishedTitle'), i18n.t('proxiwashScreen.notifications.machineFinishedTitle'),
i18n.t('proxiwashScreen.notifications.machineFinishedBody', {number: number}), i18n.t('proxiwashScreen.notifications.machineFinishedBody', {number: machineId}),
new Date().getTime() + remainingTime * (60 * 1000) // Convert back to milliseconds new Date().getTime() + remainingTime * (60 * 1000) // Convert back to milliseconds
); );
let reminderNotifID = undefined; let reminderNotifID = undefined;
@ -127,41 +136,41 @@ export default class ProxiwashScreen extends React.Component {
if (remainingTime > reminderNotifTime && reminderNotifTime > 0) { if (remainingTime > reminderNotifTime && reminderNotifTime > 0) {
reminderNotifID = await NotificationsManager.scheduleNotification( reminderNotifID = await NotificationsManager.scheduleNotification(
i18n.t('proxiwashScreen.notifications.machineRunningTitle', {time: reminderNotifTime}), i18n.t('proxiwashScreen.notifications.machineRunningTitle', {time: reminderNotifTime}),
i18n.t('proxiwashScreen.notifications.machineRunningBody', {number: number}), i18n.t('proxiwashScreen.notifications.machineRunningBody', {number: machineId}),
new Date().getTime() + (remainingTime - reminderNotifTime) * (60 * 1000) // Convert back to milliseconds new Date().getTime() + (remainingTime - reminderNotifTime) * (60 * 1000) // Convert back to milliseconds
); );
} }
let data = this.state.machinesWatched; let data = this.state.machinesWatched;
data.push({machineNumber: number, endNotifID: endNotifID, reminderNotifID: reminderNotifID}); data.push({machineNumber: machineId, endNotifID: endNotifID, reminderNotifID: reminderNotifID});
this.setState({machinesWatched: data}); this.setState({machinesWatched: data});
AsyncStorage.setItem(WATCHED_MACHINES_PREFKEY, JSON.stringify(data)); AsyncStorage.setItem(WATCHED_MACHINES_PREFKEY, JSON.stringify(data));
} else } else
this.disableNotification(number); this.disableNotification(machineId);
} }
disableNotification(number) { disableNotification(machineId: string) {
let data = this.state.machinesWatched; let data: Object = this.state.machinesWatched;
if (data.length > 0) { if (data.length > 0) {
let elem = this.state.machinesWatched.find(function (elem) { let elem = this.state.machinesWatched.find(function (elem) {
return elem.machineNumber === number return elem.machineNumber === machineId
}); });
let arrayIndex = data.indexOf(elem); let arrayIndex = data.indexOf(elem);
NotificationsManager.cancelScheduledNoification(data[arrayIndex].endNotifID); NotificationsManager.cancelScheduledNotification(data[arrayIndex].endNotifID);
if (data[arrayIndex].reminderNotifID !== undefined) if (data[arrayIndex].reminderNotifID !== undefined)
NotificationsManager.cancelScheduledNoification(data[arrayIndex].reminderNotifID); NotificationsManager.cancelScheduledNotification(data[arrayIndex].reminderNotifID);
data.splice(arrayIndex, 1); data.splice(arrayIndex, 1);
this.setState({machinesWatched: data}); this.setState({machinesWatched: data});
AsyncStorage.setItem(WATCHED_MACHINES_PREFKEY, JSON.stringify(data)); AsyncStorage.setItem(WATCHED_MACHINES_PREFKEY, JSON.stringify(data));
} }
} }
isMachineWatched(number) { isMachineWatched(number: string) {
return this.state.machinesWatched.find(function (elem) { return this.state.machinesWatched.find(function (elem) {
return elem.machineNumber === number return elem.machineNumber === number
}) !== undefined; }) !== undefined;
} }
renderItem(item, section, data) { renderItem(item: Object, section: Object, data: Object) {
return ( return (
<ListItem <ListItem
thumbnail thumbnail

View file

@ -1,4 +1,6 @@
import React from 'react'; // @flow
import * as React from 'react';
import { import {
Container, Container,
Content, Content,
@ -22,7 +24,16 @@ import {AsyncStorage} from 'react-native'
const proxiwashNotifKey = "proxiwashNotifKey"; const proxiwashNotifKey = "proxiwashNotifKey";
export default class SettingsScreen extends React.Component { type Props = {
navigation: Object,
};
type State = {
nightMode: boolean,
proxiwashNotifPickerSelected: string,
};
export default class SettingsScreen extends React.Component<Props, State> {
state = { state = {
nightMode: ThemeManager.getInstance().getNightMode(), nightMode: ThemeManager.getInstance().getNightMode(),
proxiwashNotifPickerSelected: "5" proxiwashNotifPickerSelected: "5"
@ -38,21 +49,21 @@ export default class SettingsScreen extends React.Component {
} }
onProxiwashNotidPickerValueChange(value) { onProxiwashNotifPickerValueChange(value: string) {
AsyncStorage.setItem(proxiwashNotifKey, value); AsyncStorage.setItem(proxiwashNotifKey, value);
this.setState({ this.setState({
proxiwashNotifPickerSelected: value proxiwashNotifPickerSelected: value
}); });
} }
getproxiwashNotifPicker() { getProxiwashNotifPicker() {
return ( return (
<Picker <Picker
note note
mode="dropdown" mode="dropdown"
style={{width: 120}} style={{width: 120}}
selectedValue={this.state.proxiwashNotifPickerSelected} selectedValue={this.state.proxiwashNotifPickerSelected}
onValueChange={(value) => this.onProxiwashNotidPickerValueChange(value)} onValueChange={(value) => this.onProxiwashNotifPickerValueChange(value)}
> >
<Picker.Item label={i18n.t('settingsScreen.proxiwashNotifReminderPicker.never')} value="never"/> <Picker.Item label={i18n.t('settingsScreen.proxiwashNotifReminderPicker.never')} value="never"/>
<Picker.Item label={i18n.t('settingsScreen.proxiwashNotifReminderPicker.1')} value="1"/> <Picker.Item label={i18n.t('settingsScreen.proxiwashNotifReminderPicker.1')} value="1"/>
@ -67,7 +78,7 @@ export default class SettingsScreen extends React.Component {
} }
toggleNightMode() { toggleNightMode() {
ThemeManager.getInstance().setNightmode(!this.state.nightMode); ThemeManager.getInstance().setNightMode(!this.state.nightMode);
this.setState({nightMode: !this.state.nightMode}); this.setState({nightMode: !this.state.nightMode});
// Alert.alert(i18n.t('settingsScreen.nightMode'), i18n.t('settingsScreen.restart')); // Alert.alert(i18n.t('settingsScreen.nightMode'), i18n.t('settingsScreen.restart'));
this.resetStack(); this.resetStack();
@ -83,7 +94,7 @@ export default class SettingsScreen extends React.Component {
this.props.navigation.navigate('Settings'); this.props.navigation.navigate('Settings');
} }
getToggleItem(onPressCallback, icon, text, subtitle) { getToggleItem(onPressCallback: Function, icon: string, text: string, subtitle: string) {
return ( return (
<ListItem <ListItem
button button
@ -109,7 +120,7 @@ export default class SettingsScreen extends React.Component {
); );
} }
getGeneralItem(control, icon, text, subtitle) { static getGeneralItem(control: React.Node, icon: string, text: string, subtitle: string) {
return ( return (
<ListItem <ListItem
thumbnail thumbnail
@ -152,7 +163,7 @@ export default class SettingsScreen extends React.Component {
<Text>Proxiwash</Text> <Text>Proxiwash</Text>
</CardItem> </CardItem>
<List> <List>
{this.getGeneralItem(this.getproxiwashNotifPicker(), 'washing-machine', i18n.t('settingsScreen.proxiwashNotifReminder'), i18n.t('settingsScreen.proxiwashNotifReminderSub'))} {SettingsScreen.getGeneralItem(this.getProxiwashNotifPicker(), 'washing-machine', i18n.t('settingsScreen.proxiwashNotifReminder'), i18n.t('settingsScreen.proxiwashNotifReminderSub'))}
</List> </List>
</Card> </Card>
</Content> </Content>

View file

@ -1,21 +1,20 @@
// @flow
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as Localization from 'expo-localization'; import * as Localization from 'expo-localization';
import en from '../translations/en'; import en from '../translations/en';
import fr from '../translations/fr'; import fr from '../translations/fr';
/**
* Static class used to manage locales
*/
export default class LocaleManager { export default class LocaleManager {
static instance = null; /**
* Initialize translations using language files
static getInstance() { */
if (LocaleManager.instance == null) { static initTranslations() {
LocaleManager.instance = new LocaleManager();
}
return this.instance;
}
initTranslations() {
i18n.fallbacks = true; i18n.fallbacks = true;
i18n.translations = {fr, en}; i18n.translations = {fr, en};
i18n.locale = Localization.locale; i18n.locale = Localization.locale;

View file

@ -1,8 +1,18 @@
// @flow
import * as Permissions from 'expo-permissions'; import * as Permissions from 'expo-permissions';
import { Notifications } from 'expo'; import { Notifications } from 'expo';
/**
* Static class used to manage notifications sent to the user
*/
export default class NotificationsManager { export default class NotificationsManager {
/**
* Async function asking permission to send notifications to the user
*
* @returns {Promise}
*/
static async askPermissions() { static async askPermissions() {
const {status: existingStatus} = await Permissions.getAsync(Permissions.NOTIFICATIONS); const {status: existingStatus} = await Permissions.getAsync(Permissions.NOTIFICATIONS);
let finalStatus = existingStatus; let finalStatus = existingStatus;
@ -13,7 +23,14 @@ export default class NotificationsManager {
return finalStatus === 'granted'; return finalStatus === 'granted';
} }
static async sendNotificationImmediately (title, body) { /**
* Async function sending a notification without delay to the user
*
* @param title {String} Notification title
* @param body {String} Notification body text
* @returns {Promise<import("react").ReactText>} Notification Id
*/
static async sendNotificationImmediately (title: string, body: string) {
await NotificationsManager.askPermissions(); await NotificationsManager.askPermissions();
return await Notifications.presentLocalNotificationAsync({ return await Notifications.presentLocalNotificationAsync({
title: title, title: title,
@ -21,7 +38,15 @@ export default class NotificationsManager {
}); });
}; };
static async scheduleNotification(title, body, time) { /**
* Async function sending notification at the specified time
*
* @param title Notification title
* @param body Notification body text
* @param time Time at which we should send the notification
* @returns {Promise<import("react").ReactText>} Notification Id
*/
static async scheduleNotification(title: string, body: string, time: number): Promise<void> {
await NotificationsManager.askPermissions(); await NotificationsManager.askPermissions();
return Notifications.scheduleLocalNotificationAsync( return Notifications.scheduleLocalNotificationAsync(
{ {
@ -34,7 +59,12 @@ export default class NotificationsManager {
); );
}; };
static async cancelScheduledNoification(notifID) { /**
await Notifications.cancelScheduledNotificationAsync(notifID); * Async function used to cancel the notification of a specific ID
* @param notificationID {Number} The notification ID
* @returns {Promise}
*/
static async cancelScheduledNotification(notificationID: number) {
await Notifications.cancelScheduledNotificationAsync(notificationID);
} }
} }

View file

@ -1,3 +1,5 @@
// @flow
import {AsyncStorage} from 'react-native' import {AsyncStorage} from 'react-native'
import platform from '../native-base-theme/variables/platform'; import platform from '../native-base-theme/variables/platform';
import platformDark from '../native-base-theme/variables/platformDark'; import platformDark from '../native-base-theme/variables/platformDark';
@ -5,53 +7,85 @@ import getTheme from '../native-base-theme/components';
const nightModeKey = 'nightMode'; const nightModeKey = 'nightMode';
/**
* Singleton class used to manage themes
*/
export default class ThemeManager { export default class ThemeManager {
static instance = null; static instance: ThemeManager | null = null;
nightMode: boolean;
updateThemeCallback: Function;
constructor() { constructor() {
this.nightMode = false; this.nightMode = false;
this.updateThemeCallback = undefined; this.updateThemeCallback = null;
} }
static getInstance() { /**
if (ThemeManager.instance == null) { * Get this class instance or create one if none is found
ThemeManager.instance = new ThemeManager(); * @returns {ThemeManager}
} */
return this.instance; static getInstance(): ThemeManager {
return ThemeManager.instance === null ?
ThemeManager.instance = new ThemeManager() :
ThemeManager.instance;
} }
setUpdateThemeCallback(callback) { /**
* Set 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: ?Function) {
this.updateThemeCallback = callback; this.updateThemeCallback = callback;
} }
async getDataFromPreferences() { /**
let result = await AsyncStorage.getItem(nightModeKey); * Read async storage to get preferences
* @returns {Promise<void>}
*/
async getDataFromPreferences(): Promise<void> {
let result: string = await AsyncStorage.getItem(nightModeKey);
if (result === '1') if (result === '1')
this.nightMode = true; this.nightMode = true;
console.log('nightmode: ' + this.nightMode); // console.log('nightmode: ' + this.nightMode);
} }
setNightmode(isNightMode) { /**
* Set night mode and save it to preferences
*
* @param isNightMode Whether to enable night mode
*/
setNightMode(isNightMode: boolean) {
this.nightMode = isNightMode; this.nightMode = isNightMode;
AsyncStorage.setItem(nightModeKey, isNightMode ? '1' : '0'); AsyncStorage.setItem(nightModeKey, isNightMode ? '1' : '0');
if (this.updateThemeCallback !== undefined) if (this.updateThemeCallback !== null)
this.updateThemeCallback(); this.updateThemeCallback();
} }
getNightMode() { /**
* @returns {boolean} Night mode state
*/
getNightMode(): boolean {
return this.nightMode; return this.nightMode;
} }
getCurrentTheme() { /**
* Get the current theme based on night mode
* @returns {Object}
*/
getCurrentTheme(): Object {
if (this.nightMode) if (this.nightMode)
return getTheme(platformDark); return getTheme(platformDark);
else else
return getTheme(platform); return getTheme(platform);
} }
getCurrentThemeVariables() { /**
* Get the variables contained in the current theme
* @returns {Object}
*/
getCurrentThemeVariables(): Object {
return this.getCurrentTheme().variables; return this.getCurrentTheme().variables;
} }