diff options
Diffstat (limited to 'src/view/shell/mobile')
-rw-r--r-- | src/view/shell/mobile/history-menu.tsx | 99 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 235 | ||||
-rw-r--r-- | src/view/shell/mobile/tabs-selector.tsx | 158 |
3 files changed, 492 insertions, 0 deletions
diff --git a/src/view/shell/mobile/history-menu.tsx b/src/view/shell/mobile/history-menu.tsx new file mode 100644 index 000000000..b625162d4 --- /dev/null +++ b/src/view/shell/mobile/history-menu.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import { + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native' +import RootSiblings from 'react-native-root-siblings' +import {NavigationTabModel} from '../../../state/models/navigation' + +export function createBackMenu(tab: NavigationTabModel): RootSiblings { + const onPressItem = (index: number) => { + sibling.destroy() + tab.goToIndex(index) + } + const onOuterPress = () => sibling.destroy() + const sibling = new RootSiblings( + ( + <> + <TouchableWithoutFeedback onPress={onOuterPress}> + <View style={styles.bg} /> + </TouchableWithoutFeedback> + <View style={[styles.menu, styles.back]}> + {tab.backTen.map((item, i) => ( + <TouchableOpacity + key={item.index} + style={[styles.menuItem, i !== 0 && styles.menuItemBorder]} + onPress={() => onPressItem(item.index)}> + <Text>{item.title || item.url}</Text> + </TouchableOpacity> + ))} + </View> + </> + ), + ) + return sibling +} +export function createForwardMenu(tab: NavigationTabModel): RootSiblings { + const onPressItem = (index: number) => { + sibling.destroy() + tab.goToIndex(index) + } + const onOuterPress = () => sibling.destroy() + const sibling = new RootSiblings( + ( + <> + <TouchableWithoutFeedback onPress={onOuterPress}> + <View style={styles.bg} /> + </TouchableWithoutFeedback> + <View style={[styles.menu, styles.forward]}> + {tab.forwardTen.reverse().map((item, i) => ( + <TouchableOpacity + key={item.index} + style={[styles.menuItem, i !== 0 && styles.menuItemBorder]} + onPress={() => onPressItem(item.index)}> + <Text>{item.title || item.url}</Text> + </TouchableOpacity> + ))} + </View> + </> + ), + ) + return sibling +} + +const styles = StyleSheet.create({ + bg: { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + backgroundColor: '#000', + opacity: 0.1, + }, + menu: { + position: 'absolute', + bottom: 80, + backgroundColor: '#fff', + borderRadius: 8, + opacity: 1, + }, + back: { + left: 10, + }, + forward: { + left: 60, + }, + menuItem: { + paddingVertical: 10, + paddingLeft: 15, + paddingRight: 30, + }, + menuItemBorder: { + borderTopWidth: 1, + borderTopColor: '#ddd', + }, +}) 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', + }, +}) diff --git a/src/view/shell/mobile/tabs-selector.tsx b/src/view/shell/mobile/tabs-selector.tsx new file mode 100644 index 000000000..10651ba1f --- /dev/null +++ b/src/view/shell/mobile/tabs-selector.tsx @@ -0,0 +1,158 @@ +import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react' +import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native' +import BottomSheet from '@gorhom/bottom-sheet' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {s} from '../../lib/styles' +import {NavigationTabModel} from '../../../state/models/navigation' +import {createCustomBackdrop} from '../../com/util/BottomSheetCustomBackdrop' +import {match} from '../../routes' + +const TAB_HEIGHT = 38 +const TAB_SPACING = 5 +const BOTTOM_MARGIN = 70 + +export const TabsSelectorModal = forwardRef(function TabsSelectorModal( + { + onNewTab, + onChangeTab, + onCloseTab, + tabs, + currentTabIndex, + }: { + onNewTab: () => void + onChangeTab: (tabIndex: number) => void + onCloseTab: (tabIndex: number) => void + tabs: NavigationTabModel[] + currentTabIndex: number + }, + ref, +) { + const [isOpen, setIsOpen] = useState<boolean>(false) + const [snapPoints, setSnapPoints] = useState<number[]>([100]) + const bottomSheetRef = useRef<BottomSheet>(null) + + useImperativeHandle(ref, () => ({ + open() { + setIsOpen(true) + setSnapPoints([ + (tabs.length + 1) * (TAB_HEIGHT + TAB_SPACING) + BOTTOM_MARGIN, + ]) + bottomSheetRef.current?.expand() + }, + })) + + const onShareBottomSheetChange = (snapPoint: number) => { + if (snapPoint === -1) { + setIsOpen(false) + } + } + const onPressNewTab = () => { + onNewTab() + onClose() + } + const onPressChangeTab = (tabIndex: number) => { + onChangeTab(tabIndex) + onClose() + } + const onClose = () => { + setIsOpen(false) + bottomSheetRef.current?.close() + } + return ( + <BottomSheet + ref={bottomSheetRef} + index={-1} + snapPoints={snapPoints} + enablePanDownToClose + backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined} + onChange={onShareBottomSheetChange}> + <View style={s.p10}> + {tabs.map((tab, tabIndex) => { + const {icon} = match(tab.current.url) + const isActive = tabIndex === currentTabIndex + return ( + <View + key={tabIndex} + style={[styles.tab, styles.existing, isActive && styles.active]}> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <View style={styles.tabIcon}> + <FontAwesomeIcon size={16} icon={icon} /> + </View> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <Text + style={[styles.tabText, isActive && styles.tabTextActive]}> + {tab.current.title || tab.current.url} + </Text> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback onPress={() => onCloseTab(tabIndex)}> + <View style={styles.tabClose}> + <FontAwesomeIcon + size={16} + icon="x" + style={styles.tabCloseIcon} + /> + </View> + </TouchableWithoutFeedback> + </View> + ) + })} + <TouchableWithoutFeedback onPress={onPressNewTab}> + <View style={[styles.tab, styles.create]}> + <View style={styles.tabIcon}> + <FontAwesomeIcon size={16} icon="plus" /> + </View> + <Text style={styles.tabText}>New tab</Text> + </View> + </TouchableWithoutFeedback> + </View> + </BottomSheet> + ) +}) + +const styles = StyleSheet.create({ + tab: { + flexDirection: 'row', + width: '100%', + borderRadius: 4, + height: TAB_HEIGHT, + marginBottom: TAB_SPACING, + }, + existing: { + borderColor: '#000', + borderWidth: 1, + }, + create: { + backgroundColor: '#F8F3F3', + }, + active: { + backgroundColor: '#faf0f0', + borderColor: '#f00', + borderWidth: 1, + }, + tabIcon: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 15, + paddingRight: 10, + }, + tabText: { + flex: 1, + paddingTop: 10, + paddingBottom: 10, + }, + tabTextActive: { + fontWeight: 'bold', + }, + tabClose: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 15, + }, + tabCloseIcon: { + color: '#655', + }, +}) |