diff --git a/App.js b/App.js index 76b7d26..64d6c10 100644 --- a/App.js +++ b/App.js @@ -1,9 +1,8 @@ // @flow -import React from 'react'; +import * as React from 'react'; import {Root, StyleProvider, Text} from 'native-base'; -import {Ionicons} from '@expo/vector-icons'; -import {StyleSheet, View, Image} from 'react-native' +import {Image, StyleSheet, View} from 'react-native' import AppNavigator from './navigation/AppNavigator'; import ThemeManager from './utils/ThemeManager'; import LocaleManager from './utils/LocaleManager'; @@ -43,6 +42,7 @@ const styles = StyleSheet.create({ }, }); +// Content to be used int the intro slides const slides = [ { key: '1', @@ -61,7 +61,7 @@ const slides = [ { key: '3', title: 'Le proximo', - text: 'Regardez le stock de la supérette de l\'INSA depuis n\'importe où' , + text: 'Regardez le stock de la supérette de l\'INSA depuis n\'importe où', icon: 'shopping', colors: ['#f9a967', '#da5204'], }, @@ -101,12 +101,14 @@ export default class App extends React.Component { * @returns {Promise} */ async componentWillMount() { + // Wait for custom fonts to be loaded before showing the app await Font.loadAsync({ 'Roboto': require('native-base/Fonts/Roboto.ttf'), 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'), }); await AsyncStorageManager.getInstance().loadPreferences(); ThemeManager.getInstance().setUpdateThemeCallback(() => this.updateTheme()); + // Only show intro if this is the first time starting the app this.setState({ isLoading: false, currentTheme: ThemeManager.getCurrentTheme(), @@ -115,17 +117,20 @@ export default class App extends React.Component { } /** - * Updates the theme and clears the cache to force reloading the app colors + * Updates the theme and clears the cache to force reloading the app colors. Need to edit shoutem theme for ti to work */ updateTheme() { - // console.log('update theme called'); this.setState({ currentTheme: ThemeManager.getCurrentTheme() }); clearThemeCache(); } - + /** + * Render item to be used for the intro slides + * @param item + * @param dimensions + */ getIntroRenderItem(item: Object, dimensions: Object) { return ( { ); } + /** + * Callback when user ends the intro. Save in preferences to avaoid showing back the slides + */ onIntroDone() { this.setState({showIntro: false}); AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.showIntro.key, '0'); @@ -155,8 +163,6 @@ export default class App extends React.Component { /** * Renders the app based on loading state - * - * @returns {*} */ render() { if (this.state.isLoading) { diff --git a/app.json b/app.json index c75b5ab..1ed1a35 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,7 @@ { "expo": { "name": "Amicale INSAT", + "description": "Application mobile compatible Android et iOS pour l'Amicale INSA Toulouse. Grâce à cette application, vous avez facilement accès aux news du campus, aux emplois du temps, à l'état de la laverie, et bien d'autres services ! Ceci est une version Beta, Toutes les fonctionnalités ne sont pas encore implémentées, et il est possible de rencontrer quelques bugs.", "slug": "application-amicale", "privacy": "public", "sdkVersion": "33.0.0", @@ -9,29 +10,42 @@ "android", "web" ], - "version": "0.0.4", + "version": "0.0.5", "orientation": "portrait", - "icon": "./assets/icon.png", "primaryColor": "#e42612", + "icon": "./assets/icon.png", "splash": { - "image": "./assets/splash.png", - "resizeMode": "cover", - "backgroundColor": "#fff" + "backgroundColor": "#fff", + "resizeMode": "contain", + "image": "./assets/splash.png" + }, + "notification": { + "icon": "./assets/amicale-notification.png", + "color": "#e42612", + "androidMode": "default" }, "updates": { - "fallbackToCacheTimeout": 0 + "enabled": false }, "assetBundlePatterns": [ "**/*" ], "ios": { - "supportsTablet": true, - "icon": "./assets/ios.icon.png", - "bundleIdentifier": "com.test.applicationamicale" + "bundleIdentifier": "amicale.insat.application", + "buildNumber": "0.0.5", + "icon": "./assets/ios.icon.png" }, "android": { + "package": "amicale.insat.application", + "versionCode": 1, "icon": "./assets/android.icon.png", - "package": "com.test.applicationamicale" + "adaptiveIcon": { + "foregroundImage": "./assets/android.adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "permissions": [ + "VIBRATE" + ] } } } diff --git a/assets/amicale-notification.png b/assets/amicale-notification.png new file mode 100644 index 0000000..b7a4789 Binary files /dev/null and b/assets/amicale-notification.png differ diff --git a/assets/android.icon.png b/assets/android.icon.png index 7546192..a9a3217 100644 Binary files a/assets/android.icon.png and b/assets/android.icon.png differ diff --git a/assets/splash.png b/assets/splash.png index 662de06..ecbd5f3 100644 Binary files a/assets/splash.png and b/assets/splash.png differ diff --git a/components/CustomHeader.js b/components/CustomHeader.js index 4a354ae..01aba89 100644 --- a/components/CustomHeader.js +++ b/components/CustomHeader.js @@ -1,8 +1,8 @@ // @flow import * as React from "react"; -import {Body, Header, Icon, Left, Right, Title} from "native-base"; -import {StyleSheet, Platform} from "react-native"; +import {Body, Header, Left, Right, Title} from "native-base"; +import {Platform, StyleSheet} from "react-native"; import {getStatusBarHeight} from "react-native-status-bar-height"; import Touchable from 'react-native-platform-touchable'; import ThemeManager from "../utils/ThemeManager"; @@ -34,6 +34,7 @@ export default class CustomHeader extends React.Component { render() { let button; + // Does the app have a back button or a burger menu ? if (this.props.backButton) button = , }; +/** + * Class used to create a basic list view using online json data. + * Used by inheriting from it and redefining getters. + */ export default class FetchedDataSectionList extends React.Component { webDataManager: WebDataManager; - willFocusSubscription : function; - willBlurSubscription : function; + willFocusSubscription: function; + willBlurSubscription: function; refreshInterval: IntervalID; refreshTime: number; - constructor(fetchUrl: string, refreshTime : number) { + constructor(fetchUrl: string, refreshTime: number) { super(); this.webDataManager = new WebDataManager(fetchUrl); this.refreshTime = refreshTime; @@ -42,16 +46,25 @@ export default class FetchedDataSectionList extends React.Component { return ["whoa", "nah"]; } /** - * Register react navigation events on first screen load + * Register react navigation events on first screen load. + * Allows to detect when the screen is focused */ componentDidMount() { this.willFocusSubscription = this.props.navigation.addListener( @@ -68,28 +81,37 @@ export default class FetchedDataSectionList extends React.Component 0) this.refreshInterval = setInterval(() => this._onRefresh(), this.refreshTime) } + /** + * Remove any interval on un-focus + */ onScreenBlur() { clearInterval(this.refreshInterval); } - + /** + * Unregister from event when un-mounting components + */ componentWillUnmount() { if (this.willBlurSubscription !== undefined) this.willBlurSubscription.remove(); if (this.willFocusSubscription !== undefined) this.willFocusSubscription.remove(); - } - + /** + * Refresh data and show a toast if any error occurred + * @private + */ _onRefresh = () => { - console.log('refresh'); this.setState({refreshing: true}); this.webDataManager.readData().then((fetchedData) => { this.setState({ @@ -101,14 +123,38 @@ export default class FetchedDataSectionList extends React.Component; } + /** + * Get the render item to be used for the section title in the list. + * Must be overridden by inheriting class. + * + * @param title + * @return {*} + */ getRenderSectionHeader(title: String) { return ; } + /** + * Get the render item to be used when the list is empty. + * No need to be overridden, has good defaults. + * + * @param text + * @param icon + * @return {*} + */ getEmptyRenderItem(text: string, icon: string) { return ( @@ -138,7 +184,9 @@ export default class FetchedDataSectionList extends React.Component) { let isEmpty = dataset[0].data.length === 0; if (isEmpty) @@ -201,6 +268,12 @@ export default class FetchedDataSectionList extends React.Component) { let tabbedView = []; for (let i = 0; i < dataset.length; i++) { @@ -214,7 +287,7 @@ export default class FetchedDataSectionList extends React.Component{dataset[i].title} } key={dataset[i].title} - style={{backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor}}> + style={{backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor}}> {this.getSectionList( [ { diff --git a/components/SideMenu.js b/components/SideMenu.js index 503ac3b..baff6ea 100644 --- a/components/SideMenu.js +++ b/components/SideMenu.js @@ -1,8 +1,8 @@ // @flow import * as React from 'react'; -import {Platform, Dimensions, StyleSheet, Image, FlatList, Linking} from 'react-native'; -import {Badge, Text, Container, Content, Left, ListItem, Right} from "native-base"; +import {Dimensions, FlatList, Image, Linking, Platform, StyleSheet} from 'react-native'; +import {Badge, Container, Content, Left, ListItem, Right, Text} from "native-base"; import i18n from "i18n-js"; import CustomMaterialIcon from '../components/CustomMaterialIcon'; @@ -40,72 +40,56 @@ export default class SideBar extends React.Component { */ constructor(props: Props) { super(props); + // Dataset used to render the drawer + // If the link field is defined, clicking on the item will open the link this.dataSet = [ { name: i18n.t('screens.home'), route: "Home", icon: "home", - bg: "#C5F442" - // types: "11" // Shows the badge }, { name: i18n.t('screens.planning'), route: "Planning", icon: "calendar-range", - bg: "#477EEA", - // types: "11" }, { name: "Proxiwash", route: "Proxiwash", icon: "washing-machine", - bg: "#477EEA", - // types: "11" }, { name: "Proximo", route: "Proximo", icon: "shopping", - bg: "#477EEA", - // types: "11" }, { name: "Amicale", route: "amicale", icon: "web", - bg: "#477EEA", link: Amicale_LINK - // types: "11" }, { name: i18n.t('screens.timetable'), route: "timetable", icon: "timetable", - bg: "#477EEA", link: TIMETABLE_LINK - // types: "11" }, { name: "Wiketud", route: "wiketud", icon: "wikipedia", - bg: "#477EEA", link: WIKETUD_LINK - // types: "11" }, { name: i18n.t('screens.settings'), route: "Settings", icon: "settings", - bg: "#477EEA", - // types: "11" }, { name: i18n.t('screens.about'), route: "About", icon: "information", - bg: "#477EEA", - // types: "11" }, ]; } diff --git a/screens/About/AboutDependenciesScreen.js b/screens/About/AboutDependenciesScreen.js index c1b6eb5..bb72b8a 100644 --- a/screens/About/AboutDependenciesScreen.js +++ b/screens/About/AboutDependenciesScreen.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import {Container, Text, Content, ListItem, Body} from 'native-base'; +import {Body, Container, Content, ListItem, Text} from 'native-base'; import CustomHeader from "../../components/CustomHeader"; import {FlatList} from "react-native"; import i18n from "i18n-js"; @@ -30,7 +30,7 @@ export default class AboutDependenciesScreen extends React.Component { const data = generateListFromObject(nav.getParam('data', {})); return ( - + { {Platform.OS === 'android' ? : } diff --git a/screens/Proximo/ProximoListScreen.js b/screens/Proximo/ProximoListScreen.js index f3fabb3..c13f414 100644 --- a/screens/Proximo/ProximoListScreen.js +++ b/screens/Proximo/ProximoListScreen.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import {Container, Text, Content, ListItem, Left, Thumbnail, Right, Body} from 'native-base'; +import {Body, Container, Content, Left, ListItem, Right, Text, Thumbnail} from 'native-base'; import CustomHeader from "../../components/CustomHeader"; import {FlatList, Platform} from "react-native"; import Touchable from 'react-native-platform-touchable'; @@ -132,8 +132,14 @@ export default class ProximoListScreen extends React.Component { this.setSortMode(this.state.currentSortMode, this.state.isSortReversed); } + /** + * get color depending on quantity available + * + * @param availableStock + * @return + */ getStockColor(availableStock: number) { - let color : string; + let color: string; if (availableStock > 3) color = ThemeManager.getCurrentThemeVariables().brandSuccess; else if (availableStock > 0) @@ -234,7 +240,8 @@ export default class ProximoListScreen extends React.Component { + color: this.getStockColor(parseInt(item.quantity)) + }}> {item.quantity + ' ' + i18n.t('proximoScreen.inStock')} diff --git a/screens/Proximo/ProximoMainScreen.js b/screens/Proximo/ProximoMainScreen.js index 4ea9c04..2ed1688 100644 --- a/screens/Proximo/ProximoMainScreen.js +++ b/screens/Proximo/ProximoMainScreen.js @@ -78,7 +78,7 @@ export default class ProximoMainScreen extends FetchedDataSectionList { return finalData; } - getRenderItem(item: Object, section : Object, data : Object) { + getRenderItem(item: Object, section: Object, data: Object) { if (item.data.length > 0) { return ( ) { this.setState({machinesWatched: data}); let prefKey = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.key; AsyncStorageManager.getInstance().savePref(prefKey, JSON.stringify(data)); @@ -266,6 +288,13 @@ export default class ProxiwashScreen extends FetchedDataSectionList { return true; } + /** + * Show an alert fo a machine, allowing to enable/disable notifications if running + * + * @param title + * @param item + * @param remainingTime + */ showAlert(title: string, item: Object, remainingTime: number) { let buttons = [{text: i18n.t("proxiwashScreen.modal.ok")}]; let message = modalStateStrings[MACHINE_STATES[item.state]]; diff --git a/screens/SettingsScreen.js b/screens/SettingsScreen.js index d6665ca..61fd43a 100644 --- a/screens/SettingsScreen.js +++ b/screens/SettingsScreen.js @@ -2,18 +2,18 @@ import * as React from 'react'; import { + Body, + Card, + CardItem, + CheckBox, Container, Content, Left, + List, ListItem, + Picker, Right, Text, - List, - CheckBox, - Body, - CardItem, - Card, - Picker, } from "native-base"; import CustomHeader from "../components/CustomHeader"; import ThemeManager from '../utils/ThemeManager'; diff --git a/utils/AsyncStorageManager.js b/utils/AsyncStorageManager.js index 4069929..aff8548 100644 --- a/utils/AsyncStorageManager.js +++ b/utils/AsyncStorageManager.js @@ -3,7 +3,9 @@ import {AsyncStorage} from "react-native"; /** - * Static class used to manage preferences + * Static class 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 { @@ -20,30 +22,36 @@ export default class AsyncStorageManager { AsyncStorageManager.instance; } - + // Object storing preferences keys, default and current values for use in the app preferences = { showIntro: { key: 'showIntro', default: '1', - current : '', + current: '', }, proxiwashNotifications: { key: 'proxiwashNotifications', default: '5', - current : '', + current: '', }, - proxiwashWatchedMachines : { + proxiwashWatchedMachines: { key: 'proxiwashWatchedMachines', default: '[]', - current : '', + current: '', }, nightMode: { key: 'nightMode', - default : '0', - current : '', + default: '0', + current: '', } }; + /** + * Set preferences object current values from AsyncStorage. + * This function should be called at the app's start. + * + * @return {Promise} + */ async loadPreferences() { let prefKeys = []; // Get all available keys @@ -51,18 +59,25 @@ export default class AsyncStorageManager { prefKeys.push(value.key); } // Get corresponding values - let resultArray : Array> = await AsyncStorage.multiGet(prefKeys); + let resultArray: Array> = await AsyncStorage.multiGet(prefKeys); // Save those values for later use for (let i = 0; i < resultArray.length; i++) { - let key : string = resultArray[i][0]; - let val : string | null = resultArray[i][1]; + let key: string = resultArray[i][0]; + let val: string | null = resultArray[i][1]; if (val === null) val = this.preferences[key].default; this.preferences[key].current = val; } } - savePref(key : string, val : string) { + /** + * Save the value associated to the given key to preferences. + * This updates the preferences object and saves it to AsynStorage. + * + * @param key + * @param val + */ + savePref(key: string, val: string) { this.preferences[key].current = val; AsyncStorage.setItem(key, val); } diff --git a/utils/ThemeManager.js b/utils/ThemeManager.js index 18252cf..cad5868 100644 --- a/utils/ThemeManager.js +++ b/utils/ThemeManager.js @@ -4,6 +4,7 @@ import platform from '../native-base-theme/variables/platform'; import platformDark from '../native-base-theme/variables/platformDark'; import getTheme from '../native-base-theme/components'; import AsyncStorageManager from "./AsyncStorageManager"; + /** * Singleton class used to manage themes */ diff --git a/utils/WebDataManager.js b/utils/WebDataManager.js index 0bca315..51f269d 100644 --- a/utils/WebDataManager.js +++ b/utils/WebDataManager.js @@ -1,17 +1,26 @@ import {Toast} from "native-base"; +/** + * Class used to get json data from the web + */ export default class WebDataManager { - FETCH_URL : string; - lastDataFetched : Object = {}; + FETCH_URL: string; + lastDataFetched: Object = {}; constructor(url) { this.FETCH_URL = url; } + /** + * Read data from FETCH_URL and return it. + * If no data was found, returns an empty object + * + * @return {Promise} + */ async readData() { - let fetchedData : Object = {}; + let fetchedData: Object = {}; try { let response = await fetch(this.FETCH_URL); fetchedData = await response.json(); @@ -23,10 +32,21 @@ export default class WebDataManager { return fetchedData; } - isDataObjectValid() { + /** + * Detects if the fetched data is not an empty object + * + * @return + */ + isDataObjectValid(): boolean { return Object.keys(this.lastDataFetched).length > 0; } + /** + * Show a toast message depending on the validity of the fetched data + * + * @param successString + * @param errorString + */ showUpdateToast(successString, errorString) { let isSuccess = this.isDataObjectValid(); if (!isSuccess) {