Compare commits

...

17 commits

Author SHA1 Message Date
ddfac76f4e Updated libraries 2020-05-01 17:36:04 +02:00
097ea5379a use react native community async storage 2020-05-01 16:57:37 +02:00
e96f55d142 Removed unused icons and changed android activity background 2020-05-01 16:43:20 +02:00
2dab27de22 Further performance improvements 2020-05-01 16:38:57 +02:00
3d0e03cb9d Removed expo from used technologies 2020-05-01 16:36:11 +02:00
854e03e893 Show settings and about buttons by default 2020-05-01 16:35:23 +02:00
885bf239d8 Changed icons color 2020-05-01 16:32:23 +02:00
9e4e340302 Fixed modal displaying negative remaining times 2020-05-01 16:18:55 +02:00
ea16a1f50f Improved planex group search performance 2020-05-01 16:13:46 +02:00
0b7191887d Improved settings screen elements 2020-05-01 15:59:47 +02:00
517e75f4b9 Fixed accordion auto closing on setting selection 2020-05-01 11:10:26 +02:00
cb522466c7 Fixed tab bar icon not updating on theme change 2020-05-01 11:06:37 +02:00
eda9edd21c Updated icons 2020-05-01 11:03:46 +02:00
b83b142942 Fixed crash 2020-04-30 23:05:59 +02:00
65eb4dd77b Added notification translation 2020-04-30 22:49:25 +02:00
aa2fad344a Improved notification activation on corner cases 2020-04-30 22:38:33 +02:00
1835fcadf9 Added tests for proxiwash util functions 2020-04-30 22:16:35 +02:00
23 changed files with 414 additions and 170 deletions

View file

@ -0,0 +1,142 @@
import React from 'react';
import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash";
test('getMachineEndDate', () => {
jest.spyOn(Date, 'now')
.mockImplementation(() =>
new Date('2020-01-14T15:00:00.000Z').getTime()
);
let expectDate = new Date('2020-01-14T15:00:00.000Z');
expectDate.setHours(23);
expectDate.setMinutes(10);
expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime());
expectDate.setHours(16);
expectDate.setMinutes(30);
expect(getMachineEndDate({endTime: "16:30"}).getTime()).toBe(expectDate.getTime());
expect(getMachineEndDate({endTime: "15:30"})).toBeNull();
expect(getMachineEndDate({endTime: "13:10"})).toBeNull();
jest.spyOn(Date, 'now')
.mockImplementation(() =>
new Date('2020-01-14T23:00:00.000Z').getTime()
);
expectDate = new Date('2020-01-14T23:00:00.000Z');
expectDate.setHours(0);
expectDate.setMinutes(30);
expect(getMachineEndDate({endTime: "00:30"}).getTime()).toBe(expectDate.getTime());
});
test('isMachineWatched', () => {
let machineList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:30",
},
];
expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue();
expect(isMachineWatched({number: "1", endTime: "20:30"}, machineList)).toBeTrue();
expect(isMachineWatched({number: "3", endTime: "20:30"}, machineList)).toBeFalse();
expect(isMachineWatched({number: "1", endTime: "23:30"}, machineList)).toBeFalse();
});
test('getMachineOfId', () => {
let machineList = [
{
number: "0",
},
{
number: "1",
},
];
expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"});
expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"});
expect(getMachineOfId("3", machineList)).toBeNull();
});
test('getCleanedMachineWatched', () => {
let machineList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:30",
},
{
number: "2",
endTime: "",
},
];
let watchList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:30",
},
{
number: "2",
endTime: "",
},
];
let cleanedList = watchList;
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
watchList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:30",
},
{
number: "2",
endTime: "15:30",
},
];
cleanedList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:30",
},
];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
watchList = [
{
number: "0",
endTime: "23:30",
},
{
number: "1",
endTime: "20:31",
},
{
number: "3",
endTime: "15:30",
},
];
cleanedList = [
{
number: "0",
endTime: "23:30",
},
];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
});

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources> <resources>
<color name="activityBackground">#ffffff</color> <color name="activityBackground">#be1522</color>
<color name="navigationBarColor">#121212</color> <color name="navigationBarColor">#121212</color>
<color name="colorPrimaryDark">#be1522</color> <color name="colorPrimaryDark">#be1522</color>
<color name="colorPrimary">#be1522</color> <color name="colorPrimary">#be1522</color>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View file

@ -20,12 +20,13 @@
}, },
"dependencies": { "dependencies": {
"@nartc/react-native-barcode-mask": "^1.1.9", "@nartc/react-native-barcode-mask": "^1.1.9",
"@react-native-community/masked-view": "0.1.6", "@react-native-community/async-storage": "^1.9.0",
"@react-native-community/masked-view": "^0.1.10",
"@react-native-community/push-notification-ios": "^1.1.1", "@react-native-community/push-notification-ios": "^1.1.1",
"@react-navigation/bottom-tabs": "^5.1.1", "@react-native-community/slider": "^2.0.9",
"@react-navigation/drawer": "^5.1.1", "@react-navigation/bottom-tabs": "^5.3.2",
"@react-navigation/native": "^5.0.9", "@react-navigation/native": "^5.2.2",
"@react-navigation/stack": "^5.1.1", "@react-navigation/stack": "^5.2.17",
"i18n-js": "^3.3.0", "i18n-js": "^3.3.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-dom": "16.9.0", "react-dom": "16.9.0",
@ -43,29 +44,29 @@
"react-native-linear-gradient": "^2.5.6", "react-native-linear-gradient": "^2.5.6",
"react-native-localize": "^1.4.0", "react-native-localize": "^1.4.0",
"react-native-modalize": "^1.3.6", "react-native-modalize": "^1.3.6",
"react-native-paper": "^3.8.0", "react-native-paper": "^3.9.0",
"react-native-permissions": "^2.1.3", "react-native-permissions": "^2.1.4",
"react-native-push-notification": "^3.3.0", "react-native-push-notification": "^3.3.1",
"react-native-reanimated": "~1.7.0", "react-native-reanimated": "^1.8.0",
"react-native-render-html": "^4.1.2", "react-native-render-html": "^4.1.2",
"react-native-safe-area-context": "0.7.3", "react-native-safe-area-context": "0.7.3",
"react-native-screens": "~2.2.0", "react-native-screens": "^2.7.0",
"react-native-splash-screen": "^3.2.0", "react-native-splash-screen": "^3.2.0",
"react-native-vector-icons": "^6.6.0", "react-native-vector-icons": "^6.6.0",
"react-native-web": "~0.11.7", "react-native-webview": "^9.4.0",
"react-native-webview": "8.1.1",
"react-navigation-collapsible": "^5.5.0", "react-navigation-collapsible": "^5.5.0",
"react-navigation-header-buttons": "^3.0.5" "react-navigation-header-buttons": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.6.2", "@babel/core": "^7.9.6",
"@babel/runtime": "^7.6.2", "@babel/runtime": "^7.9.6",
"@react-native-community/eslint-config": "^0.0.5", "@react-native-community/eslint-config": "^1.1.0",
"babel-jest": "^24.9.0", "babel-jest": "^25.5.1",
"eslint": "^6.5.1", "eslint": "^6.5.1",
"jest": "^24.9.0", "flow-bin": "^0.123.0",
"metro-react-native-babel-preset": "^0.58.0", "jest": "^25.5.3",
"react-test-renderer": "16.9.0", "jest-extended": "^0.11.5",
"flow-bin": "^0.122.0" "metro-react-native-babel-preset": "^0.59.0",
"react-test-renderer": "16.9.0"
} }
} }

View file

@ -12,13 +12,13 @@ type Props = {
title: string, title: string,
subtitle?: string, subtitle?: string,
left?: (props: { [keys: string]: any }) => React.Node, left?: (props: { [keys: string]: any }) => React.Node,
opened: boolean, opened?: boolean,
unmountWhenCollapsed: boolean, unmountWhenCollapsed: boolean,
children?: React.Node, children?: React.Node,
} }
type State = { type State = {
expanded: boolean expanded: boolean,
} }
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
@ -26,7 +26,6 @@ const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<Props, State> { class AnimatedAccordion extends React.Component<Props, State> {
static defaultProps = { static defaultProps = {
opened: false,
unmountWhenCollapsed: false, unmountWhenCollapsed: false,
} }
chevronRef: { current: null | AnimatedListIcon }; chevronRef: { current: null | AnimatedListIcon };
@ -35,7 +34,7 @@ class AnimatedAccordion extends React.Component<Props, State> {
animEnd: string; animEnd: string;
state = { state = {
expanded: this.props.opened, expanded: this.props.opened != null ? this.props.opened : false,
} }
constructor(props) { constructor(props) {
@ -57,14 +56,15 @@ class AnimatedAccordion extends React.Component<Props, State> {
} }
toggleAccordion = () => { toggleAccordion = () => {
if (this.chevronRef.current != null) if (this.chevronRef.current != null) {
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd}); this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd});
this.setState({expanded: !this.state.expanded}) this.setState({expanded: !this.state.expanded})
}
}; };
shouldComponentUpdate(nextProps: Props) { shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
this.state.expanded = nextProps.opened; if (nextProps.opened != null && nextProps.opened !== this.props.opened)
this.setupChevron(); this.state.expanded = nextProps.opened;
return true; return true;
} }

View file

@ -3,14 +3,14 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, List, withTheme} from 'react-native-paper'; import {Avatar, Card, List, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from "react-native";
import {DrawerNavigationProp} from "@react-navigation/drawer";
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomTheme} from "../../managers/ThemeManager";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from "@react-navigation/stack";
const ICON_AMICALE = require("../../../assets/amicale.png"); const ICON_AMICALE = require("../../../assets/amicale.png");
type Props = { type Props = {
navigation: DrawerNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
isLoggedIn: boolean, isLoggedIn: boolean,
} }

View file

@ -23,10 +23,6 @@ const LIST_ITEM_HEIGHT = 64;
class GroupListAccordion extends React.Component<Props> { class GroupListAccordion extends React.Component<Props> {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps: Props) { shouldComponentUpdate(nextProps: Props) {
return (nextProps.currentSearchString !== this.props.currentSearchString) return (nextProps.currentSearchString !== this.props.currentSearchString)
|| (nextProps.favoriteNumber !== this.props.favoriteNumber) || (nextProps.favoriteNumber !== this.props.favoriteNumber)
@ -35,21 +31,31 @@ class GroupListAccordion extends React.Component<Props> {
keyExtractor = (item: group) => item.id.toString(); keyExtractor = (item: group) => item.id.toString();
renderItem = ({item}: {item: group}) => { renderItem = ({item}: { item: group }) => {
if (stringMatchQuery(item.name, this.props.currentSearchString)) { const onPress = () => this.props.onGroupPress(item);
const onPress = () => this.props.onGroupPress(item); const onStarPress = () => this.props.onFavoritePress(item);
const onStarPress = () => this.props.onFavoritePress(item); return (
return ( <GroupListItem
<GroupListItem height={LIST_ITEM_HEIGHT}
height={LIST_ITEM_HEIGHT} item={item}
item={item} onPress={onPress}
onPress={onPress} onStarPress={onStarPress}/>
onStarPress={onStarPress}/> );
);
} else
return null;
} }
getData() {
const originalData = this.props.item.content;
let displayData = [];
for (let i = 0; i < originalData.length; i++) {
if (stringMatchQuery(originalData[i].name, this.props.currentSearchString))
displayData.push(originalData[i]);
}
return displayData;
}
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
render() { render() {
const item = this.props.item; const item = this.props.item;
return ( return (
@ -73,14 +79,14 @@ class GroupListAccordion extends React.Component<Props> {
> >
{/*$FlowFixMe*/} {/*$FlowFixMe*/}
<FlatList <FlatList
data={item.content} data={this.getData()}
extraData={this.props.currentSearchString} extraData={this.props.currentSearchString}
renderItem={this.renderItem} renderItem={this.renderItem}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={item.id.toString()} listKey={item.id.toString()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
// getItemLayout={this.itemLayout} // Broken with search getItemLayout={this.itemLayout}
// removeClippedSubviews={true} removeClippedSubviews={true}
/> />
</AnimatedAccordion> </AnimatedAccordion>
</View> </View>

View file

@ -0,0 +1,58 @@
// @flow
import * as React from 'react';
import {Text, withTheme} from 'react-native-paper';
import {View} from "react-native-animatable";
import type {CustomTheme} from "../../managers/ThemeManager";
import Slider, {SliderProps} from "@react-native-community/slider";
type Props = {
theme: CustomTheme,
valueSuffix: string,
...SliderProps
}
type State = {
currentValue: number,
}
/**
* Abstraction layer for Modalize component, using custom configuration
*
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*}
*/
class CustomSlider extends React.Component<Props, State> {
static defaultProps = {
valueSuffix: "",
}
state = {
currentValue: this.props.value,
}
onValueChange = (value: number) => {
this.setState({currentValue: value});
if (this.props.onValueChange != null)
this.props.onValueChange(value);
}
render() {
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<Text style={{marginHorizontal: 10, marginTop: 'auto', marginBottom: 'auto'}}>
{this.state.currentValue}min
</Text>
<Slider
{...this.props}
onValueChange={this.onValueChange}
/>
</View>
);
}
}
export default withTheme(CustomSlider);

View file

@ -31,26 +31,16 @@ class CustomTabBar extends React.Component<Props, State> {
static TAB_BAR_HEIGHT = 48; static TAB_BAR_HEIGHT = 48;
activeColor: string;
inactiveColor: string;
state = { state = {
translateY: new Animated.Value(0), translateY: new Animated.Value(0),
barSynced: false,// Is the bar synced with the header for animations? barSynced: false,// Is the bar synced with the header for animations?
} }
// shouldComponentUpdate(nextProps: Props): boolean {
// return (nextProps.theme.dark !== this.props.theme.dark)
// || (nextProps.state.index !== this.props.state.index);
// }
tabRef: Object; tabRef: Object;
constructor(props) { constructor(props) {
super(props); super(props);
this.tabRef = React.createRef(); this.tabRef = React.createRef();
this.activeColor = props.theme.colors.primary;
this.inactiveColor = props.theme.colors.tabIcon;
} }
onItemPress(route: Object, currentIndex: number, destIndex: number) { onItemPress(route: Object, currentIndex: number, destIndex: number) {
@ -119,7 +109,7 @@ class CustomTabBar extends React.Component<Props, State> {
} }
} }
const color = isFocused ? this.activeColor : this.inactiveColor; const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
if (route.name !== "home") { if (route.name !== "home") {
return <TabIcon return <TabIcon
onPress={onPress} onPress={onPress}

View file

@ -1,6 +1,6 @@
// @flow // @flow
import {AsyncStorage} from "react-native"; import AsyncStorage from '@react-native-community/async-storage';
/** /**
* Singleton used to manage preferences. * Singleton used to manage preferences.

View file

@ -25,7 +25,6 @@ const links = {
"Coucou !\n\n", "Coucou !\n\n",
yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard', yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard',
react: 'https://facebook.github.io/react-native/', react: 'https://facebook.github.io/react-native/',
expo: 'https://expo.io/',
}; };
type Props = { type Props = {
@ -153,12 +152,6 @@ class AboutScreen extends React.Component<Props, State> {
text: i18n.t('aboutScreen.reactNative'), text: i18n.t('aboutScreen.reactNative'),
showChevron: true showChevron: true
}, },
{
onPressCallback: () => openWebLink(links.react),
icon: 'language-javascript',
text: i18n.t('aboutScreen.expo'),
showChevron: true
},
{ {
onPressCallback: () => this.props.navigation.navigate('dependencies'), onPressCallback: () => this.props.navigation.navigate('dependencies'),
icon: 'developer-board', icon: 'developer-board',

View file

@ -44,7 +44,7 @@ class FeedItemScreen extends React.Component<Props> {
getHeaderButton = () => { getHeaderButton = () => {
return <MaterialHeaderButtons> return <MaterialHeaderButtons>
<Item title="main" iconName={'facebook'} onPress={this.onOutLinkPress}/> <Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/>
</MaterialHeaderButtons>; </MaterialHeaderButtons>;
}; };

View file

@ -17,7 +17,6 @@ import AnimatedFAB from "../../components/Animations/AnimatedFAB";
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomTheme} from "../../managers/ThemeManager";
import {View} from "react-native-animatable"; import {View} from "react-native-animatable";
import {HiddenItem} from "react-navigation-header-buttons";
import ConnectionManager from "../../managers/ConnectionManager"; import ConnectionManager from "../../managers/ConnectionManager";
import LogoutDialog from "../../components/Amicale/LogoutDialog"; import LogoutDialog from "../../components/Amicale/LogoutDialog";
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
@ -178,8 +177,8 @@ class HomeScreen extends React.Component<Props, State> {
const onPressAbout = () => this.props.navigation.navigate("about"); const onPressAbout = () => this.props.navigation.navigate("about");
return <MaterialHeaderButtons> return <MaterialHeaderButtons>
<Item title="log" iconName={logIcon} color={logColor} onPress={onPressLog}/> <Item title="log" iconName={logIcon} color={logColor} onPress={onPressLog}/>
<HiddenItem title={i18n.t("screens.settings")} iconName={"settings"} onPress={onPressSettings}/> <Item title={i18n.t("screens.settings")} iconName={"settings"} onPress={onPressSettings}/>
<HiddenItem title={i18n.t("screens.about")} iconName={"information"} onPress={onPressAbout}/> <Item title={i18n.t("screens.about")} iconName={"information"} onPress={onPressAbout}/>
</MaterialHeaderButtons>; </MaterialHeaderButtons>;
}; };

View file

@ -36,6 +36,7 @@ class FeedbackScreen extends React.Component<Props> {
}}> }}>
<Button <Button
icon="email" icon="email"
mode={"contained"}
style={{ style={{
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 5, marginTop: 5,
@ -45,6 +46,8 @@ class FeedbackScreen extends React.Component<Props> {
</Button> </Button>
<Button <Button
icon="git" icon="git"
mode={"contained"}
color={"#609927"}
style={{ style={{
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 5, marginTop: 5,
@ -54,6 +57,8 @@ class FeedbackScreen extends React.Component<Props> {
</Button> </Button>
<Button <Button
icon="facebook" icon="facebook"
mode={"contained"}
color={"#2e88fe"}
style={{ style={{
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 5, marginTop: 5,

View file

@ -1,39 +1,47 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {ScrollView} from "react-native"; import {ScrollView, View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager";
import ThemeManager from '../../managers/ThemeManager'; import ThemeManager from '../../managers/ThemeManager';
import i18n from "i18n-js"; import i18n from "i18n-js";
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {Card, List, Switch, ToggleButton} from 'react-native-paper'; import {Card, List, Switch, ToggleButton, withTheme} from 'react-native-paper';
import {Appearance} from "react-native-appearance"; import {Appearance} from "react-native-appearance";
import AnimatedAccordion from "../../components/Animations/AnimatedAccordion"; import CustomSlider from "../../components/Overrides/CustomSlider";
type Props = { type Props = {
navigation: Object, theme: CustomTheme,
}; };
type State = { type State = {
nightMode: boolean, nightMode: boolean,
nightModeFollowSystem: boolean, nightModeFollowSystem: boolean,
proxiwashNotifPickerSelected: string, notificationReminderSelected: number,
startScreenPickerSelected: string, startScreenPickerSelected: string,
}; };
/** /**
* 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.
*/ */
export default class SettingsScreen extends React.Component<Props, State> { class SettingsScreen extends React.Component<Props, State> {
state = {
nightMode: ThemeManager.getNightMode(), savedNotificationReminder: number;
nightModeFollowSystem: AsyncStorageManager.getInstance().preferences.nightModeFollowSystem.current === '1' &&
Appearance.getColorScheme() !== 'no-preference',
proxiwashNotifPickerSelected: AsyncStorageManager.getInstance().preferences.proxiwashNotifications.current,
startScreenPickerSelected: AsyncStorageManager.getInstance().preferences.defaultStartScreen.current,
};
constructor() { constructor() {
super(); super();
let notifReminder = AsyncStorageManager.getInstance().preferences.proxiwashNotifications.current;
this.savedNotificationReminder = parseInt(notifReminder);
if (isNaN(this.savedNotificationReminder))
this.savedNotificationReminder = 0;
this.state = {
nightMode: ThemeManager.getNightMode(),
nightModeFollowSystem: AsyncStorageManager.getInstance().preferences.nightModeFollowSystem.current === '1' &&
Appearance.getColorScheme() !== 'no-preference',
notificationReminderSelected: this.savedNotificationReminder,
startScreenPickerSelected: AsyncStorageManager.getInstance().preferences.defaultStartScreen.current,
};
} }
/** /**
@ -41,14 +49,10 @@ export default class SettingsScreen extends React.Component<Props, State> {
* *
* @param value The value to store * @param value The value to store
*/ */
onProxiwashNotifPickerValueChange = (value: string) => { onProxiwashNotifPickerValueChange = (value: number) => {
if (value != null) { let key = AsyncStorageManager.getInstance().preferences.proxiwashNotifications.key;
let key = AsyncStorageManager.getInstance().preferences.proxiwashNotifications.key; AsyncStorageManager.getInstance().savePref(key, value.toString());
AsyncStorageManager.getInstance().savePref(key, value); this.setState({notificationReminderSelected: value})
this.setState({
proxiwashNotifPickerSelected: value
});
}
}; };
/** /**
@ -73,15 +77,16 @@ export default class SettingsScreen extends React.Component<Props, State> {
*/ */
getProxiwashNotifPicker() { getProxiwashNotifPicker() {
return ( return (
<ToggleButton.Row <CustomSlider
style={{flex: 1, marginHorizontal: 10, height: 50}}
minimumValue={0}
maximumValue={10}
step={1}
value={this.savedNotificationReminder}
onValueChange={this.onProxiwashNotifPickerValueChange} onValueChange={this.onProxiwashNotifPickerValueChange}
value={this.state.proxiwashNotifPickerSelected} thumbTintColor={this.props.theme.colors.primary}
style={{marginLeft: 'auto', marginRight: 'auto'}} minimumTrackTintColor={this.props.theme.colors.primary}
> />
<ToggleButton icon="close" value="never"/>
<ToggleButton icon="numeric-2" value="2"/>
<ToggleButton icon="numeric-5" value="5"/>
</ToggleButton.Row>
); );
} }
@ -133,6 +138,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
* @param icon The icon name to display on the list item * @param icon The icon name to display on the list item
* @param title The text to display as this list item title * @param title The text to display as this list item title
* @param subtitle The text to display as this list item subtitle * @param subtitle The text to display as this list item subtitle
* @param state The current state of the switch
* @returns {React.Node} * @returns {React.Node}
*/ */
getToggleItem(onPressCallback: Function, icon: string, title: string, subtitle: string, state: boolean) { getToggleItem(onPressCallback: Function, icon: string, title: string, subtitle: string, state: boolean) {
@ -141,7 +147,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
title={title} title={title}
description={subtitle} description={subtitle}
left={props => <List.Icon {...props} icon={icon}/>} left={props => <List.Icon {...props} icon={icon}/>}
right={props => right={() =>
<Switch <Switch
value={state} value={state}
onValueChange={onPressCallback} onValueChange={onPressCallback}
@ -177,28 +183,31 @@ export default class SettingsScreen extends React.Component<Props, State> {
this.state.nightMode this.state.nightMode
) : null ) : null
} }
<AnimatedAccordion <List.Item
title={i18n.t('settingsScreen.startScreen')} title={i18n.t('settingsScreen.startScreen')}
subtitle={i18n.t('settingsScreen.startScreenSub')} subtitle={i18n.t('settingsScreen.startScreenSub')}
left={props => <List.Icon {...props} icon="power"/>} left={props => <List.Icon {...props} icon="power"/>}
> />
{this.getStartScreenPicker()} {this.getStartScreenPicker()}
</AnimatedAccordion>
</List.Section> </List.Section>
</Card> </Card>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title title="Proxiwash"/> <Card.Title title="Proxiwash"/>
<List.Section> <List.Section>
<AnimatedAccordion <List.Item
title={i18n.t('settingsScreen.proxiwashNotifReminder')} title={i18n.t('settingsScreen.proxiwashNotifReminder')}
description={i18n.t('settingsScreen.proxiwashNotifReminderSub')} description={i18n.t('settingsScreen.proxiwashNotifReminderSub')}
left={props => <List.Icon {...props} icon="washing-machine"/>} left={props => <List.Icon {...props} icon="washing-machine"/>}
> opened={true}
/>
<View style={{marginLeft: 30}}>
{this.getProxiwashNotifPicker()} {this.getProxiwashNotifPicker()}
</AnimatedAccordion> </View>
</List.Section> </List.Section>
</Card> </Card>
</ScrollView> </ScrollView>
); );
} }
} }
export default withTheme(SettingsScreen);

View file

@ -10,7 +10,7 @@ type Props = {
navigation: Object, navigation: Object,
}; };
const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/proxiwash-logo.png"; const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proxiwash.png";
/** /**
* Class defining the proxiwash about screen. * Class defining the proxiwash about screen.

View file

@ -18,6 +18,7 @@ import type {CustomTheme} from "../../managers/ThemeManager";
import {Collapsible} from "react-navigation-collapsible"; import {Collapsible} from "react-navigation-collapsible";
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from "@react-navigation/stack";
import {getCleanedMachineWatched, getMachineEndDate, isMachineWatched} from "../../utils/Proxiwash"; import {getCleanedMachineWatched, getMachineEndDate, isMachineWatched} from "../../utils/Proxiwash";
import {Modalize} from "react-native-modalize";
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
@ -55,9 +56,12 @@ type State = {
*/ */
class ProxiwashScreen extends React.Component<Props, State> { class ProxiwashScreen extends React.Component<Props, State> {
modalRef: Object; modalRef: null | Modalize;
fetchedData: Object; fetchedData: {
dryers: Array<Machine>,
washers: Array<Machine>,
};
state = { state = {
refreshing: false, refreshing: false,
@ -95,7 +99,10 @@ class ProxiwashScreen extends React.Component<Props, State> {
*/ */
componentDidMount() { componentDidMount() {
this.props.navigation.setOptions({ this.props.navigation.setOptions({
headerRight: this.getAboutButton, headerRight: () =>
<MaterialHeaderButtons>
<Item title="information" iconName="information" onPress={this.onAboutPress}/>
</MaterialHeaderButtons>,
}); });
} }
@ -105,16 +112,6 @@ class ProxiwashScreen extends React.Component<Props, State> {
*/ */
onAboutPress = () => this.props.navigation.navigate('proxiwash-about'); onAboutPress = () => this.props.navigation.navigate('proxiwash-about');
/**
* Gets the about header button
*
* @return {*}
*/
getAboutButton = () =>
<MaterialHeaderButtons>
<Item title="information" iconName="information" onPress={this.onAboutPress}/>
</MaterialHeaderButtons>;
/** /**
* Extracts the key for the given item * Extracts the key for the given item
* *
@ -123,7 +120,6 @@ class ProxiwashScreen extends React.Component<Props, State> {
*/ */
getKeyExtractor = (item: Machine) => item.number; getKeyExtractor = (item: Machine) => item.number;
/** /**
* Setups notifications for the machine with the given ID. * Setups notifications for the machine with the given ID.
* One notification will be sent at the end of the program. * One notification will be sent at the end of the program.
@ -141,7 +137,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
this.showNotificationsDisabledWarning(); this.showNotificationsDisabledWarning();
}); });
} else { } else {
Notifications.setupMachineNotification(machine.number, false) Notifications.setupMachineNotification(machine.number, false, null)
.then(() => { .then(() => {
this.removeNotificationFromState(machine); this.removeNotificationFromState(machine);
}); });
@ -271,6 +267,10 @@ class ProxiwashScreen extends React.Component<Props, State> {
let message = modalStateStrings[ProxiwashConstants.machineStates[item.state]]; let message = modalStateStrings[ProxiwashConstants.machineStates[item.state]];
const onPress = this.onSetupNotificationsPress.bind(this, item); const onPress = this.onSetupNotificationsPress.bind(this, item);
if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates["EN COURS"]) { if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates["EN COURS"]) {
let remainingTime = parseInt(item.remainingTime)
if (remainingTime < 0)
remainingTime = 0;
button = button =
{ {
text: isMachineWatched(item, this.state.machinesWatched) ? text: isMachineWatched(item, this.state.machinesWatched) ?
@ -284,7 +284,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
{ {
start: item.startTime, start: item.startTime,
end: item.endTime, end: item.endTime,
remaining: item.remainingTime remaining: remainingTime
}); });
} else if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates.DISPONIBLE) { } else if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates.DISPONIBLE) {
if (isDryer) if (isDryer)

View file

@ -10,7 +10,7 @@ type Props = {
navigation: Object, navigation: Object,
}; };
const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/proximo-logo.png"; const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png";
/** /**
* Class defining the proximo about screen. * Class defining the proximo about screen.

View file

@ -20,17 +20,22 @@ type Props = {
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
theme: CustomTheme, theme: CustomTheme,
} }
const BIB_IMAGE = "https://scontent-cdg2-1.xx.fbcdn.net/v/t1.0-9/50695561_2124263197597162_2325349608210825216_n.jpg?_nc_cat=109&_nc_sid=8bfeb9&_nc_ohc=tmcV6FWO7_kAX9vfWHU&_nc_ht=scontent-cdg2-1.xx&oh=3b81c76e46b49f7c3a033ea3b07ec212&oe=5EC59B4D";
const RU_IMAGE = "https://scontent-cdg2-1.xx.fbcdn.net/v/t1.0-9/47123773_2041883702501779_5289372776166064128_o.jpg?_nc_cat=100&_nc_sid=cdbe9c&_nc_ohc=dpuBGlIIy_EAX8CyC0l&_nc_ht=scontent-cdg2-1.xx&oh=5c5bb4f0c7f12b554246f7c9b620a5f3&oe=5EC4DB31";
const ROOM_IMAGE = "https://scontent-cdt1-1.xx.fbcdn.net/v/t1.0-9/47041013_2043521689004647_316124496522117120_n.jpg?_nc_cat=103&_nc_sid=8bfeb9&_nc_ohc=bIp8OVJvvSEAX8mKnDZ&_nc_ht=scontent-cdt1-1.xx&oh=b4fef72a645804a849ad30e9e20fca12&oe=5EC29309";
const EMAIL_IMAGE = "https://etud-mel.insa-toulouse.fr/webmail/images/logo-bluemind.png";
const ENT_IMAGE = "https://ent.insa-toulouse.fr/media/org/jasig/portal/layout/tab-column/xhtml-theme/insa/institutional/LogoInsa.png";
const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/proximo-logo.png" const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
const WIKETUD_LINK = "https://wiki.etud.insa-toulouse.fr/resources/assets/wiketud.png?ff051"; const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/";
const AMICALE_IMAGE = require("../../../assets/amicale.png"); const AMICALE_IMAGE = require("../../../assets/amicale.png");
const EE_IMAGE = "https://etud.insa-toulouse.fr/~eeinsat/wp-content/uploads/2019/09/logo-blanc.png";
const TUTORINSA_IMAGE = "https://www.etud.insa-toulouse.fr/~tutorinsa/public/images/logo-gray.png"; const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png"
const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png";
const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png";
const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png";
const BIB_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png";
const RU_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/RU.png";
const ROOM_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png";
const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png";
const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png";
export type listItem = { export type listItem = {
title: string, title: string,
@ -59,19 +64,25 @@ class ServicesScreen extends React.Component<Props, State> {
{ {
title: i18n.t('screens.clubsAbout'), title: i18n.t('screens.clubsAbout'),
subtitle: "CLUB LIST", subtitle: "CLUB LIST",
image: AMICALE_IMAGE, image: CLUBS_IMAGE,
onPress: () => nav.navigate("club-list"), onPress: () => nav.navigate("club-list"),
}, },
{ {
title: i18n.t('screens.profile'), title: i18n.t('screens.profile'),
subtitle: "PROFIL", subtitle: "PROFIL",
image: AMICALE_IMAGE, image: PROFILE_IMAGE,
onPress: () => nav.navigate("profile"), onPress: () => nav.navigate("profile"),
}, },
{
title: i18n.t('screens.amicaleWebsite'),
subtitle: "AMICALE",
image: AMICALE_IMAGE,
onPress: () => nav.navigate("amicale-website"),
},
{ {
title: i18n.t('screens.vote'), title: i18n.t('screens.vote'),
subtitle: "ELECTIONS", subtitle: "ELECTIONS",
image: AMICALE_IMAGE, image: VOTE_IMAGE,
onPress: () => nav.navigate("vote"), onPress: () => nav.navigate("vote"),
}, },
]; ];
@ -82,16 +93,10 @@ class ServicesScreen extends React.Component<Props, State> {
image: PROXIMO_IMAGE, image: PROXIMO_IMAGE,
onPress: () => nav.navigate("proximo"), onPress: () => nav.navigate("proximo"),
}, },
{
title: i18n.t('screens.amicaleWebsite'),
subtitle: "AMICALE",
image: AMICALE_IMAGE,
onPress: () => nav.navigate("amicale-website"),
},
{ {
title: "Wiketud", title: "Wiketud",
subtitle: "wiketud", subtitle: "wiketud",
image: WIKETUD_LINK, image: WIKETUD_IMAGE,
onPress: () => nav.navigate("wiketud"), onPress: () => nav.navigate("wiketud"),
}, },
{ {
@ -201,7 +206,7 @@ class ServicesScreen extends React.Component<Props, State> {
return <Avatar.Image return <Avatar.Image
{...props} {...props}
size={48} size={48}
source={AMICALE_IMAGE} source={source}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
/> />
else else

View file

@ -2,6 +2,7 @@
import {checkNotifications, requestNotifications, RESULTS} from 'react-native-permissions'; import {checkNotifications, requestNotifications, RESULTS} from 'react-native-permissions';
import AsyncStorageManager from "../managers/AsyncStorageManager"; import AsyncStorageManager from "../managers/AsyncStorageManager";
import i18n from "i18n-js";
const PushNotification = require("react-native-push-notification"); const PushNotification = require("react-native-push-notification");
@ -38,8 +39,8 @@ function createNotifications(machineID: string, date: Date) {
let reminderDate = new Date(date); let reminderDate = new Date(date);
reminderDate.setMinutes(reminderDate.getMinutes() - reminder); reminderDate.setMinutes(reminderDate.getMinutes() - reminder);
PushNotification.localNotificationSchedule({ PushNotification.localNotificationSchedule({
title: "Title", title: i18n.t("proxiwashScreen.notifications.machineRunningTitle", {time: reminder}),
message: "Message", message: i18n.t("proxiwashScreen.notifications.machineRunningBody", {number: machineID}),
id: id.toString(), id: id.toString(),
date: reminderDate, date: reminderDate,
}); });
@ -48,8 +49,8 @@ function createNotifications(machineID: string, date: Date) {
console.log("Setting up notifications for ", date); console.log("Setting up notifications for ", date);
PushNotification.localNotificationSchedule({ PushNotification.localNotificationSchedule({
title: "Title", title: i18n.t("proxiwashScreen.notifications.machineFinishedTitle"),
message: "Message", message: i18n.t("proxiwashScreen.notifications.machineFinishedBody", {number: machineID}),
id: machineID, id: machineID,
date: date, date: date,
}); });
@ -62,7 +63,7 @@ function createNotifications(machineID: string, date: Date) {
* @param isEnabled True to enable notifications, false to disable * @param isEnabled True to enable notifications, false to disable
* @param endDate * @param endDate
*/ */
export async function setupMachineNotification(machineID: string, isEnabled: boolean, endDate?: Date) { export async function setupMachineNotification(machineID: string, isEnabled: boolean, endDate: Date | null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (isEnabled && endDate != null) { if (isEnabled && endDate != null) {
askPermissions() askPermissions()

View file

@ -1,22 +1,41 @@
// @flow // @flow
import type {Machine} from "../screens/Proxiwash/ProxiwashScreen"; import type {Machine} from "../screens/Proxiwash/ProxiwashScreen";
import ProxiwashConstants from "../constants/ProxiwashConstants";
export function getMachineEndDate(machine: Machine) { /**
* Gets the machine end Date object.
* If the end time is at least 12 hours before the current time,
* it will be considered as happening the day after.
* If it is before but less than 12 hours, it will be considered invalid (to fix proxiwash delay)
*
* @param machine The machine to get the date from
* @returns {Date} The date object representing the end time.
*/
export function getMachineEndDate(machine: Machine): Date | null {
const array = machine.endTime.split(":"); const array = machine.endTime.split(":");
let date = new Date(); let endDate = new Date(Date.now());
date.setHours(parseInt(array[0]), parseInt(array[1])); endDate.setHours(parseInt(array[0]), parseInt(array[1]));
if (date < new Date())
date.setDate(date.getDate() + 1); let limit = new Date(Date.now());
return date; if (endDate < limit) {
if (limit.getHours() > 12) {
limit.setHours(limit.getHours() - 12);
if (endDate < limit)
endDate.setDate(endDate.getDate() + 1);
else
endDate = null;
} else
endDate = null;
}
return endDate;
} }
/** /**
* Checks whether the machine of the given ID has scheduled notifications * Checks whether the machine of the given ID has scheduled notifications
* *
* @param machine * @param machine The machine to check
* @param machineList * @param machineList The machine list
* @returns {boolean} * @returns {boolean}
*/ */
export function isMachineWatched(machine: Machine, machineList: Array<Machine>) { export function isMachineWatched(machine: Machine, machineList: Array<Machine>) {
@ -30,7 +49,14 @@ export function isMachineWatched(machine: Machine, machineList: Array<Machine>)
return watched; return watched;
} }
function getMachineOfId(id: string, allMachines: Array<Machine>) { /**
* Gets the machine of the given id
*
* @param id The machine's ID
* @param allMachines The machine list
* @returns {null|Machine} The machine or null if not found
*/
export function getMachineOfId(id: string, allMachines: Array<Machine>) {
for (let i = 0; i < allMachines.length; i++) { for (let i = 0; i < allMachines.length; i++) {
if (allMachines[i].number === id) if (allMachines[i].number === id)
return allMachines[i]; return allMachines[i];
@ -38,13 +64,22 @@ function getMachineOfId(id: string, allMachines: Array<Machine>) {
return null; return null;
} }
export function getCleanedMachineWatched(machineList: Array<Machine>, allMachines: Array<Machine>) { /**
* Gets a cleaned machine watched list by removing invalid entries.
* An entry is considered invalid if the end time in the watched list
* and in the full list does not match (a new machine cycle started)
*
* @param machineWatchedList The current machine watch list
* @param allMachines The current full machine list
* @returns {Array<Machine>}
*/
export function getCleanedMachineWatched(machineWatchedList: Array<Machine>, allMachines: Array<Machine>) {
let newList = []; let newList = [];
for (let i = 0; i < machineList.length; i++) { for (let i = 0; i < machineWatchedList.length; i++) {
let machine = getMachineOfId(machineList[i].number, allMachines); let machine = getMachineOfId(machineWatchedList[i].number, allMachines);
if (machine !== null if (machine !== null
&& machineList[i].number === machine.number && machineList[i].endTime === machine.endTime && machineWatchedList[i].number === machine.number
&& ProxiwashConstants.machineStates[machineList[i].state] === ProxiwashConstants.machineStates["EN COURS"]) { && machineWatchedList[i].endTime === machine.endTime) {
newList.push(machine); newList.push(machine);
} }
} }