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

View file

@ -8,7 +8,6 @@ import Touchable from 'react-native-platform-touchable';
import ThemeManager from "../utils/ThemeManager"; import ThemeManager from "../utils/ThemeManager";
import CustomMaterialIcon from "./CustomMaterialIcon"; import CustomMaterialIcon from "./CustomMaterialIcon";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {NavigationActions} from 'react-navigation';
type Props = { type Props = {
hasBackButton: boolean, hasBackButton: boolean,
@ -105,8 +104,7 @@ export default class CustomHeader extends React.Component<Props> {
onPressBack() { onPressBack() {
const backAction = NavigationActions.back(); this.props.navigation.goBack();
this.props.navigation.dispatch(backAction);
} }
render() { 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 // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, Linking, TouchableOpacity, View} from 'react-native'; import {Image, TouchableOpacity, View} from 'react-native';
import {Body, Button, Card, CardItem, H1, Left, Text, Thumbnail} from 'native-base'; import {Body, Button, Card, CardItem, Left, Text, Thumbnail} from 'native-base';
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomMaterialIcon from '../components/CustomMaterialIcon'; import CustomMaterialIcon from '../components/CustomMaterialIcon';
import FetchedDataSectionList from "../components/FetchedDataSectionList";
import Autolink from 'react-native-autolink'; import Autolink from 'react-native-autolink';
import ThemeManager from "../utils/ThemeManager"; import ThemeManager from "../utils/ThemeManager";
import DashboardItem from "../components/DashboardItem"; 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"; // import DATA from "../dashboard_data.json";
@ -25,46 +27,30 @@ const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
const CARD_BORDER_RADIUS = 10; const CARD_BORDER_RADIUS = 10;
/** type Props = {
* Opens a link in the device's browser navigation: Object,
* @param link The link to open
*/
function openWebLink(link) {
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
} }
/** /**
* Class defining the app's home screen * Class defining the app's home screen
*/ */
export default class HomeScreen extends FetchedDataSectionList { export default class HomeScreen extends React.Component<Props> {
onProxiwashClick: Function; onProxiwashClick: Function;
onTutorInsaClick: Function; onTutorInsaClick: Function;
onMenuClick: Function; onMenuClick: Function;
onProximoClick: Function; onProximoClick: Function;
getRenderItem: Function;
createDataset: Function;
constructor() { constructor() {
super(DATA_URL, REFRESH_TIME); super();
this.onProxiwashClick = this.onProxiwashClick.bind(this); this.onProxiwashClick = this.onProxiwashClick.bind(this);
this.onTutorInsaClick = this.onTutorInsaClick.bind(this); this.onTutorInsaClick = this.onTutorInsaClick.bind(this);
this.onMenuClick = this.onMenuClick.bind(this); this.onMenuClick = this.onMenuClick.bind(this);
this.onProximoClick = this.onProximoClick.bind(this); this.onProximoClick = this.onProximoClick.bind(this);
} this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.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');
} }
/** /**
@ -77,12 +63,20 @@ export default class HomeScreen extends FetchedDataSectionList {
return date.toLocaleString(); return date.toLocaleString();
} }
getHeaderTranslation() { onProxiwashClick() {
return i18n.t("screens.home"); this.props.navigation.navigate('Proxiwash');
} }
getUpdateToastTranslations() { onTutorInsaClick() {
return [i18n.t("homeScreen.listUpdated"), i18n.t("homeScreen.listUpdateFail")]; WebBrowser.openBrowserAsync("https://www.etud.insa-toulouse.fr/~tutorinsa/");
}
onProximoClick() {
this.props.navigation.navigate('Proximo');
}
onMenuClick() {
this.props.navigation.navigate('SelfMenuScreen');
} }
getKeyExtractor(item: Object) { getKeyExtractor(item: Object) {
@ -154,25 +148,6 @@ export default class HomeScreen extends FetchedDataSectionList {
return dataset 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) { getDashboardItem(item: Object) {
let content = item['content']; let content = item['content'];
if (item['id'] === 'event') if (item['id'] === 'event')
@ -318,7 +293,7 @@ export default class HomeScreen extends FetchedDataSectionList {
if (isAvailable) if (isAvailable)
this.props.navigation.navigate('PlanningDisplayScreen', {data: displayEvent}); this.props.navigation.navigate('PlanningDisplayScreen', {data: displayEvent});
else 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'); subtitle = i18n.t('homeScreen.dashboard.todayEventsSubtitleNA');
let displayEvent = this.getDisplayEvent(futureEvents); let displayEvent = this.getDisplayEvent(futureEvents);
const clickAction = this.clickAction.bind(this, isAvailable, displayEvent);
return ( return (
<DashboardItem <DashboardItem
subtitle={subtitle} subtitle={subtitle}
color={color} color={color}
icon={icon} icon={icon}
clickAction={this.clickAction.bind(this, isAvailable, displayEvent)} clickAction={clickAction}
title={title} title={title}
isAvailable={isAvailable} isAvailable={isAvailable}
displayEvent={displayEvent} 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 ( return (
section['id'] === SECTIONS_ID[0] ? this.getDashboardItem(item) : <Card style={{
<Card style={{ flex: 0,
flex: 0, marginLeft: 10,
marginLeft: 10, marginRight: 10,
marginRight: 10, borderRadius: CARD_BORDER_RADIUS,
borderRadius: CARD_BORDER_RADIUS, }}>
<CardItem style={{
backgroundColor: 'transparent'
}}> }}>
<CardItem style={{ <Left>
backgroundColor: 'transparent' <Thumbnail source={ICON_AMICALE} square/>
}}>
<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'
}}>
<Body> <Body>
{item.full_picture !== '' && item.full_picture !== undefined ? <Text>{NAME_AMICALE}</Text>
<TouchableOpacity onPress={openWebLink.bind(null, item.full_picture)} <Text note>{HomeScreen.getFormattedDate(item.created_time)}</Text>
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> </Body>
</CardItem> </Left>
<CardItem style={{ </CardItem>
backgroundColor: 'transparent' <CardItem style={{
}}> backgroundColor: 'transparent'
<Left> }}>
<Button transparent <Body>
onPress={openWebLink.bind(null, item.permalink_url)}> {item.full_picture !== '' && item.full_picture !== undefined ?
<CustomMaterialIcon <TouchableOpacity onPress={onImagePress}
icon="facebook" style={{width: '100%', height: 250, marginBottom: 5}}>
color="#57aeff" <Image source={{uri: item.full_picture}}
width={20}/> style={{flex: 1, resizeMode: "contain"}}
<Text>En savoir plus</Text> resizeMode="contain"
</Button> />
</Left> </TouchableOpacity>
</CardItem> : <View/>}
</Card> {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 = { type Props = {
navigation: Object navigation: Object,
route: Object,
} }
type State = { type State = {
@ -60,16 +61,8 @@ export default class ProximoListScreen extends React.Component<Props, State> {
modalRef: { current: null | Modalize }; modalRef: { current: null | Modalize };
originalData: Array<Object>; originalData: Array<Object>;
navData = this.props.navigation.getParam('data', []); shouldFocusSearchBar: boolean;
shouldFocusSearchBar = this.props.navigation.getParam('shouldFocusSearchBar', false);
state = {
currentlyDisplayedData: this.navData['data'].sort(sortPrice),
currentSortMode: sortMode.price,
isSortReversed: false,
sortPriceIcon: '',
sortNameIcon: '',
modalCurrentDisplayItem: {},
};
sortMenuRef: Menu; sortMenuRef: Menu;
onMenuRef: Function; onMenuRef: Function;
@ -82,7 +75,16 @@ export default class ProximoListScreen extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.modalRef = React.createRef(); 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.onMenuRef = this.onMenuRef.bind(this);
this.onSearchStringChange = this.onSearchStringChange.bind(this); this.onSearchStringChange = this.onSearchStringChange.bind(this);
@ -365,7 +367,6 @@ export default class ProximoListScreen extends React.Component<Props, State> {
shouldFocusSearchBar={this.shouldFocusSearchBar} shouldFocusSearchBar={this.shouldFocusSearchBar}
rightButton={this.getSortMenu()} rightButton={this.getSortMenu()}
/> />
<FlatList <FlatList
data={this.state.currentlyDisplayedData} data={this.state.currentlyDisplayedData}
extraData={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 {Body, Left, ListItem, Right, Text} from 'native-base';
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon"; import CustomMaterialIcon from "../../components/CustomMaterialIcon";
import FetchedDataSectionList from "../../components/FetchedDataSectionList";
import ThemeManager from "../../utils/ThemeManager"; import ThemeManager from "../../utils/ThemeManager";
import Touchable from "react-native-platform-touchable"; 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"; 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 * Class defining the main proximo screen. This screen shows the different categories of articles
* offered by proximo. * offered by proximo.
*/ */
export default class ProximoMainScreen extends FetchedDataSectionList { export default class ProximoMainScreen extends React.Component<Props, State> {
articles: Object;
onPressSearchBtn: Function; onPressSearchBtn: Function;
onPressAboutBtn: Function; onPressAboutBtn: Function;
getRenderItem: Function;
createDataset: Function;
constructor() { constructor() {
super(DATA_URL, 0); super();
this.onPressSearchBtn = this.onPressSearchBtn.bind(this); this.onPressSearchBtn = this.onPressSearchBtn.bind(this);
this.onPressAboutBtn = this.onPressAboutBtn.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) { static sortFinalData(a: Object, b: Object) {
@ -45,14 +59,6 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
return 0; return 0;
} }
getHeaderTranslation() {
return i18n.t("screens.proximo");
}
getUpdateToastTranslations() {
return [i18n.t("proximoScreen.listUpdated"), i18n.t("proximoScreen.listUpdateFail")];
}
getKeyExtractor(item: Object) { getKeyExtractor(item: Object) {
return item !== undefined ? item.type['id'] : undefined; return item !== undefined ? item.type['id'] : undefined;
} }
@ -62,7 +68,7 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
{ {
title: '', title: '',
data: this.generateData(fetchedData), data: this.generateData(fetchedData),
extraData: super.state, extraData: this.state,
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
} }
]; ];
@ -77,21 +83,22 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
*/ */
generateData(fetchedData: Object) { generateData(fetchedData: Object) {
let finalData = []; let finalData = [];
this.articles = undefined;
if (fetchedData.types !== undefined && fetchedData.articles !== undefined) { if (fetchedData.types !== undefined && fetchedData.articles !== undefined) {
let types = fetchedData.types; let types = fetchedData.types;
let articles = fetchedData.articles; this.articles = fetchedData.articles;
finalData.push({ finalData.push({
type: { type: {
id: -1, id: -1,
name: i18n.t('proximoScreen.all'), name: i18n.t('proximoScreen.all'),
icon: 'star' icon: 'star'
}, },
data: this.getAvailableArticles(articles, undefined) data: this.getAvailableArticles(this.articles, undefined)
}); });
for (let i = 0; i < types.length; i++) { for (let i = 0; i < types.length; i++) {
finalData.push({ finalData.push({
type: types[i], 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'), name: i18n.t('proximoScreen.all'),
icon: 'star' icon: 'star'
}, },
data: this.state.fetchedData.articles !== undefined ? data: this.articles !== undefined ?
this.getAvailableArticles(this.state.fetchedData.articles, undefined) : [] this.getAvailableArticles(this.articles, undefined) : []
}, },
}; };
this.props.navigation.navigate('ProximoListScreen', searchScreenData); 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 = { let dataToSend = {
shouldFocusSearchBar: false, shouldFocusSearchBar: false,
data: item, data: item,
@ -196,10 +203,26 @@ export default class ProximoMainScreen extends FetchedDataSectionList {
</Right> </Right>
</ListItem> </ListItem>
); );
} else { } else
return <View/>; 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 ThemeManager from '../../utils/ThemeManager';
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon"; import CustomMaterialIcon from "../../components/CustomMaterialIcon";
import FetchedDataSectionList from "../../components/FetchedDataSectionList"; import WebSectionList from "../../components/WebSectionList";
import NotificationsManager from "../../utils/NotificationsManager"; import NotificationsManager from "../../utils/NotificationsManager";
import PlatformTouchable from "react-native-platform-touchable"; import PlatformTouchable from "react-native-platform-touchable";
import Touchable from "react-native-platform-touchable"; import Touchable from "react-native-platform-touchable";
import AsyncStorageManager from "../../utils/AsyncStorageManager"; import AsyncStorageManager from "../../utils/AsyncStorageManager";
import * as Expo from "expo"; import * as Expo from "expo";
import BaseContainer from "../../components/BaseContainer";
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";
@ -30,19 +31,41 @@ let stateColors = {};
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds 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 * Class defining the app's proxiwash screen. This screen shows information about washing machines and
* dryers, taken from a scrapper reading proxiwash website * 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; 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 * Creates machine state parameters using current theme and translations
*/ */
constructor() { constructor() {
super(DATA_URL, REFRESH_TIME); super();
let colors = ThemeManager.getCurrentThemeVariables(); let colors = ThemeManager.getCurrentThemeVariables();
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor; stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor; stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor;
@ -69,23 +92,16 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
stateIcons[MACHINE_STATES.ERREUR] = 'alert'; stateIcons[MACHINE_STATES.ERREUR] = 'alert';
// let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current; // let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
this.state = { // this.setMinTimeRefresh(30);
refreshing: false,
firstLoading: true,
fetchedData: {},
// machinesWatched: JSON.parse(dataString),
machinesWatched: [],
};
this.setMinTimeRefresh(30);
this.onAboutPress = this.onAboutPress.bind(this); 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 * Setup notification channel for android and add listeners to detect notifications fired
*/ */
componentDidMount() { componentDidMount() {
super.componentDidMount();
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
// Get latest watchlist from server // Get latest watchlist from server
NotificationsManager.getMachineNotificationWatchlist((fetchedList) => { 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) { getDryersKeyExtractor(item: Object) {
return item !== undefined ? "dryer" + item.number : undefined; return item !== undefined ? "dryer" + item.number : undefined;
} }
@ -212,28 +220,23 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
createDataset(fetchedData: Object) { createDataset(fetchedData: Object) {
return [ 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'), title: i18n.t('proxiwashScreen.dryers'),
icon: 'tumble-dryer', icon: 'tumble-dryer',
data: fetchedData.dryers === undefined ? [] : fetchedData.dryers, data: fetchedData.dryers === undefined ? [] : fetchedData.dryers,
extraData: super.state, extraData: this.state,
keyExtractor: this.getDryersKeyExtractor 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 * 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 * 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 * @param section The object describing the current SectionList section
* @returns {React.Node} * @returns {React.Node}
*/ */
getRenderItem(item: Object, section: Object) { getRenderItem({item, section} : Object) {
let isMachineRunning = MACHINE_STATES[item.state] === MACHINE_STATES["EN COURS"]; 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 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'); 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 {Card, CardItem, H2, H3, Text} from 'native-base';
import ThemeManager from "../utils/ThemeManager"; import ThemeManager from "../utils/ThemeManager";
import i18n from "i18n-js"; 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"; 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. * Class defining the app's menu screen.
* This screen fetches data from etud to render the RU menu * 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 // Hard code strings as toLocaleDateString does not work on current android JS engine
daysOfWeek = []; daysOfWeek = [];
monthsOfYear = []; monthsOfYear = [];
getRenderItem: Function;
getRenderSectionHeader: Function;
createDataset: Function;
constructor() { constructor() {
super(DATA_URL, 0); super();
this.daysOfWeek.push(i18n.t("date.daysOfWeek.monday")); this.daysOfWeek.push(i18n.t("date.daysOfWeek.monday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday")); this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.wednesday")); 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.october"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.november")); this.monthsOfYear.push(i18n.t("date.monthsOfYear.november"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.december")); this.monthsOfYear.push(i18n.t("date.monthsOfYear.december"));
}
getHeaderTranslation() { this.getRenderItem = this.getRenderItem.bind(this);
return i18n.t("screens.menuSelf"); this.getRenderSectionHeader = this.getRenderSectionHeader.bind(this);
} this.createDataset = this.createDataset.bind(this);
getUpdateToastTranslations() {
return [i18n.t("homeScreen.listUpdated"), i18n.t("homeScreen.listUpdateFail")];
} }
getKeyExtractor(item: Object) { getKeyExtractor(item: Object) {
return item !== undefined ? item['name'] : undefined; return item !== undefined ? item['name'] : undefined;
} }
hasBackButton() {
return true;
}
hasStickyHeader(): boolean {
return true;
}
hasSideMenu(): boolean {
return false;
}
createDataset(fetchedData: Object) { createDataset(fetchedData: Object) {
let result = []; let result = [];
// Prevent crash by giving a default value when fetchedData is empty (not yet available) // 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(); return this.daysOfWeek[date.getDay() - 1] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
} }
getRenderSectionHeader(title: string) { getRenderSectionHeader({section}: Object) {
let title = "";
return ( return (
<Card style={{ <Card style={{
marginLeft: 10, marginLeft: 10,
@ -114,12 +108,12 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
textAlign: 'center', textAlign: 'center',
marginTop: 10, marginTop: 10,
marginBottom: 10 marginBottom: 10
}}>{title}</H2> }}>{section.title}</H2>
</Card> </Card>
); );
} }
getRenderItem(item: Object, section: Object) { getRenderItem({item}: Object) {
return ( return (
<Card style={{ <Card style={{
flex: 0, flex: 0,
@ -167,5 +161,24 @@ export default class SelfMenuScreen extends FetchedDataSectionList {
return name.charAt(0) + name.substr(1).toLowerCase(); 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 * Show a toast message depending on the validity of the fetched data
* *
* @param successString
* @param errorString * @param errorString
*/ */
showUpdateToast(successString, errorString) { showUpdateToast(errorString) {
let isSuccess = this.isDataObjectValid(); let isSuccess = this.isDataObjectValid();
if (!isSuccess) { if (!isSuccess) {
Toast.show({ Toast.show({
text: isSuccess ? successString : errorString, text: errorString,
buttonText: 'OK', buttonText: 'OK',
type: isSuccess ? "success" : "danger", type: isSuccess ? "success" : "danger",
duration: 2000 duration: 2000