Reworked section list to use react native design

This commit is contained in:
keplyx 2020-03-05 19:54:56 +01:00
parent f5702297f5
commit b562357a95
10 changed files with 484 additions and 604 deletions

2
App.js
View file

@ -14,6 +14,7 @@ import ThemeManager from './utils/ThemeManager';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import DrawerNavigator from './navigation/DrawerNavigator';
import { enableScreens } from 'react-native-screens';
type Props = {};
@ -38,6 +39,7 @@ export default class App extends React.Component<Props, State> {
constructor(props: Object) {
super(props);
LocaleManager.initTranslations();
enableScreens();
}
/**

View file

@ -8,7 +8,6 @@ import Touchable from 'react-native-platform-touchable';
import ThemeManager from "../utils/ThemeManager";
import CustomMaterialIcon from "./CustomMaterialIcon";
import i18n from "i18n-js";
import {NavigationActions} from 'react-navigation';
type Props = {
hasBackButton: boolean,
@ -105,8 +104,7 @@ export default class CustomHeader extends React.Component<Props> {
onPressBack() {
const backAction = NavigationActions.back();
this.props.navigation.dispatch(backAction);
this.props.navigation.goBack();
}
render() {

View file

@ -1,399 +0,0 @@
// @flow
import * as React from 'react';
import WebDataManager from "../utils/WebDataManager";
import {H3, Spinner, Tab, TabHeading, Tabs, Text} from "native-base";
import {RefreshControl, SectionList, View} from "react-native";
import CustomMaterialIcon from "./CustomMaterialIcon";
import i18n from 'i18n-js';
import ThemeManager from "../utils/ThemeManager";
import BaseContainer from "./BaseContainer";
type Props = {
navigation: Object,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: Object,
machinesWatched: Array<string>,
};
/**
* 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<Props, State> {
webDataManager: WebDataManager;
willFocusSubscription: function;
willBlurSubscription: function;
refreshInterval: IntervalID;
refreshTime: number;
lastRefresh: Date;
minTimeBetweenRefresh = 60;
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
machinesWatched: [],
};
onRefresh: Function;
onFetchSuccess: Function;
onFetchError: Function;
renderSectionHeaderEmpty: Function;
renderSectionHeaderNotEmpty: Function;
renderItemEmpty: Function;
renderItemNotEmpty: Function;
constructor(fetchUrl: string, refreshTime: number) {
super();
this.webDataManager = new WebDataManager(fetchUrl);
this.refreshTime = refreshTime;
// creating references to functions used in render()
this.onRefresh = this.onRefresh.bind(this);
this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this);
this.renderSectionHeaderEmpty = this.renderSectionHeader.bind(this, true);
this.renderSectionHeaderNotEmpty = this.renderSectionHeader.bind(this, false);
this.renderItemEmpty = this.renderItem.bind(this, true);
this.renderItemNotEmpty = this.renderItem.bind(this, false);
}
/**
* Get the translation for the header in the current language
* @return {string}
*/
getHeaderTranslation(): string {
return "Header";
}
/**
* Get the translation for the toasts in the current language
* @return {string}
*/
getUpdateToastTranslations(): Array<string> {
return ["whoa", "nah"];
}
setMinTimeRefresh(value: number) {
this.minTimeBetweenRefresh = value;
}
/**
* Register react navigation events on first screen load.
* Allows to detect when the screen is focused
*/
componentDidMount() {
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus', this.onScreenFocus.bind(this));
this.willBlurSubscription = this.props.navigation.addListener(
'willBlur', this.onScreenBlur.bind(this));
}
/**
* Refresh data when focusing the screen and setup a refresh interval if asked to
*/
onScreenFocus() {
this.onRefresh();
if (this.refreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh.bind(this), 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();
}
onFetchSuccess(fetchedData: Object) {
this.setState({
fetchedData: fetchedData,
refreshing: false,
firstLoading: false
});
this.lastRefresh = new Date();
}
onFetchError() {
this.setState({
fetchedData: {},
refreshing: false,
firstLoading: false
});
this.webDataManager.showUpdateToast(this.getUpdateToastTranslations()[0], this.getUpdateToastTranslations()[1]);
}
/**
* Refresh data and show a toast if any error occurred
* @private
*/
onRefresh() {
let canRefresh;
if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
else
canRefresh = true;
if (canRefresh) {
this.setState({refreshing: true});
this.webDataManager.readData()
.then(this.onFetchSuccess)
.catch(this.onFetchError);
}
}
/**
* Get the render item to be used for display in the list.
* Must be overridden by inheriting class.
*
* @param item
* @param section
* @return {*}
*/
getRenderItem(item: Object, section: Object) {
return <View/>;
}
/**
* 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 <View/>;
}
/**
* Get the render item to be used when the list is empty.
* No need to be overridden, has good defaults.
*
* @param text
* @param isSpinner
* @param icon
* @return {*}
*/
getEmptyRenderItem(text: string, isSpinner: boolean, icon: string) {
return (
<View>
<View style={{
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: 100,
marginBottom: 20
}}>
{isSpinner ?
<Spinner/>
:
<CustomMaterialIcon
icon={icon}
fontSize={100}
width={100}
color={ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText}/>}
</View>
<H3 style={{
textAlign: 'center',
marginRight: 20,
marginLeft: 20,
color: ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText
}}>
{text}
</H3>
</View>);
}
/**
* Create the dataset to be used in the list from the data fetched.
* Must be overridden.
*
* @param fetchedData {Object}
* @return {Array}
*/
createDataset(fetchedData: Object): Array<Object> {
return [];
}
datasetKeyExtractor(item: Object) {
return item.text
}
/**
* Create the dataset when no fetched data is available.
* No need to be overridden, has good defaults.
*
* @return
*/
createEmptyDataset() {
return [
{
title: '',
data: [
{
text: this.state.refreshing ?
i18n.t('general.loading') :
i18n.t('general.networkError'),
isSpinner: this.state.refreshing,
icon: this.state.refreshing ?
'refresh' :
'access-point-network-off'
}
],
keyExtractor: this.datasetKeyExtractor,
}
];
}
/**
* Should the app use a tab layout instead of a section list ?
* If yes, each section will be rendered in a new tab.
* Can be overridden.
*
* @return {boolean}
*/
hasTabs() {
return false;
}
hasBackButton() {
return false;
}
getRightButton() {
return <View/>
}
hasStickyHeader() {
return false;
}
hasSideMenu() {
return true;
}
renderSectionHeader(isEmpty: boolean, {section: {title}} : Object) {
return isEmpty ?
<View/> :
this.getRenderSectionHeader(title)
}
renderItem(isEmpty: boolean, {item, section}: Object) {
return isEmpty ?
this.getEmptyRenderItem(item.text, item.isSpinner, item.icon) :
this.getRenderItem(item, section)
}
/**
* Get the section list render using the generated dataset
*
* @param dataset
* @return
*/
getSectionList(dataset: Array<Object>) {
let isEmpty = dataset[0].data.length === 0;
if (isEmpty)
dataset = this.createEmptyDataset();
return (
<SectionList
sections={dataset}
stickySectionHeadersEnabled={this.hasStickyHeader()}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
}
renderSectionHeader={isEmpty ? this.renderSectionHeaderEmpty : this.renderSectionHeaderNotEmpty}
renderItem={isEmpty ? this.renderItemEmpty : this.renderItemNotEmpty}
style={{minHeight: 300, width: '100%'}}
contentContainerStyle={
isEmpty ?
{flexGrow: 1, justifyContent: 'center', alignItems: 'center'} : {}
}
/>
);
}
/**
* Generate the tabs containing the lists
*
* @param dataset
* @return
*/
getTabbedView(dataset: Array<Object>) {
let tabbedView = [];
for (let i = 0; i < dataset.length; i++) {
tabbedView.push(
<Tab heading={
<TabHeading>
<CustomMaterialIcon
icon={dataset[i].icon}
color={ThemeManager.getCurrentThemeVariables().tabIconColor}
fontSize={20}
/>
<Text>{dataset[i].title}</Text>
</TabHeading>}
key={dataset[i].title}
style={{backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor}}>
{this.getSectionList(
[
{
title: dataset[i].title,
data: dataset[i].data,
extraData: dataset[i].extraData,
keyExtractor: dataset[i].keyExtractor
}
]
)}
</Tab>);
}
return tabbedView;
}
render() {
// console.log("rendering FetchedDataSectionList");
const dataset = this.createDataset(this.state.fetchedData);
return (
<BaseContainer
navigation={this.props.navigation}
headerTitle={this.getHeaderTranslation()}
headerRightButton={this.getRightButton()}
hasTabs={this.hasTabs()}
hasBackButton={this.hasBackButton()}
hasSideMenu={this.hasSideMenu()}
>
{this.hasTabs() ?
<Tabs
tabContainerStyle={{
elevation: 0, // Fix for android shadow
}}
>
{this.getTabbedView(dataset)}
</Tabs>
:
this.getSectionList(dataset)
}
</BaseContainer>
);
}
}

View file

@ -0,0 +1,221 @@
// @flow
import * as React from 'react';
import {H3, Spinner, View} from "native-base";
import ThemeManager from '../utils/ThemeManager';
import WebDataManager from "../utils/WebDataManager";
import CustomMaterialIcon from "./CustomMaterialIcon";
import i18n from "i18n-js";
import {RefreshControl, SectionList} from "react-native";
type Props = {
navigation: Object,
fetchUrl: string,
refreshTime: number,
renderItem: React.Node,
renderSectionHeader: React.Node,
stickyHeader: boolean,
createDataset: Function,
updateErrorText: string,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: Object,
};
/**
* Custom component defining a material icon using native base
*
* @prop active {boolean} Whether to set the icon color to active
* @prop icon {string} The icon string to use from MaterialCommunityIcons
* @prop color {string} The icon color. Use default theme color if unspecified
* @prop fontSize {number} The icon size. Use 26 if unspecified
* @prop width {number} The icon width. Use 30 if unspecified
*/
export default class WebSectionList extends React.Component<Props, State> {
static defaultProps = {
renderSectionHeader: undefined,
stickyHeader: false,
};
webDataManager: WebDataManager;
refreshInterval: IntervalID;
lastRefresh: Date;
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
};
onRefresh: Function;
onFetchSuccess: Function;
onFetchError: Function;
getEmptyRenderItem: Function;
getEmptySectionHeader: Function;
constructor() {
super();
// creating references to functions used in render()
this.onRefresh = this.onRefresh.bind(this);
this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this);
this.getEmptyRenderItem = this.getEmptyRenderItem.bind(this);
this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this);
}
/**
* Register react navigation events on first screen load.
* Allows to detect when the screen is focused
*/
componentDidMount() {
this.webDataManager = new WebDataManager(this.props.fetchUrl);
const onScreenFocus = this.onScreenFocus.bind(this);
const onScreenBlur = this.onScreenBlur.bind(this);
this.props.navigation.addListener('focus', onScreenFocus);
this.props.navigation.addListener('blur', onScreenBlur);
}
/**
* Refresh data when focusing the screen and setup a refresh interval if asked to
*/
onScreenFocus() {
this.onRefresh();
if (this.props.refreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh, this.props.refreshTime)
}
/**
* Remove any interval on un-focus
*/
onScreenBlur() {
clearInterval(this.refreshInterval);
}
onFetchSuccess(fetchedData: Object) {
this.setState({
fetchedData: fetchedData,
refreshing: false,
firstLoading: false
});
this.lastRefresh = new Date();
}
onFetchError() {
this.setState({
fetchedData: {},
refreshing: false,
firstLoading: false
});
this.webDataManager.showUpdateToast(this.props.updateErrorText);
}
/**
* Refresh data and show a toast if any error occurred
* @private
*/
onRefresh() {
let canRefresh;
if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > this.props.refreshTime;
else
canRefresh = true;
if (canRefresh) {
this.setState({refreshing: true});
this.webDataManager.readData()
.then(this.onFetchSuccess)
.catch(this.onFetchError);
}
}
getEmptySectionHeader({section}: Object) {
return <View/>;
}
getEmptyRenderItem({item}: Object) {
return (
<View>
<View style={{
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: 100,
marginBottom: 20
}}>
{this.state.refreshing ?
<Spinner/>
:
<CustomMaterialIcon
icon={item.icon}
fontSize={100}
width={100}
color={ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText}/>}
</View>
<H3 style={{
textAlign: 'center',
marginRight: 20,
marginLeft: 20,
color: ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText
}}>
{item.text}
</H3>
</View>);
}
createEmptyDataset() {
return [
{
title: '',
data: [
{
text: this.state.refreshing ?
i18n.t('general.loading') :
i18n.t('general.networkError'),
isSpinner: this.state.refreshing,
icon: this.state.refreshing ?
'refresh' :
'access-point-network-off'
}
],
keyExtractor: this.datasetKeyExtractor,
}
];
}
datasetKeyExtractor(item: Object) {
return item.text
}
render() {
let dataset = this.props.createDataset(this.state.fetchedData);
const isEmpty = dataset[0].data.length === 0;
const shouldRenderHeader = isEmpty || (this.props.renderSectionHeader !== undefined);
if (isEmpty)
dataset = this.createEmptyDataset();
return (
<SectionList
sections={dataset}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
}
renderSectionHeader={shouldRenderHeader ? this.getEmptySectionHeader : this.props.renderSectionHeader}
renderItem={isEmpty ? this.getEmptyRenderItem : this.props.renderItem}
style={{minHeight: 300, width: '100%'}}
stickySectionHeadersEnabled={this.props.stickyHeader}
contentContainerStyle={
isEmpty ?
{flexGrow: 1, justifyContent: 'center', alignItems: 'center'} : {}
}
/>
);
}
}

View file

@ -1,14 +1,16 @@
// @flow
import * as React from 'react';
import {Image, Linking, TouchableOpacity, View} from 'react-native';
import {Body, Button, Card, CardItem, H1, Left, Text, Thumbnail} from 'native-base';
import {Image, TouchableOpacity, View} from 'react-native';
import {Body, Button, Card, CardItem, Left, Text, Thumbnail} from 'native-base';
import i18n from "i18n-js";
import CustomMaterialIcon from '../components/CustomMaterialIcon';
import FetchedDataSectionList from "../components/FetchedDataSectionList";
import Autolink from 'react-native-autolink';
import ThemeManager from "../utils/ThemeManager";
import DashboardItem from "../components/DashboardItem";
import * as WebBrowser from 'expo-web-browser';
import BaseContainer from "../components/BaseContainer";
import WebSectionList from "../components/WebSectionList";
// import DATA from "../dashboard_data.json";
@ -25,46 +27,30 @@ const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
const CARD_BORDER_RADIUS = 10;
/**
* Opens a link in the device's browser
* @param link The link to open
*/
function openWebLink(link) {
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
type Props = {
navigation: Object,
}
/**
* Class defining the app's home screen
*/
export default class HomeScreen extends FetchedDataSectionList {
export default class HomeScreen extends React.Component<Props> {
onProxiwashClick: Function;
onTutorInsaClick: Function;
onMenuClick: Function;
onProximoClick: Function;
getRenderItem: Function;
createDataset: Function;
constructor() {
super(DATA_URL, REFRESH_TIME);
super();
this.onProxiwashClick = this.onProxiwashClick.bind(this);
this.onTutorInsaClick = this.onTutorInsaClick.bind(this);
this.onMenuClick = this.onMenuClick.bind(this);
this.onProximoClick = this.onProximoClick.bind(this);
}
onProxiwashClick() {
this.props.navigation.navigate('Proxiwash');
}
onTutorInsaClick() {
this.props.navigation.navigate('TutorInsaScreen');
}
onProximoClick() {
this.props.navigation.navigate('Proximo');
}
onMenuClick() {
this.props.navigation.navigate('SelfMenuScreen');
this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.bind(this);
}
/**
@ -77,12 +63,20 @@ export default class HomeScreen extends FetchedDataSectionList {
return date.toLocaleString();
}
getHeaderTranslation() {
return i18n.t("screens.home");
onProxiwashClick() {
this.props.navigation.navigate('Proxiwash');
}
getUpdateToastTranslations() {
return [i18n.t("homeScreen.listUpdated"), i18n.t("homeScreen.listUpdateFail")];
onTutorInsaClick() {
WebBrowser.openBrowserAsync("https://www.etud.insa-toulouse.fr/~tutorinsa/");
}
onProximoClick() {
this.props.navigation.navigate('Proximo');
}
onMenuClick() {
this.props.navigation.navigate('SelfMenuScreen');
}
getKeyExtractor(item: Object) {
@ -154,25 +148,6 @@ export default class HomeScreen extends FetchedDataSectionList {
return dataset
}
getRenderSectionHeader(title: string) {
if (title === '') {
return <View/>;
} else {
return (
<View style={{
backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor
}}>
<H1 style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10
}}>{title}</H1>
</View>
);
}
}
getDashboardItem(item: Object) {
let content = item['content'];
if (item['id'] === 'event')
@ -318,7 +293,7 @@ export default class HomeScreen extends FetchedDataSectionList {
if (isAvailable)
this.props.navigation.navigate('PlanningDisplayScreen', {data: displayEvent});
else
this.props.navigation.navigate('PlanningScreen');
this.props.navigation.navigate('Planning');
};
@ -345,13 +320,13 @@ export default class HomeScreen extends FetchedDataSectionList {
subtitle = i18n.t('homeScreen.dashboard.todayEventsSubtitleNA');
let displayEvent = this.getDisplayEvent(futureEvents);
const clickAction = this.clickAction.bind(this, isAvailable, displayEvent);
return (
<DashboardItem
subtitle={subtitle}
color={color}
icon={icon}
clickAction={this.clickAction.bind(this, isAvailable, displayEvent)}
clickAction={clickAction}
title={title}
isAvailable={isAvailable}
displayEvent={displayEvent}
@ -523,64 +498,90 @@ export default class HomeScreen extends FetchedDataSectionList {
);
}
openLink(link: string) {
WebBrowser.openBrowserAsync(link);
}
getRenderItem(item: Object, section: Object) {
getFeedItem(item: Object) {
const onImagePress = this.openLink.bind(this, item.full_picture);
const onOutLinkPress = this.openLink.bind(this, item.permalink_url);
return (
section['id'] === SECTIONS_ID[0] ? this.getDashboardItem(item) :
<Card style={{
flex: 0,
marginLeft: 10,
marginRight: 10,
borderRadius: CARD_BORDER_RADIUS,
<Card style={{
flex: 0,
marginLeft: 10,
marginRight: 10,
borderRadius: CARD_BORDER_RADIUS,
}}>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<Left>
<Thumbnail source={ICON_AMICALE} square/>
<Body>
<Text>{NAME_AMICALE}</Text>
<Text note>{HomeScreen.getFormattedDate(item.created_time)}</Text>
</Body>
</Left>
</CardItem>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<Left>
<Thumbnail source={ICON_AMICALE} square/>
<Body>
{item.full_picture !== '' && item.full_picture !== undefined ?
<TouchableOpacity onPress={openWebLink.bind(null, item.full_picture)}
style={{width: '100%', height: 250, marginBottom: 5}}>
<Image source={{uri: item.full_picture}}
style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain"
/>
</TouchableOpacity>
: <View/>}
{item.message !== undefined ?
<Autolink
text={item.message}
hashtag="facebook"
style={{color: ThemeManager.getCurrentThemeVariables().textColor}}
/> : <View/>
}
<Text>{NAME_AMICALE}</Text>
<Text note>{HomeScreen.getFormattedDate(item.created_time)}</Text>
</Body>
</CardItem>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<Left>
<Button transparent
onPress={openWebLink.bind(null, item.permalink_url)}>
<CustomMaterialIcon
icon="facebook"
color="#57aeff"
width={20}/>
<Text>En savoir plus</Text>
</Button>
</Left>
</CardItem>
</Card>
</Left>
</CardItem>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<Body>
{item.full_picture !== '' && item.full_picture !== undefined ?
<TouchableOpacity onPress={onImagePress}
style={{width: '100%', height: 250, marginBottom: 5}}>
<Image source={{uri: item.full_picture}}
style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain"
/>
</TouchableOpacity>
: <View/>}
{item.message !== undefined ?
<Autolink
text={item.message}
hashtag="facebook"
style={{color: ThemeManager.getCurrentThemeVariables().textColor}}
/> : <View/>
}
</Body>
</CardItem>
<CardItem style={{
backgroundColor: 'transparent'
}}>
<Left>
<Button transparent
onPress={onOutLinkPress}>
<CustomMaterialIcon
icon="facebook"
color="#57aeff"
width={20}/>
<Text>En savoir plus</Text>
</Button>
</Left>
</CardItem>
</Card>
);
}
getRenderItem({item, section}: Object) {
return (section['id'] === SECTIONS_ID[0] ?
this.getDashboardItem(item) : this.getFeedItem(item));
}
render() {
const nav = this.props.navigation;
return (
<BaseContainer
navigation={nav}
headerTitle={i18n.t('screens.home')}>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
refreshTime={REFRESH_TIME}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
updateErrorText={i18n.t("homeScreen.listUpdateFail")}/>
</BaseContainer>
);
}
}

View file

@ -41,7 +41,8 @@ function sortNameReverse(a, b) {
}
type Props = {
navigation: Object
navigation: Object,
route: Object,
}
type State = {
@ -60,16 +61,8 @@ export default class ProximoListScreen extends React.Component<Props, State> {
modalRef: { current: null | Modalize };
originalData: Array<Object>;
navData = this.props.navigation.getParam('data', []);
shouldFocusSearchBar = this.props.navigation.getParam('shouldFocusSearchBar', false);
state = {
currentlyDisplayedData: this.navData['data'].sort(sortPrice),
currentSortMode: sortMode.price,
isSortReversed: false,
sortPriceIcon: '',
sortNameIcon: '',
modalCurrentDisplayItem: {},
};
shouldFocusSearchBar: boolean;
sortMenuRef: Menu;
onMenuRef: Function;
@ -82,7 +75,16 @@ export default class ProximoListScreen extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.modalRef = React.createRef();
this.originalData = this.navData['data'];
this.originalData = this.props.route.params['data']['data'];
this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar'];
this.state = {
currentlyDisplayedData: this.originalData,
currentSortMode: sortMode.price,
isSortReversed: false,
sortPriceIcon: '',
sortNameIcon: '',
modalCurrentDisplayItem: {},
};
this.onMenuRef = this.onMenuRef.bind(this);
this.onSearchStringChange = this.onSearchStringChange.bind(this);
@ -365,7 +367,6 @@ export default class ProximoListScreen extends React.Component<Props, State> {
shouldFocusSearchBar={this.shouldFocusSearchBar}
rightButton={this.getSortMenu()}
/>
<FlatList
data={this.state.currentlyDisplayedData}
extraData={this.state.currentlyDisplayedData}

View file

@ -5,26 +5,40 @@ import {Platform, View} from 'react-native'
import {Body, Left, ListItem, Right, Text} from 'native-base';
import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon";
import FetchedDataSectionList from "../../components/FetchedDataSectionList";
import ThemeManager from "../../utils/ThemeManager";
import Touchable from "react-native-platform-touchable";
import BaseContainer from "../../components/BaseContainer";
import WebSectionList from "../../components/WebSectionList";
const DATA_URL = "https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json";
type Props = {
navigation: Object,
}
type State = {
fetchedData: Object,
}
/**
* Class defining the main proximo screen. This screen shows the different categories of articles
* offered by proximo.
*/
export default class ProximoMainScreen extends FetchedDataSectionList {
export default class ProximoMainScreen extends React.Component<Props, State> {
articles: Object;
onPressSearchBtn: Function;
onPressAboutBtn: Function;
getRenderItem: Function;
createDataset: Function;
constructor() {
super(DATA_URL, 0);
super();
this.onPressSearchBtn = this.onPressSearchBtn.bind(this);
this.onPressAboutBtn = this.onPressAboutBtn.bind(this);
this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.bind(this);
}
static sortFinalData(a: Object, b: Object) {
@ -45,14 +59,6 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
return 0;
}
getHeaderTranslation() {
return i18n.t("screens.proximo");
}
getUpdateToastTranslations() {
return [i18n.t("proximoScreen.listUpdated"), i18n.t("proximoScreen.listUpdateFail")];
}
getKeyExtractor(item: Object) {
return item !== undefined ? item.type['id'] : undefined;
}
@ -62,7 +68,7 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
{
title: '',
data: this.generateData(fetchedData),
extraData: super.state,
extraData: this.state,
keyExtractor: this.getKeyExtractor
}
];
@ -77,21 +83,22 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
*/
generateData(fetchedData: Object) {
let finalData = [];
this.articles = undefined;
if (fetchedData.types !== undefined && fetchedData.articles !== undefined) {
let types = fetchedData.types;
let articles = fetchedData.articles;
this.articles = fetchedData.articles;
finalData.push({
type: {
id: -1,
name: i18n.t('proximoScreen.all'),
icon: 'star'
},
data: this.getAvailableArticles(articles, undefined)
data: this.getAvailableArticles(this.articles, undefined)
});
for (let i = 0; i < types.length; i++) {
finalData.push({
type: types[i],
data: this.getAvailableArticles(articles, types[i])
data: this.getAvailableArticles(this.articles, types[i])
});
}
@ -128,8 +135,8 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
name: i18n.t('proximoScreen.all'),
icon: 'star'
},
data: this.state.fetchedData.articles !== undefined ?
this.getAvailableArticles(this.state.fetchedData.articles, undefined) : []
data: this.articles !== undefined ?
this.getAvailableArticles(this.articles, undefined) : []
},
};
this.props.navigation.navigate('ProximoListScreen', searchScreenData);
@ -163,7 +170,7 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
);
}
getRenderItem(item: Object, section: Object) {
getRenderItem({item}: Object) {
let dataToSend = {
shouldFocusSearchBar: false,
data: item,
@ -196,10 +203,26 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
</Right>
</ListItem>
);
} else {
} else
return <View/>;
}
}
render() {
const nav = this.props.navigation;
return (
<BaseContainer
navigation={nav}
headerTitle={i18n.t('screens.proximo')}
headerRightButton={this.getRightButton()}>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
refreshTime={0}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
updateErrorText={i18n.t("homeScreen.listUpdateFail")}/>
</BaseContainer>
);
}
}

View file

@ -6,12 +6,13 @@ import {Body, Card, CardItem, Left, Right, Text} from 'native-base';
import ThemeManager from '../../utils/ThemeManager';
import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon";
import FetchedDataSectionList from "../../components/FetchedDataSectionList";
import WebSectionList from "../../components/WebSectionList";
import NotificationsManager from "../../utils/NotificationsManager";
import PlatformTouchable from "react-native-platform-touchable";
import Touchable from "react-native-platform-touchable";
import AsyncStorageManager from "../../utils/AsyncStorageManager";
import * as Expo from "expo";
import BaseContainer from "../../components/BaseContainer";
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
@ -30,19 +31,41 @@ let stateColors = {};
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
type Props = {
navigation: Object,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: Object,
machinesWatched: Array<string>,
};
/**
* Class defining the app's proxiwash screen. This screen shows information about washing machines and
* dryers, taken from a scrapper reading proxiwash website
*/
export default class ProxiwashScreen extends FetchedDataSectionList {
export default class ProxiwashScreen extends React.Component<Props, State> {
onAboutPress: Function;
getRenderItem: Function;
createDataset: Function;
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
// machinesWatched: JSON.parse(dataString),
machinesWatched: [],
};
/**
* Creates machine state parameters using current theme and translations
*/
constructor() {
super(DATA_URL, REFRESH_TIME);
super();
let colors = ThemeManager.getCurrentThemeVariables();
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor;
@ -69,23 +92,16 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
stateIcons[MACHINE_STATES.ERREUR] = 'alert';
// let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
this.state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
// machinesWatched: JSON.parse(dataString),
machinesWatched: [],
};
this.setMinTimeRefresh(30);
// this.setMinTimeRefresh(30);
this.onAboutPress = this.onAboutPress.bind(this);
this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.bind(this);
}
/**
* Setup notification channel for android and add listeners to detect notifications fired
*/
componentDidMount() {
super.componentDidMount();
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
// Get latest watchlist from server
NotificationsManager.getMachineNotificationWatchlist((fetchedList) => {
@ -107,14 +123,6 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
}
}
getHeaderTranslation() {
return i18n.t("screens.proxiwash");
}
getUpdateToastTranslations() {
return [i18n.t("proxiwashScreen.listUpdated"), i18n.t("proxiwashScreen.listUpdateFail")];
}
getDryersKeyExtractor(item: Object) {
return item !== undefined ? "dryer" + item.number : undefined;
}
@ -212,28 +220,23 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
createDataset(fetchedData: Object) {
return [
{
title: i18n.t('proxiwashScreen.washers'),
icon: 'washing-machine',
data: fetchedData.washers === undefined ? [] : fetchedData.washers,
extraData: super.state,
keyExtractor: this.getWashersKeyExtractor
},
{
title: i18n.t('proxiwashScreen.dryers'),
icon: 'tumble-dryer',
data: fetchedData.dryers === undefined ? [] : fetchedData.dryers,
extraData: super.state,
extraData: this.state,
keyExtractor: this.getDryersKeyExtractor
},
{
title: i18n.t('proxiwashScreen.washers'),
icon: 'washing-machine',
data: fetchedData.washers === undefined ? [] : fetchedData.washers,
extraData: this.state,
keyExtractor: this.getWashersKeyExtractor
},
];
}
hasTabs(): boolean {
return true;
}
/**
* Show an alert fo a machine, allowing to enable/disable notifications if running
*
@ -292,6 +295,24 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
);
}
render() {
const nav = this.props.navigation;
return (
<BaseContainer
navigation={nav}
headerTitle={i18n.t('screens.proxiwash')}
headerRightButton={this.getRightButton()}>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
refreshTime={REFRESH_TIME}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
updateErrorText={i18n.t("proxiwashScreen.listUpdateFail")}/>
</BaseContainer>
);
}
/**
* Get list item to be rendered
*
@ -299,7 +320,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
* @param section The object describing the current SectionList section
* @returns {React.Node}
*/
getRenderItem(item: Object, section: Object) {
getRenderItem({item, section} : Object) {
let isMachineRunning = MACHINE_STATES[item.state] === MACHINE_STATES["EN COURS"];
let machineName = (section.title === i18n.t('proxiwashScreen.dryers') ? i18n.t('proxiwashScreen.dryer') : i18n.t('proxiwashScreen.washer')) + ' n°' + item.number;
let isDryer = section.title === i18n.t('proxiwashScreen.dryers');

View file

@ -5,22 +5,31 @@ import {View} from 'react-native';
import {Card, CardItem, H2, H3, Text} from 'native-base';
import ThemeManager from "../utils/ThemeManager";
import i18n from "i18n-js";
import FetchedDataSectionList from "../components/FetchedDataSectionList";
import BaseContainer from "../components/BaseContainer";
import WebSectionList from "../components/WebSectionList";
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json";
type Props = {
navigation: Object,
}
/**
* Class defining the app's menu screen.
* This screen fetches data from etud to render the RU menu
*/
export default class SelfMenuScreen extends FetchedDataSectionList {
export default class SelfMenuScreen extends React.Component<Props> {
// Hard code strings as toLocaleDateString does not work on current android JS engine
daysOfWeek = [];
monthsOfYear = [];
getRenderItem: Function;
getRenderSectionHeader: Function;
createDataset: Function;
constructor() {
super(DATA_URL, 0);
super();
this.daysOfWeek.push(i18n.t("date.daysOfWeek.monday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.wednesday"));
@ -41,32 +50,16 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
this.monthsOfYear.push(i18n.t("date.monthsOfYear.october"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.november"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.december"));
}
getHeaderTranslation() {
return i18n.t("screens.menuSelf");
}
getUpdateToastTranslations() {
return [i18n.t("homeScreen.listUpdated"), i18n.t("homeScreen.listUpdateFail")];
this.getRenderItem = this.getRenderItem.bind(this);
this.getRenderSectionHeader = this.getRenderSectionHeader.bind(this);
this.createDataset = this.createDataset.bind(this);
}
getKeyExtractor(item: Object) {
return item !== undefined ? item['name'] : undefined;
}
hasBackButton() {
return true;
}
hasStickyHeader(): boolean {
return true;
}
hasSideMenu(): boolean {
return false;
}
createDataset(fetchedData: Object) {
let result = [];
// Prevent crash by giving a default value when fetchedData is empty (not yet available)
@ -101,7 +94,8 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
return this.daysOfWeek[date.getDay() - 1] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
}
getRenderSectionHeader(title: string) {
getRenderSectionHeader({section}: Object) {
let title = "";
return (
<Card style={{
marginLeft: 10,
@ -114,12 +108,12 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
textAlign: 'center',
marginTop: 10,
marginBottom: 10
}}>{title}</H2>
}}>{section.title}</H2>
</Card>
);
}
getRenderItem(item: Object, section: Object) {
getRenderItem({item}: Object) {
return (
<Card style={{
flex: 0,
@ -167,5 +161,24 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
return name.charAt(0) + name.substr(1).toLowerCase();
}
render() {
const nav = this.props.navigation;
return (
<BaseContainer
navigation={nav}
headerTitle={i18n.t('screens.menuSelf')}
hasBackButton={true}>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
refreshTime={0}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
renderSectionHeader={this.getRenderSectionHeader}
updateErrorText={i18n.t("homeScreen.listUpdateFail")}
stickyHeader={true}/>
</BaseContainer>
);
}
}

View file

@ -45,14 +45,13 @@ export default class WebDataManager {
/**
* Show a toast message depending on the validity of the fetched data
*
* @param successString
* @param errorString
*/
showUpdateToast(successString, errorString) {
showUpdateToast(errorString) {
let isSuccess = this.isDataObjectValid();
if (!isSuccess) {
Toast.show({
text: isSuccess ? successString : errorString,
text: errorString,
buttonText: 'OK',
type: isSuccess ? "success" : "danger",
duration: 2000