diff options
Diffstat (limited to 'src/view/shell/mobile/index.tsx')
-rw-r--r-- | src/view/shell/mobile/index.tsx | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx new file mode 100644 index 000000000..7b0098c51 --- /dev/null +++ b/src/view/shell/mobile/index.tsx @@ -0,0 +1,235 @@ +import React, {useRef} from 'react' +import {observer} from 'mobx-react-lite' +import { + GestureResponderEvent, + SafeAreaView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native' +import {ScreenContainer, Screen} from 'react-native-screens' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {useStores} from '../../../state' +import {NavigationModel} from '../../../state/models/navigation' +import {match, MatchResult} from '../../routes' +import {TabsSelectorModal} from './tabs-selector' +import {createBackMenu, createForwardMenu} from './history-menu' + +const Location = ({icon, title}: {icon: IconProp; title?: string}) => { + return ( + <TouchableOpacity style={styles.location}> + {title ? ( + <FontAwesomeIcon size={16} style={styles.locationIcon} icon={icon} /> + ) : ( + <FontAwesomeIcon + size={16} + style={styles.locationIconLight} + icon="magnifying-glass" + /> + )} + <Text style={title ? styles.locationText : styles.locationTextLight}> + {title || 'Search'} + </Text> + </TouchableOpacity> + ) +} + +const Btn = ({ + icon, + inactive, + onPress, + onLongPress, +}: { + icon: IconProp + inactive?: boolean + onPress?: (event: GestureResponderEvent) => void + onLongPress?: (event: GestureResponderEvent) => void +}) => { + if (inactive) { + return ( + <View style={styles.ctrl}> + <FontAwesomeIcon + size={18} + style={[styles.ctrlIcon, styles.inactive]} + icon={icon} + /> + </View> + ) + } + return ( + <TouchableOpacity + style={styles.ctrl} + onPress={onPress} + onLongPress={onLongPress}> + <FontAwesomeIcon size={18} style={styles.ctrlIcon} icon={icon} /> + </TouchableOpacity> + ) +} + +export const MobileShell: React.FC = observer(() => { + const stores = useStores() + const tabSelectorRef = useRef<{open: () => void}>() + const screenRenderDesc = constructScreenRenderDesc(stores.nav) + + const onPressBack = () => stores.nav.tab.goBack() + const onPressForward = () => stores.nav.tab.goForward() + const onPressHome = () => stores.nav.navigate('/') + const onPressNotifications = () => stores.nav.navigate('/notifications') + const onPressTabs = () => tabSelectorRef.current?.open() + + const onLongPressBack = () => createBackMenu(stores.nav.tab) + const onLongPressForward = () => createForwardMenu(stores.nav.tab) + + const onNewTab = () => stores.nav.newTab('/') + const onChangeTab = (tabIndex: number) => stores.nav.setActiveTab(tabIndex) + const onCloseTab = (tabIndex: number) => stores.nav.closeTab(tabIndex) + + return ( + <View style={styles.outerContainer}> + <View style={styles.topBar}> + <Location + icon={screenRenderDesc.icon} + title={stores.nav.tab.current.title} + /> + </View> + <SafeAreaView style={styles.innerContainer}> + <ScreenContainer> + {screenRenderDesc.screens.map(({Com, params, key, activityState}) => ( + <Screen + key={key} + style={{backgroundColor: '#fff'}} + activityState={activityState}> + <Com params={params} /> + </Screen> + ))} + </ScreenContainer> + </SafeAreaView> + <View style={styles.bottomBar}> + <Btn + icon="angle-left" + inactive={!stores.nav.tab.canGoBack} + onPress={onPressBack} + onLongPress={onLongPressBack} + /> + <Btn + icon="angle-right" + inactive={!stores.nav.tab.canGoForward} + onPress={onPressForward} + onLongPress={onLongPressForward} + /> + <Btn icon="house" onPress={onPressHome} /> + <Btn icon={['far', 'bell']} onPress={onPressNotifications} /> + <Btn icon={['far', 'clone']} onPress={onPressTabs} /> + </View> + <TabsSelectorModal + ref={tabSelectorRef} + tabs={stores.nav.tabs} + currentTabIndex={stores.nav.tabIndex} + onNewTab={onNewTab} + onChangeTab={onChangeTab} + onCloseTab={onCloseTab} + /> + </View> + ) +}) + +/** + * This method produces the information needed by the shell to + * render the current screens with screen-caching behaviors. + */ +type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2} +function constructScreenRenderDesc(nav: NavigationModel): { + icon: IconProp + screens: ScreenRenderDesc[] +} { + let icon: IconProp = 'magnifying-glass' + let screens: ScreenRenderDesc[] = [] + for (const tab of nav.tabs) { + const tabScreens = [ + ...tab.getBackList(5), + Object.assign({}, tab.current, {index: tab.index}), + ] + const parsedTabScreens = tabScreens.map(screen => { + const isCurrent = nav.isCurrentScreen(tab.id, screen.index) + const matchRes = match(screen.url) + if (isCurrent) { + icon = matchRes.icon + } + return Object.assign(matchRes, { + key: `t${tab.id}-s${screen.index}`, + activityState: isCurrent ? 2 : 0, + }) + }) + screens = screens.concat(parsedTabScreens) + } + return { + icon, + screens, + } +} + +const styles = StyleSheet.create({ + outerContainer: { + height: '100%', + }, + innerContainer: { + flex: 1, + }, + topBar: { + flexDirection: 'row', + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#ccc', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 40, + paddingBottom: 5, + }, + location: { + flex: 1, + flexDirection: 'row', + borderRadius: 4, + paddingLeft: 10, + paddingRight: 6, + paddingTop: 6, + paddingBottom: 6, + backgroundColor: '#F8F3F3', + }, + locationIcon: { + color: '#DB00FF', + marginRight: 8, + }, + locationIconLight: { + color: '#909090', + marginRight: 8, + }, + locationText: { + color: '#000', + }, + locationTextLight: { + color: '#868788', + }, + bottomBar: { + flexDirection: 'row', + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#ccc', + paddingLeft: 5, + paddingRight: 15, + paddingBottom: 20, + }, + ctrl: { + flex: 1, + paddingTop: 15, + paddingBottom: 15, + }, + ctrlIcon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + inactive: { + color: '#888', + }, +}) |