update react native collapsible
This commit is contained in:
parent
0b4f115a14
commit
286c1e6411
28 changed files with 1147 additions and 1155 deletions
44
App.tsx
44
App.tsx
|
@ -37,6 +37,7 @@ import { setupStatusBar } from './src/utils/Utils';
|
|||
import initLocales from './src/utils/Locales';
|
||||
import { NavigationContainerRef } from '@react-navigation/core';
|
||||
import GENERAL_STYLES from './src/constants/Styles';
|
||||
import CollapsibleProvider from './src/components/providers/CollapsibleProvider';
|
||||
|
||||
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||
// Crashes app when navigating away from webview on android 9+
|
||||
|
@ -210,26 +211,29 @@ export default class App extends React.Component<{}, StateType> {
|
|||
}
|
||||
return (
|
||||
<PaperProvider theme={state.currentTheme}>
|
||||
<OverflowMenuProvider>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
|
||||
...GENERAL_STYLES.flex,
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={GENERAL_STYLES.flex}>
|
||||
<NavigationContainer
|
||||
theme={state.currentTheme}
|
||||
ref={this.navigatorRef}
|
||||
>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverflowMenuProvider>
|
||||
<CollapsibleProvider>
|
||||
<OverflowMenuProvider>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: ThemeManager.getCurrentTheme().colors
|
||||
.background,
|
||||
...GENERAL_STYLES.flex,
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={GENERAL_STYLES.flex}>
|
||||
<NavigationContainer
|
||||
theme={state.currentTheme}
|
||||
ref={this.navigatorRef}
|
||||
>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverflowMenuProvider>
|
||||
</CollapsibleProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
|
|
46
package-lock.json
generated
46
package-lock.json
generated
|
@ -3931,7 +3931,6 @@
|
|||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
|
||||
"integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-fetch": "2.6.1"
|
||||
}
|
||||
|
@ -10596,14 +10595,39 @@
|
|||
"integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg=="
|
||||
},
|
||||
"react-native-gesture-handler": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.8.0.tgz",
|
||||
"integrity": "sha512-E2FZa0qZ5Bi0Z8Jg4n9DaFomHvedSjwbO2DPmUUHYRy1lH2yxXUpSrqJd6yymu+Efzmjg2+JZzsjFYA2Iq8VEQ==",
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
|
||||
"integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==",
|
||||
"requires": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"fbjs": "^3.0.0",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"fbjs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz",
|
||||
"integrity": "sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.0.4",
|
||||
"fbjs-css-vars": "^1.0.0",
|
||||
"loose-envify": "^1.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"promise": "^7.1.1",
|
||||
"setimmediate": "^1.0.5",
|
||||
"ua-parser-js": "^0.7.18"
|
||||
}
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"requires": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-image-pan-zoom": {
|
||||
|
@ -10871,11 +10895,12 @@
|
|||
}
|
||||
},
|
||||
"react-navigation-collapsible": {
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.6.4.tgz",
|
||||
"integrity": "sha512-dXMbDw2TQ6s5XLk9h+2hUShXoS8KPChfdh/xmmLqfKmntS5YteE01+x78gU5KogB3etDraH1kvhW7xDnbG9AfA==",
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.9.1.tgz",
|
||||
"integrity": "sha512-yUwHe8Z7++A8ThrjPI+Mcm7LqBhIqJc+1F4XszpI7EoHz3bJElzczbfyfuEvjSbYU9AgW3MdBWzaRIDluxcEuA==",
|
||||
"requires": {
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
"react-native-iphone-x-helper": "^1.3.0",
|
||||
"shallowequal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-navigation-header-buttons": {
|
||||
|
@ -11453,6 +11478,11 @@
|
|||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"react-native-splash-screen": "3.2.0",
|
||||
"react-native-vector-icons": "8.1.0",
|
||||
"react-native-webview": "11.4.3",
|
||||
"react-navigation-collapsible": "5.6.4",
|
||||
"react-navigation-collapsible": "5.9.1",
|
||||
"react-navigation-header-buttons": "7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { FAB, IconButton, Surface, withTheme } from 'react-native-paper';
|
|||
import * as Animatable from 'react-native-animatable';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
|
@ -159,7 +159,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
|||
useNativeDriver
|
||||
style={{
|
||||
...styles.container,
|
||||
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT,
|
||||
bottom: 10 + TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<Surface style={styles.surface}>
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
import { FAB } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
|
||||
type PropsType = {
|
||||
icon: string;
|
||||
|
@ -82,7 +82,7 @@ export default class AnimatedFAB extends React.Component<PropsType> {
|
|||
useNativeDriver={true}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
bottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<FAB icon={props.icon} onPress={props.onPress} />
|
||||
|
|
|
@ -17,22 +17,27 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { useCollapsibleStack } from 'react-navigation-collapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCollapsibleHeader } from 'react-navigation-collapsible';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsible } from '../../utils/CollapsibleContext';
|
||||
import { useFocusEffect } from '@react-navigation/core';
|
||||
|
||||
export type CollapsibleComponentPropsType = {
|
||||
children?: React.ReactNode;
|
||||
hasTab?: boolean;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
paddedProps?: (paddingTop: number) => Record<string, any>;
|
||||
headerColors: string;
|
||||
};
|
||||
|
||||
type PropsType = CollapsibleComponentPropsType & {
|
||||
type Props = CollapsibleComponentPropsType & {
|
||||
component: React.ComponentType<any>;
|
||||
};
|
||||
|
||||
|
@ -42,22 +47,46 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
function CollapsibleComponent(props: PropsType) {
|
||||
function CollapsibleComponent(props: Props) {
|
||||
const { paddedProps, headerColors } = props;
|
||||
const Comp = props.component;
|
||||
const theme = useTheme();
|
||||
const { setCollapsible } = useCollapsible();
|
||||
|
||||
const collapsible = useCollapsibleHeader({
|
||||
config: {
|
||||
collapsedColor: headerColors ? headerColors : theme.colors.surface,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCollapsible(collapsible);
|
||||
}, [collapsible, setCollapsible])
|
||||
);
|
||||
|
||||
const {
|
||||
containerPaddingTop,
|
||||
scrollIndicatorInsetTop,
|
||||
onScrollWithListener,
|
||||
} = collapsible;
|
||||
|
||||
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
|
||||
|
||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (props.onScroll) {
|
||||
props.onScroll(event);
|
||||
}
|
||||
};
|
||||
const Comp = props.component;
|
||||
const {
|
||||
containerPaddingTop,
|
||||
scrollIndicatorInsetTop,
|
||||
onScrollWithListener,
|
||||
} = useCollapsibleStack();
|
||||
const paddingBottom = props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0;
|
||||
|
||||
const pprops =
|
||||
paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
{...props}
|
||||
{...pprops}
|
||||
onScroll={onScrollWithListener(onScroll)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: containerPaddingTop,
|
||||
|
|
|
@ -21,7 +21,7 @@ import * as React from 'react';
|
|||
import { useTheme } from 'react-native-paper';
|
||||
import { Modalize } from 'react-native-modalize';
|
||||
import { View } from 'react-native-animatable';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
|
||||
/**
|
||||
* Abstraction layer for Modalize component, using custom configuration
|
||||
|
@ -45,7 +45,7 @@ function CustomModal(props: {
|
|||
>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
paddingBottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -32,10 +32,10 @@ import { Collapsible } from 'react-navigation-collapsible';
|
|||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
import { ERROR_TYPE, readData } from '../../utils/WebData';
|
||||
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
export type SectionListDataType<ItemT> = Array<{
|
||||
title: string;
|
||||
|
@ -260,19 +260,20 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
dataset = props.createDataset(state.fetchedData, state.refreshing);
|
||||
}
|
||||
|
||||
const { containerPaddingTop } = props.collapsibleStack;
|
||||
return (
|
||||
<View>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleSectionList
|
||||
sections={dataset}
|
||||
extraData={props.updateData}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
progressViewOffset={containerPaddingTop}
|
||||
refreshing={state.refreshing}
|
||||
onRefresh={this.onRefresh}
|
||||
/>
|
||||
}
|
||||
paddedProps={(paddingTop) => ({
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
progressViewOffset={paddingTop}
|
||||
refreshing={state.refreshing}
|
||||
onRefresh={this.onRefresh}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
renderItem={this.getRenderItem}
|
||||
stickySectionHeadersEnabled={props.stickyHeader}
|
||||
|
@ -299,7 +300,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
: undefined
|
||||
}
|
||||
onScroll={this.onScroll}
|
||||
hasTab
|
||||
hasTab={true}
|
||||
/>
|
||||
<Snackbar
|
||||
visible={state.snackbarVisible}
|
||||
|
@ -310,7 +311,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
}}
|
||||
duration={4000}
|
||||
style={{
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
bottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{i18n.t('general.listUpdateFail')}
|
||||
|
@ -320,4 +321,4 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
}
|
||||
}
|
||||
|
||||
export default withCollapsible(WebSectionList);
|
||||
export default WebSectionList;
|
||||
|
|
|
@ -17,7 +17,13 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import WebView from 'react-native-webview';
|
||||
import {
|
||||
Divider,
|
||||
|
@ -34,23 +40,21 @@ import {
|
|||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { withTheme } from 'react-native-paper';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsibleHeader } from 'react-navigation-collapsible';
|
||||
import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
|
||||
import { ERROR_TYPE } from '../../utils/WebData';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/core';
|
||||
import { useCollapsible } from '../../utils/CollapsibleContext';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
type Props = {
|
||||
url: string;
|
||||
collapsibleStack: Collapsible;
|
||||
onMessage: (event: { nativeEvent: { data: string } }) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
customJS?: string;
|
||||
onMessage?: (event: { nativeEvent: { data: string } }) => void;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
initialJS?: string;
|
||||
injectJS?: string;
|
||||
customPaddingFunction?: null | ((padding: number) => string);
|
||||
showAdvancedControls?: boolean;
|
||||
};
|
||||
|
@ -66,134 +70,113 @@ const styles = StyleSheet.create({
|
|||
/**
|
||||
* Class defining a webview screen.
|
||||
*/
|
||||
class WebViewScreen extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
customJS: '',
|
||||
showAdvancedControls: true,
|
||||
customPaddingFunction: null,
|
||||
};
|
||||
function WebViewScreen(props: Props) {
|
||||
const [currentUrl, setCurrentUrl] = useState(props.url);
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const webviewRef = useRef<WebView>();
|
||||
|
||||
currentUrl: string;
|
||||
const { setCollapsible } = useCollapsible();
|
||||
const collapsible = useCollapsibleHeader({
|
||||
config: { collapsedColor: theme.colors.surface, useNativeDriver: false },
|
||||
});
|
||||
const { containerPaddingTop, onScrollWithListener } = collapsible;
|
||||
|
||||
webviewRef: { current: null | WebView };
|
||||
const [currentInjectedJS, setCurrentInjectedJS] = useState(props.injectJS);
|
||||
|
||||
canGoBack: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.webviewRef = React.createRef();
|
||||
this.canGoBack = false;
|
||||
this.currentUrl = props.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates header buttons and listens to events after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({
|
||||
headerRight: props.showAdvancedControls
|
||||
? this.getAdvancedButtons
|
||||
: this.getBasicButton,
|
||||
});
|
||||
props.navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCollapsible(collapsible);
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
props.navigation.addListener('blur', () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collapsible, setCollapsible])
|
||||
);
|
||||
|
||||
/**
|
||||
* Goes back on the webview or on the navigation stack if we cannot go back anymore
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
if (this.canGoBack) {
|
||||
this.onGoBackClicked();
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: props.showAdvancedControls
|
||||
? getAdvancedButtons
|
||||
: getBasicButton,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation, props.showAdvancedControls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.injectJS && props.injectJS !== currentInjectedJS) {
|
||||
injectJavaScript(props.injectJS);
|
||||
setCurrentInjectedJS(props.injectJS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.injectJS]);
|
||||
|
||||
const onBackButtonPressAndroid = () => {
|
||||
if (canGoBack) {
|
||||
onGoBackClicked();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets header refresh and open in browser buttons
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getBasicButton = () => {
|
||||
const getBasicButton = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
title={'refresh'}
|
||||
iconName={'refresh'}
|
||||
onPress={onRefreshClicked}
|
||||
/>
|
||||
<Item
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
iconName="open-in-new"
|
||||
onPress={this.onOpenClicked}
|
||||
iconName={'open-in-new'}
|
||||
onPress={onOpenClicked}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates advanced header control buttons.
|
||||
* These buttons allows the user to refresh, go back, go forward and open in the browser.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAdvancedButtons = () => {
|
||||
const { props } = this;
|
||||
const getAdvancedButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
/>
|
||||
<Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
|
||||
<OverflowMenu
|
||||
style={styles.overflow}
|
||||
OverflowIcon={
|
||||
<MaterialCommunityIcons
|
||||
name="dots-vertical"
|
||||
size={26}
|
||||
color={props.theme.colors.text}
|
||||
color={theme.colors.text}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goBack')}
|
||||
onPress={this.onGoBackClicked}
|
||||
onPress={onGoBackClicked}
|
||||
/>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goForward')}
|
||||
onPress={this.onGoForwardClicked}
|
||||
onPress={onGoForwardClicked}
|
||||
/>
|
||||
<Divider />
|
||||
<HiddenItem
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
onPress={this.onOpenClicked}
|
||||
onPress={onOpenClicked}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the loading indicator
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
|
||||
const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
|
||||
|
||||
/**
|
||||
* Gets the javascript needed to generate a padding on top of the page
|
||||
|
@ -202,91 +185,78 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
* @param padding The padding to add in pixels
|
||||
* @returns {string}
|
||||
*/
|
||||
getJavascriptPadding(padding: number): string {
|
||||
const { props } = this;
|
||||
const getJavascriptPadding = (padding: number) => {
|
||||
const customPadding =
|
||||
props.customPaddingFunction != null
|
||||
? props.customPaddingFunction(padding)
|
||||
: '';
|
||||
return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to use when refresh button is clicked. Reloads the webview.
|
||||
*/
|
||||
onRefreshClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.reload();
|
||||
const onRefreshClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.reload();
|
||||
}
|
||||
};
|
||||
|
||||
onGoBackClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goBack();
|
||||
const onGoBackClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
onGoForwardClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goForward();
|
||||
const onGoForwardClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.goForward();
|
||||
}
|
||||
};
|
||||
|
||||
onOpenClicked = () => {
|
||||
Linking.openURL(this.currentUrl);
|
||||
};
|
||||
const onOpenClicked = () => Linking.openURL(currentUrl);
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const { onScroll } = this.props;
|
||||
if (onScroll) {
|
||||
onScroll(event);
|
||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (props.onScroll) {
|
||||
props.onScroll(event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the given javascript string into the web page
|
||||
*
|
||||
* @param script The script to inject
|
||||
*/
|
||||
injectJavaScript = (script: string) => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.injectJavaScript(script);
|
||||
const injectJavaScript = (script: string) => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.injectJavaScript(script);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const {
|
||||
containerPaddingTop,
|
||||
onScrollWithListener,
|
||||
} = props.collapsibleStack;
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={this.webviewRef}
|
||||
source={{ uri: props.url }}
|
||||
startInLoadingState
|
||||
injectedJavaScript={props.customJS}
|
||||
javaScriptEnabled
|
||||
renderLoading={this.getRenderLoading}
|
||||
renderError={() => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefreshClicked}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={(navState) => {
|
||||
this.currentUrl = navState.url;
|
||||
this.canGoBack = navState.canGoBack;
|
||||
}}
|
||||
onMessage={props.onMessage}
|
||||
onLoad={() => {
|
||||
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
|
||||
}}
|
||||
// Animations
|
||||
onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={webviewRef}
|
||||
source={{ uri: props.url }}
|
||||
startInLoadingState={true}
|
||||
injectedJavaScript={props.initialJS}
|
||||
javaScriptEnabled={true}
|
||||
renderLoading={getRenderLoading}
|
||||
renderError={() => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={onRefreshClicked}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={(navState) => {
|
||||
setCurrentUrl(navState.url);
|
||||
setCanGoBack(navState.canGoBack);
|
||||
}}
|
||||
onMessage={props.onMessage}
|
||||
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
|
||||
// Animations
|
||||
onScroll={onScrollWithListener(onScroll)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withCollapsible(withTheme(WebViewScreen));
|
||||
export default WebViewScreen;
|
||||
|
|
|
@ -17,211 +17,88 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
import { Animated, StyleSheet } from 'react-native';
|
||||
import { withTheme } from 'react-native-paper';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
import TabIcon from './TabIcon';
|
||||
import TabHomeIcon from './TabHomeIcon';
|
||||
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
import { NavigationState } from '@react-navigation/native';
|
||||
import {
|
||||
PartialState,
|
||||
Route,
|
||||
} from '@react-navigation/routers/lib/typescript/src/types';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsible } from '../../utils/CollapsibleContext';
|
||||
|
||||
type RouteType = Route<string> & {
|
||||
state?: NavigationState | PartialState<NavigationState>;
|
||||
};
|
||||
export const TAB_BAR_HEIGHT = 50;
|
||||
|
||||
interface PropsType extends BottomTabBarProps {
|
||||
theme: ReactNativePaper.Theme;
|
||||
function CustomTabBar(
|
||||
props: BottomTabBarProps<any> & {
|
||||
icons: {
|
||||
[key: string]: {
|
||||
normal: string;
|
||||
focused: string;
|
||||
};
|
||||
};
|
||||
labels: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const state = props.state;
|
||||
const theme = useTheme();
|
||||
|
||||
const { collapsible } = useCollapsible();
|
||||
let translateY: number | Animated.AnimatedInterpolation = 0;
|
||||
if (collapsible) {
|
||||
translateY = Animated.multiply(-1.5, collapsible.translateY);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
...styles.bar,
|
||||
backgroundColor: theme.colors.surface,
|
||||
transform: [{ translateY: translateY }],
|
||||
}}
|
||||
>
|
||||
{state.routes.map(
|
||||
(
|
||||
route: {
|
||||
key: string;
|
||||
name: string;
|
||||
params?: object | undefined;
|
||||
},
|
||||
index: number
|
||||
) => {
|
||||
const iconData = props.icons[route.name];
|
||||
return (
|
||||
<TabIcon
|
||||
isMiddle={index === 2}
|
||||
onPress={() => props.navigation.navigate(route.name)}
|
||||
icon={iconData.normal}
|
||||
focusedIcon={iconData.focused}
|
||||
label={props.labels[route.name]}
|
||||
focused={state.index === index}
|
||||
key={route.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
type StateType = {
|
||||
translateY: any;
|
||||
};
|
||||
|
||||
type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
|
||||
|
||||
const TAB_ICONS = {
|
||||
proxiwash: 'tshirt-crew',
|
||||
services: 'account-circle',
|
||||
planning: 'calendar-range',
|
||||
planex: 'clock',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
bar: {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
height: 50,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
});
|
||||
|
||||
class CustomTabBar extends React.Component<PropsType, StateType> {
|
||||
static TAB_BAR_HEIGHT = 48;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
translateY: new Animated.Value(0),
|
||||
};
|
||||
// @ts-ignore
|
||||
props.navigation.addListener('state', this.onRouteChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the given route if it is different from the current one
|
||||
*
|
||||
* @param route Destination route
|
||||
* @param currentIndex The current route index
|
||||
* @param destIndex The destination route index
|
||||
*/
|
||||
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
|
||||
const { navigation } = this.props;
|
||||
if (currentIndex !== destIndex) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to tetris screen on home button long press
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
onItemLongPress(route: RouteType) {
|
||||
const { navigation } = this.props;
|
||||
if (route.name === 'home') {
|
||||
navigation.navigate('game-start');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the active route and syncs the tab bar animation with the header bar
|
||||
*/
|
||||
onRouteChange = () => {
|
||||
const { props } = this;
|
||||
props.state.routes.map(this.syncTabBar);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an icon for the given route if it is not the home one as it uses a custom button
|
||||
*
|
||||
* @param route
|
||||
* @param focused
|
||||
* @returns {null}
|
||||
*/
|
||||
getTabBarIcon = (route: RouteType, focused: boolean) => {
|
||||
let icon = TAB_ICONS[route.name as validRoutes];
|
||||
icon = focused ? icon : `${icon}-outline`;
|
||||
if (route.name !== 'home') {
|
||||
return icon;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a tab icon render.
|
||||
* If the given route is focused, it syncs the tab bar and header bar animations together
|
||||
*
|
||||
* @param route The route for the icon
|
||||
* @param index The index of the current route
|
||||
* @returns {*}
|
||||
*/
|
||||
getRenderIcon = (route: RouteType, index: number) => {
|
||||
const { props } = this;
|
||||
const { state } = props;
|
||||
const { options } = props.descriptors[route.key];
|
||||
let label;
|
||||
if (options.tabBarLabel != null) {
|
||||
label = options.tabBarLabel;
|
||||
} else if (options.title != null) {
|
||||
label = options.title;
|
||||
} else {
|
||||
label = route.name;
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
this.onItemPress(route, state.index, index);
|
||||
};
|
||||
const onLongPress = () => {
|
||||
this.onItemLongPress(route);
|
||||
};
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const color = isFocused
|
||||
? props.theme.colors.primary
|
||||
: props.theme.colors.tabIcon;
|
||||
if (route.name !== 'home') {
|
||||
return (
|
||||
<TabIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
icon={this.getTabBarIcon(route, isFocused)}
|
||||
color={color}
|
||||
label={label as string}
|
||||
focused={isFocused}
|
||||
extraData={state.index > index}
|
||||
key={route.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TabHomeIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
focused={isFocused}
|
||||
key={route.key}
|
||||
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getIcons() {
|
||||
const { props } = this;
|
||||
return props.state.routes.map(this.getRenderIcon);
|
||||
}
|
||||
|
||||
syncTabBar = (route: RouteType, index: number) => {
|
||||
const { state } = this.props;
|
||||
const isFocused = state.index === index;
|
||||
if (isFocused) {
|
||||
const stackState = route.state;
|
||||
const stackRoute =
|
||||
stackState && stackState.index != null
|
||||
? stackState.routes[stackState.index]
|
||||
: null;
|
||||
const params: { collapsible: Collapsible } | null | undefined = stackRoute
|
||||
? (stackRoute.params as { collapsible: Collapsible })
|
||||
: null;
|
||||
const collapsible = params != null ? params.collapsible : null;
|
||||
if (collapsible != null) {
|
||||
this.setState({
|
||||
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const icons = this.getIcons();
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
height: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
backgroundColor: props.theme.colors.surface,
|
||||
transform: [{ translateY: state.translateY }],
|
||||
...styles.container,
|
||||
}}
|
||||
>
|
||||
{icons}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
function areEqual(
|
||||
prevProps: BottomTabBarProps<any>,
|
||||
nextProps: BottomTabBarProps<any>
|
||||
) {
|
||||
return prevProps.state.index === nextProps.state.index;
|
||||
}
|
||||
|
||||
export default withTheme(CustomTabBar);
|
||||
export default React.memo(CustomTabBar, areEqual);
|
||||
|
|
|
@ -1,135 +1,114 @@
|
|||
/*
|
||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||
*
|
||||
* This file is part of Campus INSAT.
|
||||
*
|
||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Campus INSAT is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, Image } from 'react-native';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
const FOCUSED_ICON = require('../../../assets/tab-icon.png');
|
||||
const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
|
||||
|
||||
type PropsType = {
|
||||
interface Props {
|
||||
icon: string;
|
||||
focusedIcon: string;
|
||||
focused: boolean;
|
||||
onPress: () => void;
|
||||
onLongPress: () => void;
|
||||
tabBarHeight: number;
|
||||
};
|
||||
}
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
0.4: {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: -9,
|
||||
},
|
||||
0.6: {
|
||||
// @ts-ignore
|
||||
scale: 1.05,
|
||||
translateY: -6,
|
||||
},
|
||||
0.8: {
|
||||
// @ts-ignore
|
||||
scale: 1.15,
|
||||
translateY: -6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
outer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
subcontainer: {
|
||||
inner: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
marginBottom: -15,
|
||||
height: 60,
|
||||
},
|
||||
fab: {
|
||||
marginTop: 15,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabHomeIcon extends React.Component<PropsType> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.2,
|
||||
translateY: -9,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.1,
|
||||
translateY: -7,
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const FOCUSED_ICON = require('../../../assets/tab-icon.png');
|
||||
const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const { focused } = this.props;
|
||||
return nextProps.focused !== focused;
|
||||
}
|
||||
|
||||
getIconRender = ({ size, color }: { size: number; color: string }) => {
|
||||
const { focused } = this.props;
|
||||
function TabHomeIcon(props: Props) {
|
||||
const getImage = (iconProps: { size: number; color: string }) => {
|
||||
return (
|
||||
<Image
|
||||
source={focused ? FOCUSED_ICON : UNFOCUSED_ICON}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
tintColor: color,
|
||||
}}
|
||||
/>
|
||||
<Animatable.View useNativeDriver={true} animation={'rubberBand'}>
|
||||
<Image
|
||||
source={props.focused ? FOCUSED_ICON : UNFOCUSED_ICON}
|
||||
style={{
|
||||
width: iconProps.size,
|
||||
height: iconProps.size,
|
||||
tintColor: iconProps.color,
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
style={{
|
||||
height: props.tabBarHeight + 30,
|
||||
...styles.subcontainer,
|
||||
}}
|
||||
return (
|
||||
<View style={styles.outer}>
|
||||
<View style={styles.inner}>
|
||||
<Animatable.View
|
||||
style={styles.fab}
|
||||
useNativeDriver={true}
|
||||
duration={props.focused ? 500 : 200}
|
||||
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
|
||||
easing={'ease-out'}
|
||||
>
|
||||
<AnimatedFAB
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
|
||||
icon={this.getIconRender}
|
||||
<FAB
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
style={styles.fab}
|
||||
animated={false}
|
||||
icon={getImage}
|
||||
color={'#fff'}
|
||||
/>
|
||||
</View>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabHomeIcon;
|
||||
|
|
|
@ -1,143 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||
*
|
||||
* This file is part of Campus INSAT.
|
||||
*
|
||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Campus INSAT is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from 'react';
|
||||
import TabHomeIcon from './TabHomeIcon';
|
||||
import TabSideIcon from './TabSideIcon';
|
||||
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { TouchableRipple, withTheme } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
interface Props {
|
||||
isMiddle: boolean;
|
||||
focused: boolean;
|
||||
color: string;
|
||||
label: string;
|
||||
label: string | undefined;
|
||||
icon: string;
|
||||
focusedIcon: string;
|
||||
onPress: () => void;
|
||||
onLongPress: () => void;
|
||||
theme: ReactNativePaper.Theme;
|
||||
extraData: null | boolean | number | string;
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
borderRadius: 10,
|
||||
},
|
||||
text: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
fontSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabIcon extends React.Component<PropsType> {
|
||||
firstRender: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
focusIn: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.3,
|
||||
translateY: 7,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
},
|
||||
focusOut: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.firstRender = true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.firstRender = false;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const { props } = this;
|
||||
function TabIcon(props: Props) {
|
||||
if (props.isMiddle) {
|
||||
return (
|
||||
nextProps.focused !== props.focused ||
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.extraData !== props.extraData
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
<TabHomeIcon
|
||||
icon={props.icon}
|
||||
focusedIcon={props.focusedIcon}
|
||||
focused={props.focused}
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
rippleColor={props.theme.colors.primary}
|
||||
borderless={true}
|
||||
style={styles.container}
|
||||
>
|
||||
<View>
|
||||
<Animatable.View
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'focusIn' : 'focusOut'}
|
||||
useNativeDriver
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={props.icon}
|
||||
color={props.color}
|
||||
size={26}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Animatable.Text
|
||||
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
|
||||
useNativeDriver
|
||||
style={{
|
||||
color: props.color,
|
||||
...styles.text,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</Animatable.Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TabSideIcon
|
||||
focused={props.focused}
|
||||
label={props.label}
|
||||
icon={props.icon}
|
||||
focusedIcon={props.focusedIcon}
|
||||
onPress={props.onPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(TabIcon);
|
||||
function areEqual(prevProps: Props, nextProps: Props) {
|
||||
return prevProps.focused === nextProps.focused;
|
||||
}
|
||||
|
||||
export default React.memo(TabIcon, areEqual);
|
||||
|
|
113
src/components/Tabbar/TabSideIcon.tsx
Normal file
113
src/components/Tabbar/TabSideIcon.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import React from 'react';
|
||||
import { TouchableRipple, useTheme } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
interface Props {
|
||||
focused: boolean;
|
||||
label: string | undefined;
|
||||
icon: string;
|
||||
focusedIcon: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
focusIn: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
0.4: {
|
||||
// @ts-ignore
|
||||
scale: 1.3,
|
||||
translateY: 6,
|
||||
},
|
||||
0.6: {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: 6,
|
||||
},
|
||||
0.8: {
|
||||
// @ts-ignore
|
||||
scale: 1.25,
|
||||
translateY: 6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
},
|
||||
focusOut: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function TabSideIcon(props: Props) {
|
||||
const theme = useTheme();
|
||||
const color = props.focused ? theme.colors.primary : theme.colors.disabled;
|
||||
let icon = props.focused ? props.focusedIcon : props.icon;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
rippleColor={theme.colors.primary}
|
||||
style={{
|
||||
...styles.ripple,
|
||||
borderTopEndRadius: theme.roundness,
|
||||
borderTopStartRadius: theme.roundness,
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Animatable.View
|
||||
duration={props.focused ? 500 : 200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'focusIn' : 'focusOut'}
|
||||
useNativeDriver
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={color}
|
||||
size={26}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Animatable.Text
|
||||
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
|
||||
useNativeDriver
|
||||
style={{
|
||||
color: color,
|
||||
...styles.text,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</Animatable.Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ripple: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
...GENERAL_STYLES.centerHorizontal,
|
||||
fontSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default TabSideIcon;
|
33
src/components/providers/CollapsibleProvider.tsx
Normal file
33
src/components/providers/CollapsibleProvider.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
import {
|
||||
CollapsibleContext,
|
||||
CollapsibleContextType,
|
||||
} from '../../utils/CollapsibleContext';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactChild;
|
||||
};
|
||||
|
||||
export default function CollapsibleProvider(props: Props) {
|
||||
const setCollapsible = (collapsible: Collapsible) => {
|
||||
setCurrentCollapsible((prevState) => ({
|
||||
...prevState,
|
||||
collapsible,
|
||||
}));
|
||||
};
|
||||
|
||||
const [
|
||||
currentCollapsible,
|
||||
setCurrentCollapsible,
|
||||
] = useState<CollapsibleContextType>({
|
||||
collapsible: undefined,
|
||||
setCollapsible: setCollapsible,
|
||||
});
|
||||
|
||||
return (
|
||||
<CollapsibleContext.Provider value={currentCollapsible}>
|
||||
{props.children}
|
||||
</CollapsibleContext.Provider>
|
||||
);
|
||||
}
|
|
@ -18,12 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from '@react-navigation/stack';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
import { Platform } from 'react-native';
|
||||
import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
|
||||
import AboutScreen from '../screens/About/AboutScreen';
|
||||
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
||||
|
@ -40,10 +36,6 @@ import ProfileScreen from '../screens/Amicale/ProfileScreen';
|
|||
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
|
||||
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
|
||||
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||
import {
|
||||
CreateScreenCollapsibleStack,
|
||||
getWebsiteStack,
|
||||
} from '../utils/CollapsibleUtils';
|
||||
import BugReportScreen from '../screens/Other/FeedbackScreen';
|
||||
import WebsiteScreen from '../screens/Services/WebsiteScreen';
|
||||
import EquipmentScreen, {
|
||||
|
@ -54,6 +46,7 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir
|
|||
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
|
||||
import GameStartScreen from '../screens/Game/screens/GameStartScreen';
|
||||
import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
|
||||
import Test from '../screens/Test';
|
||||
|
||||
export enum MainRoutes {
|
||||
Main = 'main',
|
||||
|
@ -83,7 +76,7 @@ export enum MainRoutes {
|
|||
|
||||
type DefaultParams = { [key in MainRoutes]: object | undefined };
|
||||
|
||||
export interface FullParamsList extends DefaultParams {
|
||||
export type FullParamsList = DefaultParams & {
|
||||
'login': { nextScreen: string };
|
||||
'equipment-confirm': {
|
||||
item?: DeviceType;
|
||||
|
@ -91,34 +84,22 @@ export interface FullParamsList extends DefaultParams {
|
|||
};
|
||||
'equipment-rent': { item?: DeviceType };
|
||||
'gallery': { images: Array<{ url: string }> };
|
||||
}
|
||||
};
|
||||
|
||||
// Don't know why but TS is complaining without this
|
||||
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
|
||||
export type MainStackParamsList = FullParamsList &
|
||||
Record<string, object | undefined>;
|
||||
|
||||
const modalTransition =
|
||||
Platform.OS === 'ios'
|
||||
? TransitionPresets.ModalPresentationIOS
|
||||
: TransitionPresets.ModalTransition;
|
||||
|
||||
const defaultScreenOptions = {
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
};
|
||||
|
||||
const MainStack = createStackNavigator<MainStackParamsList>();
|
||||
|
||||
function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
|
||||
function MainStackComponent(props: {
|
||||
createTabNavigator: () => React.ReactElement;
|
||||
}) {
|
||||
const { createTabNavigator } = props;
|
||||
return (
|
||||
<MainStack.Navigator
|
||||
initialRouteName={MainRoutes.Main}
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
<MainStack.Navigator initialRouteName={MainRoutes.Main} headerMode="screen">
|
||||
<MainStack.Screen name={'test'} component={Test} />
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Main}
|
||||
component={createTabNavigator}
|
||||
|
@ -132,49 +113,53 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
|
|||
component={ImageGalleryScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
...modalTransition,
|
||||
}}
|
||||
/>
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Settings,
|
||||
MainStack,
|
||||
SettingsScreen,
|
||||
i18n.t('screens.settings.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.DashboardEdit,
|
||||
MainStack,
|
||||
DashboardEditScreen,
|
||||
i18n.t('screens.settings.dashboardEdit.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.About,
|
||||
MainStack,
|
||||
AboutScreen,
|
||||
i18n.t('screens.about.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Dependencies,
|
||||
MainStack,
|
||||
AboutDependenciesScreen,
|
||||
i18n.t('screens.about.libs')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Debug,
|
||||
MainStack,
|
||||
DebugScreen,
|
||||
i18n.t('screens.about.debug')
|
||||
)}
|
||||
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.GameStart,
|
||||
MainStack,
|
||||
GameStartScreen,
|
||||
i18n.t('screens.game.title'),
|
||||
true,
|
||||
undefined,
|
||||
'transparent'
|
||||
)}
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Settings}
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.settings.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.DashboardEdit}
|
||||
component={DashboardEditScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.settings.dashboardEdit.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.About}
|
||||
component={AboutScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.about.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Dependencies}
|
||||
component={AboutDependenciesScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.about.libs'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Debug}
|
||||
component={DebugScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.about.debug'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.GameStart}
|
||||
component={GameStartScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.game.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.GameMain}
|
||||
component={GameMainScreen}
|
||||
|
@ -182,102 +167,114 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
|
|||
title: i18n.t('screens.game.title'),
|
||||
}}
|
||||
/>
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Login,
|
||||
MainStack,
|
||||
LoginScreen,
|
||||
i18n.t('screens.login.title'),
|
||||
true,
|
||||
{ headerTintColor: '#fff' },
|
||||
'transparent'
|
||||
)}
|
||||
{getWebsiteStack('website', MainStack, WebsiteScreen, '')}
|
||||
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.SelfMenu,
|
||||
MainStack,
|
||||
SelfMenuScreen,
|
||||
i18n.t('screens.menu.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Proximo,
|
||||
MainStack,
|
||||
ProximoMainScreen,
|
||||
i18n.t('screens.proximo.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.ProximoList,
|
||||
MainStack,
|
||||
ProximoListScreen,
|
||||
i18n.t('screens.proximo.articleList')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.ProximoAbout,
|
||||
MainStack,
|
||||
ProximoAboutScreen,
|
||||
i18n.t('screens.proximo.title'),
|
||||
true,
|
||||
{ ...modalTransition }
|
||||
)}
|
||||
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Profile,
|
||||
MainStack,
|
||||
ProfileScreen,
|
||||
i18n.t('screens.profile.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.ClubList,
|
||||
MainStack,
|
||||
ClubListScreen,
|
||||
i18n.t('screens.clubs.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.ClubInformation,
|
||||
MainStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details'),
|
||||
true,
|
||||
{ ...modalTransition }
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.ClubAbout,
|
||||
MainStack,
|
||||
ClubAboutScreen,
|
||||
i18n.t('screens.clubs.title'),
|
||||
true,
|
||||
{ ...modalTransition }
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.EquipmentList,
|
||||
MainStack,
|
||||
EquipmentScreen,
|
||||
i18n.t('screens.equipment.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.EquipmentRent,
|
||||
MainStack,
|
||||
EquipmentLendScreen,
|
||||
i18n.t('screens.equipment.book')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.EquipmentConfirm,
|
||||
MainStack,
|
||||
EquipmentConfirmScreen,
|
||||
i18n.t('screens.equipment.confirm')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Vote,
|
||||
MainStack,
|
||||
VoteScreen,
|
||||
i18n.t('screens.vote.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
MainRoutes.Feedback,
|
||||
MainStack,
|
||||
BugReportScreen,
|
||||
i18n.t('screens.feedback.title')
|
||||
)}
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Login}
|
||||
component={LoginScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.login.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={'website'}
|
||||
component={WebsiteScreen}
|
||||
options={{
|
||||
title: '',
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.SelfMenu}
|
||||
component={SelfMenuScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.menu.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Proximo}
|
||||
component={ProximoMainScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.proximo.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ProximoList}
|
||||
component={ProximoListScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.proximo.articleList'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ProximoAbout}
|
||||
component={ProximoAboutScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.proximo.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Profile}
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.profile.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubList}
|
||||
component={ClubListScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubInformation}
|
||||
component={ClubDisplayScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.details'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubAbout}
|
||||
component={ClubAboutScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentList}
|
||||
component={EquipmentScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentRent}
|
||||
component={EquipmentLendScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.book'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentConfirm}
|
||||
component={EquipmentConfirmScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.confirm'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Vote}
|
||||
component={VoteScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.vote.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Feedback}
|
||||
component={BugReportScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.feedback.title'),
|
||||
}}
|
||||
/>
|
||||
</MainStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,16 +18,12 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from '@react-navigation/stack';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
|
||||
import { Title, useTheme } from 'react-native-paper';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { createCollapsibleStack } from 'react-navigation-collapsible';
|
||||
import { View } from 'react-native-animatable';
|
||||
import HomeScreen from '../screens/Home/HomeScreen';
|
||||
import PlanningScreen from '../screens/Planning/PlanningScreen';
|
||||
|
@ -44,23 +40,8 @@ import CustomTabBar from '../components/Tabbar/CustomTabBar';
|
|||
import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
|
||||
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
|
||||
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
|
||||
import {
|
||||
CreateScreenCollapsibleStack,
|
||||
getWebsiteStack,
|
||||
} from '../utils/CollapsibleUtils';
|
||||
import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
|
||||
|
||||
const modalTransition =
|
||||
Platform.OS === 'ios'
|
||||
? TransitionPresets.ModalPresentationIOS
|
||||
: TransitionPresets.ModalTransition;
|
||||
|
||||
const defaultScreenOptions = {
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...modalTransition,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
|
@ -79,29 +60,22 @@ const ServicesStack = createStackNavigator();
|
|||
|
||||
function ServicesStackComponent() {
|
||||
return (
|
||||
<ServicesStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{CreateScreenCollapsibleStack(
|
||||
'index',
|
||||
ServicesStack,
|
||||
WebsitesHomeScreen,
|
||||
i18n.t('screens.services.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'services-section',
|
||||
ServicesStack,
|
||||
ServicesSectionScreen,
|
||||
'SECTION'
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'amicale-contact',
|
||||
ServicesStack,
|
||||
AmicaleContactScreen,
|
||||
i18n.t('screens.amicaleAbout.title')
|
||||
)}
|
||||
<ServicesStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
|
||||
<ServicesStack.Screen
|
||||
name={'index'}
|
||||
component={WebsitesHomeScreen}
|
||||
options={{ title: i18n.t('screens.services.title') }}
|
||||
/>
|
||||
<ServicesStack.Screen
|
||||
name={'services-section'}
|
||||
component={ServicesSectionScreen}
|
||||
options={{ title: 'SECTION' }}
|
||||
/>
|
||||
<ServicesStack.Screen
|
||||
name={'amicale-contact'}
|
||||
component={AmicaleContactScreen}
|
||||
options={{ title: i18n.t('screens.amicaleAbout.title') }}
|
||||
/>
|
||||
</ServicesStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
@ -110,23 +84,17 @@ const ProxiwashStack = createStackNavigator();
|
|||
|
||||
function ProxiwashStackComponent() {
|
||||
return (
|
||||
<ProxiwashStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{CreateScreenCollapsibleStack(
|
||||
'index',
|
||||
ProxiwashStack,
|
||||
ProxiwashScreen,
|
||||
i18n.t('screens.proxiwash.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'proxiwash-about',
|
||||
ProxiwashStack,
|
||||
ProxiwashAboutScreen,
|
||||
i18n.t('screens.proxiwash.title')
|
||||
)}
|
||||
<ProxiwashStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
|
||||
<ProxiwashStack.Screen
|
||||
name={'index-contact'}
|
||||
component={ProxiwashScreen}
|
||||
options={{ title: i18n.t('screens.proxiwash.title') }}
|
||||
/>
|
||||
<ProxiwashStack.Screen
|
||||
name={'proxiwash-about'}
|
||||
component={ProxiwashAboutScreen}
|
||||
options={{ title: i18n.t('screens.proxiwash.title') }}
|
||||
/>
|
||||
</ProxiwashStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
@ -135,22 +103,17 @@ const PlanningStack = createStackNavigator();
|
|||
|
||||
function PlanningStackComponent() {
|
||||
return (
|
||||
<PlanningStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
<PlanningStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
|
||||
<PlanningStack.Screen
|
||||
name="index"
|
||||
name={'index'}
|
||||
component={PlanningScreen}
|
||||
options={{ title: i18n.t('screens.planning.title') }}
|
||||
/>
|
||||
{CreateScreenCollapsibleStack(
|
||||
'planning-information',
|
||||
PlanningStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails')
|
||||
)}
|
||||
<PlanningStack.Screen
|
||||
name={'planning-information'}
|
||||
component={PlanningDisplayScreen}
|
||||
options={{ title: i18n.t('screens.planning.eventDetails') }}
|
||||
/>
|
||||
</PlanningStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
@ -167,73 +130,63 @@ function HomeStackComponent(
|
|||
}
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<HomeStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{createCollapsibleStack(
|
||||
<HomeStack.Screen
|
||||
name="index"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.home.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
headerTitle: () => (
|
||||
<View style={styles.header}>
|
||||
<Mascot
|
||||
style={styles.mascot}
|
||||
emotion={MASCOT_STYLE.RANDOM}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: 'pulse',
|
||||
duration: 2000,
|
||||
iterationCount: 'infinite',
|
||||
}}
|
||||
/>
|
||||
<Title style={styles.title}>
|
||||
{i18n.t('screens.home.title')}
|
||||
</Title>
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
initialParams={params}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: colors.surface,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
)}
|
||||
<HomeStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
|
||||
<HomeStack.Screen
|
||||
name="scanner"
|
||||
name={'index'}
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.home.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
headerTitle: (headerProps) => (
|
||||
<View style={styles.header}>
|
||||
<Mascot
|
||||
style={styles.mascot}
|
||||
emotion={MASCOT_STYLE.RANDOM}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: 'pulse',
|
||||
duration: 2000,
|
||||
iterationCount: 'infinite',
|
||||
}}
|
||||
/>
|
||||
<Title style={styles.title}>{headerProps.children}</Title>
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
initialParams={params}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name={'scanner'}
|
||||
component={ScannerScreen}
|
||||
options={{ title: i18n.t('screens.scanner.title') }}
|
||||
/>
|
||||
|
||||
{CreateScreenCollapsibleStack(
|
||||
'club-information',
|
||||
HomeStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'feed-information',
|
||||
HomeStack,
|
||||
FeedItemScreen,
|
||||
i18n.t('screens.home.feed')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'planning-information',
|
||||
HomeStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails')
|
||||
)}
|
||||
<HomeStack.Screen
|
||||
name={'club-information'}
|
||||
component={ClubDisplayScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.details'),
|
||||
}}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name={'feed-information'}
|
||||
component={FeedItemScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.home.feed'),
|
||||
}}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name={'planning-information'}
|
||||
component={PlanningDisplayScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.planning.eventDetails'),
|
||||
}}
|
||||
/>
|
||||
</HomeStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
@ -242,23 +195,21 @@ const PlanexStack = createStackNavigator();
|
|||
|
||||
function PlanexStackComponent() {
|
||||
return (
|
||||
<PlanexStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{getWebsiteStack(
|
||||
'index',
|
||||
PlanexStack,
|
||||
PlanexScreen,
|
||||
i18n.t('screens.planex.title')
|
||||
)}
|
||||
{CreateScreenCollapsibleStack(
|
||||
'group-select',
|
||||
PlanexStack,
|
||||
GroupSelectionScreen,
|
||||
''
|
||||
)}
|
||||
<PlanexStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
|
||||
<PlanexStack.Screen
|
||||
name={'index'}
|
||||
component={PlanexScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.planex.title'),
|
||||
}}
|
||||
/>
|
||||
<PlanexStack.Screen
|
||||
name={'group-select'}
|
||||
component={GroupSelectionScreen}
|
||||
options={{
|
||||
title: '',
|
||||
}}
|
||||
/>
|
||||
</PlanexStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
@ -270,6 +221,34 @@ type PropsType = {
|
|||
defaultHomeData: { [key: string]: string };
|
||||
};
|
||||
|
||||
const ICONS: {
|
||||
[key: string]: {
|
||||
normal: string;
|
||||
focused: string;
|
||||
};
|
||||
} = {
|
||||
services: {
|
||||
normal: 'account-circle-outline',
|
||||
focused: 'account-circle',
|
||||
},
|
||||
proxiwash: {
|
||||
normal: 'tshirt-crew-outline',
|
||||
focused: 'tshirt-crew',
|
||||
},
|
||||
home: {
|
||||
normal: '',
|
||||
focused: '',
|
||||
},
|
||||
planning: {
|
||||
normal: 'calendar-range-outline',
|
||||
focused: 'calendar-range',
|
||||
},
|
||||
planex: {
|
||||
normal: 'clock-outline',
|
||||
focused: 'clock',
|
||||
},
|
||||
};
|
||||
|
||||
export default class TabNavigator extends React.Component<PropsType> {
|
||||
defaultRoute: string;
|
||||
createHomeStackComponent: () => any;
|
||||
|
@ -287,33 +266,44 @@ export default class TabNavigator extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const LABELS: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
services: i18n.t('screens.services.title'),
|
||||
proxiwash: i18n.t('screens.proxiwash.title'),
|
||||
home: i18n.t('screens.home.title'),
|
||||
planning: i18n.t('screens.planning.title'),
|
||||
planex: i18n.t('screens.planex.title'),
|
||||
};
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName={this.defaultRoute}
|
||||
tabBar={(tabProps) => <CustomTabBar {...tabProps} />}
|
||||
tabBar={(tabProps) => (
|
||||
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
|
||||
)}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="services"
|
||||
name={'services'}
|
||||
component={ServicesStackComponent}
|
||||
options={{ title: i18n.t('screens.services.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="proxiwash"
|
||||
name={'proxiwash'}
|
||||
component={ProxiwashStackComponent}
|
||||
options={{ title: i18n.t('screens.proxiwash.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="home"
|
||||
name={'home'}
|
||||
component={this.createHomeStackComponent}
|
||||
options={{ title: i18n.t('screens.home.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="planning"
|
||||
name={'planning'}
|
||||
component={PlanningStackComponent}
|
||||
options={{ title: i18n.t('screens.planning.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="planex"
|
||||
name={'planex'}
|
||||
component={PlanexStackComponent}
|
||||
options={{ title: i18n.t('screens.planex.title') }}
|
||||
/>
|
||||
|
|
|
@ -31,7 +31,9 @@ import i18n from 'i18n-js';
|
|||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../../components/Tabbar/CustomTabBar';
|
||||
import type { ClubCategoryType, ClubType } from './ClubListScreen';
|
||||
import { ERROR_TYPE } from '../../../utils/WebData';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
|
@ -174,7 +176,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20,
|
||||
marginBottom: TAB_BAR_HEIGHT + 20,
|
||||
...styles.card,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -441,7 +441,7 @@ class LoginScreen extends React.Component<Props, StateType> {
|
|||
enabled
|
||||
keyboardVerticalOffset={100}
|
||||
>
|
||||
<CollapsibleScrollView>
|
||||
<CollapsibleScrollView headerColors={'transparent'}>
|
||||
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
|
|
|
@ -25,7 +25,9 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../components/Tabbar/CustomTabBar';
|
||||
import type { FeedItemType } from './HomeScreen';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
|
||||
|
@ -117,9 +119,7 @@ class FeedItemScreen extends React.Component<PropsType> {
|
|||
style={styles.button}
|
||||
/>
|
||||
) : null}
|
||||
<Card.Content
|
||||
style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
|
||||
>
|
||||
<Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
|
||||
{this.displayData.message !== undefined ? (
|
||||
<Autolink
|
||||
text={this.displayData.message}
|
||||
|
|
|
@ -26,7 +26,9 @@ import i18n from 'i18n-js';
|
|||
import { PERMISSIONS, request, RESULTS } from 'react-native-permissions';
|
||||
import URLHandler from '../../utils/URLHandler';
|
||||
import AlertDialog from '../../components/Dialogs/AlertDialog';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../components/Tabbar/CustomTabBar';
|
||||
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
@ -218,7 +220,7 @@ class ScannerScreen extends React.Component<{}, StateType> {
|
|||
<View
|
||||
style={{
|
||||
...styles.container,
|
||||
marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
marginBottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
|
||||
|
|
|
@ -54,6 +54,7 @@ type StateType = {
|
|||
dialogTitle: string | React.ReactNode;
|
||||
dialogMessage: string;
|
||||
currentGroup: PlanexGroupType;
|
||||
injectJS: string;
|
||||
};
|
||||
|
||||
const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
|
||||
|
@ -153,20 +154,14 @@ const styles = StyleSheet.create({
|
|||
* This screen uses a webview to render the page
|
||||
*/
|
||||
class PlanexScreen extends React.Component<PropsType, StateType> {
|
||||
webScreenRef: { current: null | WebViewScreen };
|
||||
|
||||
barRef: { current: null | AnimatedBottomBar };
|
||||
|
||||
customInjectedJS: string;
|
||||
|
||||
/**
|
||||
* Defines custom injected JavaScript to improve the page display on mobile
|
||||
*/
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.webScreenRef = React.createRef();
|
||||
this.barRef = React.createRef();
|
||||
this.customInjectedJS = '';
|
||||
let currentGroupString = AsyncStorageManager.getString(
|
||||
AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
|
||||
);
|
||||
|
@ -184,8 +179,8 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
dialogTitle: '',
|
||||
dialogMessage: '',
|
||||
currentGroup,
|
||||
injectJS: '',
|
||||
};
|
||||
this.generateInjectedJS(currentGroup.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,20 +191,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
navigation.addListener('focus', this.onScreenFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only update the screen if the dark theme changed
|
||||
*
|
||||
* @param nextProps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const { props, state } = this;
|
||||
if (nextProps.theme.dark !== props.theme.dark) {
|
||||
this.generateInjectedJS(state.currentGroup.id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Webview, with an error view on top if no group is selected.
|
||||
*
|
||||
|
@ -218,6 +199,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
getWebView() {
|
||||
const { props, state } = this;
|
||||
const showWebview = state.currentGroup.id !== -1;
|
||||
console.log(state.injectJS);
|
||||
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
|
@ -230,10 +212,9 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
/>
|
||||
) : null}
|
||||
<WebViewScreen
|
||||
ref={this.webScreenRef}
|
||||
navigation={props.navigation}
|
||||
url={PLANEX_URL}
|
||||
customJS={this.customInjectedJS}
|
||||
initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
|
||||
injectJS={this.state.injectJS}
|
||||
onMessage={this.onMessage}
|
||||
onScroll={this.onScroll}
|
||||
showAdvancedControls={false}
|
||||
|
@ -269,9 +250,13 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
} else {
|
||||
command = `$('#calendar').fullCalendar('${action}', '${data}')`;
|
||||
}
|
||||
if (this.webScreenRef.current != null) {
|
||||
this.webScreenRef.current.injectJavaScript(`${command};true;`);
|
||||
} // Injected javascript must end with true
|
||||
// String must resolve to true to prevent crash on iOS
|
||||
command += ';true;';
|
||||
// Change the injected
|
||||
if (command === this.state.injectJS) {
|
||||
command += ';true;';
|
||||
}
|
||||
this.setState({ injectJS: command });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -373,7 +358,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
group
|
||||
);
|
||||
navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
|
||||
this.generateInjectedJS(group.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -382,16 +366,20 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
* @param groupID The current group selected
|
||||
*/
|
||||
generateInjectedJS(groupID: number) {
|
||||
this.customInjectedJS = `$(document).ready(function() {${OBSERVE_MUTATIONS_INJECTED}${FULL_CALENDAR_SETTINGS}displayAde(${groupID});${
|
||||
// Reset Ade
|
||||
DateManager.isWeekend(new Date()) ? 'calendar.next()' : ''
|
||||
}${INJECT_STYLE}`;
|
||||
|
||||
let customInjectedJS = `$(document).ready(function() {
|
||||
${OBSERVE_MUTATIONS_INJECTED}
|
||||
${FULL_CALENDAR_SETTINGS}
|
||||
displayAde(${groupID});
|
||||
${INJECT_STYLE}`;
|
||||
if (DateManager.isWeekend(new Date())) {
|
||||
customInjectedJS += `calendar.next();`;
|
||||
}
|
||||
if (ThemeManager.getNightMode()) {
|
||||
this.customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
|
||||
customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
|
||||
}
|
||||
|
||||
this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
|
||||
customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
|
||||
return customInjectedJS;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -28,7 +28,9 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
|||
import { apiRequest, ERROR_TYPE } from '../../utils/WebData';
|
||||
import ErrorView from '../../components/Screens/ErrorView';
|
||||
import CustomHTML from '../../components/Overrides/CustomHTML';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../components/Tabbar/CustomTabBar';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
import type { PlanningEventType } from '../../utils/Planning';
|
||||
import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
|
||||
|
@ -145,9 +147,7 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
|||
) : null}
|
||||
|
||||
{displayData.description !== null ? (
|
||||
<Card.Content
|
||||
style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
|
||||
>
|
||||
<Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
|
||||
<CustomHTML html={displayData.description} />
|
||||
</Card.Content>
|
||||
) : (
|
||||
|
|
|
@ -21,7 +21,9 @@ import * as React from 'react';
|
|||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { Card, Avatar, Paragraph, Text } from 'react-native-paper';
|
||||
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../../components/Tabbar/CustomTabBar';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
|
||||
|
@ -72,7 +74,7 @@ export default function ProximoAboutScreen() {
|
|||
<Card
|
||||
style={{
|
||||
...styles.card,
|
||||
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20,
|
||||
marginBottom: TAB_BAR_HEIGHT + 20,
|
||||
}}
|
||||
>
|
||||
<Card.Title
|
||||
|
|
|
@ -23,11 +23,15 @@ import WebViewScreen from '../../components/Screens/WebViewScreen';
|
|||
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
|
||||
type PropsType = {
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: { params: { host: string; path: string | null; title: string } };
|
||||
};
|
||||
|
||||
type State = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
const ENABLE_MOBILE_STRING =
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
|
||||
|
@ -43,18 +47,18 @@ const BIB_BACK_BUTTON =
|
|||
'</a>' +
|
||||
'</div>';
|
||||
|
||||
class WebsiteScreen extends React.Component<PropsType> {
|
||||
fullUrl: string;
|
||||
|
||||
class WebsiteScreen extends React.Component<Props, State> {
|
||||
injectedJS: { [key: string]: string };
|
||||
|
||||
customPaddingFunctions: { [key: string]: (padding: string) => string };
|
||||
customPaddingFunctions: { [key: string]: (padding: number) => string };
|
||||
|
||||
host: string;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.fullUrl = '';
|
||||
this.state = {
|
||||
url: '',
|
||||
};
|
||||
this.host = '';
|
||||
props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.injectedJS = {};
|
||||
|
@ -70,7 +74,7 @@ class WebsiteScreen extends React.Component<PropsType> {
|
|||
`$(".hero-unit-form").append("${BIB_BACK_BUTTON}");true;`;
|
||||
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = (
|
||||
padding: string
|
||||
padding: number
|
||||
): string => {
|
||||
return (
|
||||
`$('head').append('${ENABLE_MOBILE_STRING}');` +
|
||||
|
@ -79,7 +83,7 @@ class WebsiteScreen extends React.Component<PropsType> {
|
|||
);
|
||||
};
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = (
|
||||
padding: string
|
||||
padding: number
|
||||
): string => {
|
||||
return (
|
||||
`$('#p-logo-text').css('top', 10 + ${padding});` +
|
||||
|
@ -99,16 +103,20 @@ class WebsiteScreen extends React.Component<PropsType> {
|
|||
*/
|
||||
handleNavigationParams() {
|
||||
const { route, navigation } = this.props;
|
||||
|
||||
if (route.params != null) {
|
||||
console.log(route.params);
|
||||
this.host = route.params.host;
|
||||
let { path } = route.params;
|
||||
const { title } = route.params;
|
||||
let fullUrl = '';
|
||||
if (this.host != null && path != null) {
|
||||
path = path.replace(this.host, '');
|
||||
this.fullUrl = this.host + path;
|
||||
fullUrl = this.host + path;
|
||||
} else {
|
||||
this.fullUrl = this.host;
|
||||
fullUrl = this.host;
|
||||
}
|
||||
this.setState({ url: fullUrl });
|
||||
|
||||
if (title != null) {
|
||||
navigation.setOptions({ title });
|
||||
|
@ -117,7 +125,6 @@ class WebsiteScreen extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
let injectedJavascript = '';
|
||||
let customPadding = null;
|
||||
if (this.host != null && this.injectedJS[this.host] != null) {
|
||||
|
@ -127,12 +134,11 @@ class WebsiteScreen extends React.Component<PropsType> {
|
|||
customPadding = this.customPaddingFunctions[this.host];
|
||||
}
|
||||
|
||||
if (this.fullUrl != null) {
|
||||
if (this.state.url) {
|
||||
return (
|
||||
<WebViewScreen
|
||||
navigation={navigation}
|
||||
url={this.fullUrl}
|
||||
customJS={injectedJavascript}
|
||||
url={this.state.url}
|
||||
initialJS={injectedJavascript}
|
||||
customPaddingFunction={customPadding}
|
||||
/>
|
||||
);
|
||||
|
|
157
src/screens/Test.tsx
Normal file
157
src/screens/Test.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { useNavigation } from '@react-navigation/core';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import React from 'react';
|
||||
import { Animated, View } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import {
|
||||
Collapsible,
|
||||
useCollapsibleHeader,
|
||||
} from 'react-navigation-collapsible';
|
||||
import CollapsibleFlatList from '../components/Collapsible/CollapsibleFlatList';
|
||||
import FeedItem from '../components/Home/FeedItem';
|
||||
import WebSectionList from '../components/Screens/WebSectionList';
|
||||
import withCollapsible from '../utils/withCollapsible';
|
||||
import { FeedItemType } from './Home/HomeScreen';
|
||||
import i18n from 'i18n-js';
|
||||
import CollapsibleSectionList from '../components/Collapsible/CollapsibleSectionList';
|
||||
|
||||
// export default function Test() {
|
||||
// const {
|
||||
// onScroll /* Event handler */,
|
||||
// onScrollWithListener /* Event handler creator */,
|
||||
// containerPaddingTop /* number */,
|
||||
// scrollIndicatorInsetTop /* number */,
|
||||
// /* Animated.AnimatedValue contentOffset from scrolling */
|
||||
// positionY /* 0.0 ~ length of scrollable component (contentOffset)
|
||||
// /* Animated.AnimatedInterpolation by scrolling */,
|
||||
// translateY /* 0.0 ~ -headerHeight */,
|
||||
// progress /* 0.0 ~ 1.0 */,
|
||||
// opacity /* 1.0 ~ 0.0 */,
|
||||
// } = useCollapsibleHeader();
|
||||
|
||||
// const renderItem = () => {
|
||||
// return (
|
||||
// <View
|
||||
// style={{
|
||||
// marginTop: 50,
|
||||
// marginBottom: 50,
|
||||
// }}
|
||||
// >
|
||||
// <Text>TEST</Text>
|
||||
// </View>
|
||||
// );
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Animated.FlatList
|
||||
// onScroll={onScroll}
|
||||
// contentContainerStyle={{ paddingTop: containerPaddingTop }}
|
||||
// scrollIndicatorInsets={{ top: scrollIndicatorInsetTop }}
|
||||
// data={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
|
||||
// renderItem={renderItem}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
collapsibleStack: Collapsible;
|
||||
};
|
||||
|
||||
const DATA_URL =
|
||||
'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json';
|
||||
const FEED_ITEM_HEIGHT = 500;
|
||||
|
||||
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
|
||||
class Test extends React.Component<Props> {
|
||||
createDataset = (): Array<{
|
||||
title: string;
|
||||
data: [] | Array<FeedItemType>;
|
||||
id: string;
|
||||
}> => {
|
||||
return [
|
||||
{
|
||||
title: 'title',
|
||||
data: [
|
||||
{
|
||||
id: '0',
|
||||
message: 'message',
|
||||
image: '',
|
||||
link: '',
|
||||
page_id: 'amicale.deseleves',
|
||||
time: 0,
|
||||
url: '',
|
||||
video: '',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
message: 'message',
|
||||
image: '',
|
||||
link: '',
|
||||
page_id: 'amicale.deseleves',
|
||||
time: 0,
|
||||
url: '',
|
||||
video: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
message: 'message',
|
||||
image: '',
|
||||
link: '',
|
||||
page_id: 'amicale.deseleves',
|
||||
time: 0,
|
||||
url: '',
|
||||
video: '',
|
||||
},
|
||||
],
|
||||
id: '0',
|
||||
},
|
||||
];
|
||||
};
|
||||
getRenderItem = ({ item }: { item: FeedItemType }) => (
|
||||
<FeedItem item={item} height={FEED_ITEM_HEIGHT} />
|
||||
);
|
||||
|
||||
render() {
|
||||
const renderItem = () => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 50,
|
||||
marginBottom: 50,
|
||||
}}
|
||||
>
|
||||
<Text>TEST</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const props = this.props;
|
||||
// return (
|
||||
// <CollapsibleFlatList
|
||||
// data={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
|
||||
// renderItem={renderItem}
|
||||
// />
|
||||
// );
|
||||
// return (
|
||||
// <CollapsibleSectionList
|
||||
// sections={[{ title: '', data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }]}
|
||||
// renderItem={renderItem}
|
||||
// />
|
||||
// );
|
||||
return (
|
||||
<WebSectionList
|
||||
navigation={props.navigation}
|
||||
createDataset={this.createDataset}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus
|
||||
fetchUrl={DATA_URL}
|
||||
renderItem={this.getRenderItem}
|
||||
itemHeight={FEED_ITEM_HEIGHT}
|
||||
showError={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Test;
|
16
src/utils/CollapsibleContext.ts
Normal file
16
src/utils/CollapsibleContext.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
|
||||
export type CollapsibleContextType = {
|
||||
collapsible?: Collapsible;
|
||||
setCollapsible: (collapsible: Collapsible) => void;
|
||||
};
|
||||
|
||||
export const CollapsibleContext = React.createContext<CollapsibleContextType>({
|
||||
collapsible: undefined,
|
||||
setCollapsible: () => undefined,
|
||||
});
|
||||
|
||||
export function useCollapsible() {
|
||||
return useContext(CollapsibleContext);
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||
*
|
||||
* This file is part of Campus INSAT.
|
||||
*
|
||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Campus INSAT is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { createCollapsibleStack } from 'react-navigation-collapsible';
|
||||
import StackNavigator, {
|
||||
StackNavigationOptions,
|
||||
} from '@react-navigation/stack';
|
||||
import { StackNavigationState, TypedNavigator } from '@react-navigation/native';
|
||||
import { StackNavigationEventMap } from '@react-navigation/stack/lib/typescript/src/types';
|
||||
|
||||
type StackNavigatorType = import('@react-navigation/native').TypedNavigator<
|
||||
Record<string, object | undefined>,
|
||||
StackNavigationState<any>,
|
||||
StackNavigationOptions,
|
||||
StackNavigationEventMap,
|
||||
typeof StackNavigator
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
|
||||
*
|
||||
* Please use the getWebsiteStack function if your screen uses a webview as their main component as it needs special parameters.
|
||||
*
|
||||
* @param name The screen name in the navigation stack
|
||||
* @param Stack The stack component
|
||||
* @param component The screen component
|
||||
* @param title The screen title shown in the header (needs to be translated)
|
||||
* @param useNativeDriver Whether to use the native driver for animations.
|
||||
* Set to false if the screen uses a webview as this component does not support native driver.
|
||||
* In all other cases, set it to true for increase performance.
|
||||
* @param options Screen options to use, or null if no options are necessary.
|
||||
* @param headerColor The color of the header. Uses default color if not specified
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function CreateScreenCollapsibleStack(
|
||||
name: string,
|
||||
Stack: TypedNavigator<any, any, any, any, any>,
|
||||
component: React.ComponentType<any>,
|
||||
title: string,
|
||||
useNativeDriver: boolean = true,
|
||||
options: StackNavigationOptions = {},
|
||||
headerColor?: string
|
||||
) {
|
||||
const { colors } = useTheme();
|
||||
return createCollapsibleStack(
|
||||
<Stack.Screen
|
||||
name={name}
|
||||
component={component}
|
||||
options={{
|
||||
title,
|
||||
headerStyle: {
|
||||
backgroundColor: headerColor != null ? headerColor : colors.surface,
|
||||
},
|
||||
...options,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: headerColor != null ? headerColor : colors.surface,
|
||||
useNativeDriver: useNativeDriver, // native driver does not work with webview
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
|
||||
*
|
||||
* This is a preset for screens using a webview as their main component, as it uses special parameters to work.
|
||||
* (aka a dirty workaround)
|
||||
*
|
||||
* @param name
|
||||
* @param Stack
|
||||
* @param component
|
||||
* @param title
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function getWebsiteStack(
|
||||
name: string,
|
||||
Stack: TypedNavigator<any, any, any, any, any>,
|
||||
component: React.ComponentType<any>,
|
||||
title: string
|
||||
) {
|
||||
return CreateScreenCollapsibleStack(name, Stack, component, title, false);
|
||||
}
|
|
@ -18,29 +18,28 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { useCollapsibleStack } from 'react-navigation-collapsible';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import {
|
||||
useCollapsibleHeader,
|
||||
UseCollapsibleOptions,
|
||||
} from 'react-navigation-collapsible';
|
||||
|
||||
/**
|
||||
* Function used to manipulate Collapsible Hooks from a class.
|
||||
*
|
||||
* Usage :
|
||||
*
|
||||
* export withCollapsible(Component)
|
||||
*
|
||||
* replacing Component with the one you want to use.
|
||||
* This component will then receive the collapsibleStack prop.
|
||||
*
|
||||
* @param Component The component to use Collapsible with
|
||||
* @returns {React.ComponentType<any>}
|
||||
*/
|
||||
export default function withCollapsible(Component: React.ComponentType<any>) {
|
||||
return React.forwardRef((props: any, ref: any) => {
|
||||
return (
|
||||
<Component
|
||||
collapsibleStack={useCollapsibleStack()}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export default function withCollapsible<T>(
|
||||
Component: React.ComponentType<any>,
|
||||
options?: UseCollapsibleOptions
|
||||
) {
|
||||
return function WrappedComponent(props: T) {
|
||||
const theme = useTheme();
|
||||
if (!options?.config?.collapsedColor) {
|
||||
options = {
|
||||
...options,
|
||||
config: {
|
||||
...options?.config,
|
||||
collapsedColor: theme.colors.surface,
|
||||
},
|
||||
};
|
||||
}
|
||||
const collapsible = useCollapsibleHeader(options);
|
||||
return <Component {...props} collapsible={collapsible} />;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue