Showing
14 changed files
with
640 additions
and
3 deletions
| @@ -4,6 +4,7 @@ | @@ -4,6 +4,7 @@ | ||
| 4 | "parser": "@typescript-eslint/parser", | 4 | "parser": "@typescript-eslint/parser", |
| 5 | "plugins": ["@typescript-eslint"], | 5 | "plugins": ["@typescript-eslint"], |
| 6 | "rules": { | 6 | "rules": { |
| 7 | - "@typescript-eslint/no-unused-vars": "off" | 7 | + "@typescript-eslint/no-unused-vars": "off", |
| 8 | + "no-console": "warn" | ||
| 8 | } | 9 | } |
| 9 | } | 10 | } |
| @@ -235,6 +235,8 @@ PODS: | @@ -235,6 +235,8 @@ PODS: | ||
| 235 | - React-jsinspector (0.62.1) | 235 | - React-jsinspector (0.62.1) |
| 236 | - react-native-safe-area-context (0.7.3): | 236 | - react-native-safe-area-context (0.7.3): |
| 237 | - React | 237 | - React |
| 238 | + - react-native-webview (9.1.1): | ||
| 239 | + - React | ||
| 238 | - React-RCTActionSheet (0.62.1): | 240 | - React-RCTActionSheet (0.62.1): |
| 239 | - React-Core/RCTActionSheetHeaders (= 0.62.1) | 241 | - React-Core/RCTActionSheetHeaders (= 0.62.1) |
| 240 | - React-RCTAnimation (0.62.1): | 242 | - React-RCTAnimation (0.62.1): |
| @@ -294,6 +296,8 @@ PODS: | @@ -294,6 +296,8 @@ PODS: | ||
| 294 | - React-cxxreact (= 0.62.1) | 296 | - React-cxxreact (= 0.62.1) |
| 295 | - React-jsi (= 0.62.1) | 297 | - React-jsi (= 0.62.1) |
| 296 | - ReactCommon/callinvoker (= 0.62.1) | 298 | - ReactCommon/callinvoker (= 0.62.1) |
| 299 | + - ReactNativeART (1.2.0): | ||
| 300 | + - React | ||
| 297 | - ReactNativeDarkMode (0.2.2): | 301 | - ReactNativeDarkMode (0.2.2): |
| 298 | - React | 302 | - React |
| 299 | - RNCMaskedView (0.1.7): | 303 | - RNCMaskedView (0.1.7): |
| @@ -335,6 +339,7 @@ DEPENDENCIES: | @@ -335,6 +339,7 @@ DEPENDENCIES: | ||
| 335 | - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) | 339 | - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) |
| 336 | - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) | 340 | - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) |
| 337 | - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) | 341 | - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) |
| 342 | + - react-native-webview (from `../node_modules/react-native-webview`) | ||
| 338 | - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) | 343 | - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) |
| 339 | - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) | 344 | - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) |
| 340 | - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) | 345 | - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) |
| @@ -346,6 +351,7 @@ DEPENDENCIES: | @@ -346,6 +351,7 @@ DEPENDENCIES: | ||
| 346 | - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) | 351 | - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) |
| 347 | - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) | 352 | - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) |
| 348 | - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) | 353 | - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) |
| 354 | + - "ReactNativeART (from `../node_modules/@react-native-community/art`)" | ||
| 349 | - ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`) | 355 | - ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`) |
| 350 | - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" | 356 | - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" |
| 351 | - RNDeviceInfo (from `../node_modules/react-native-device-info`) | 357 | - RNDeviceInfo (from `../node_modules/react-native-device-info`) |
| @@ -401,6 +407,8 @@ EXTERNAL SOURCES: | @@ -401,6 +407,8 @@ EXTERNAL SOURCES: | ||
| 401 | :path: "../node_modules/react-native/ReactCommon/jsinspector" | 407 | :path: "../node_modules/react-native/ReactCommon/jsinspector" |
| 402 | react-native-safe-area-context: | 408 | react-native-safe-area-context: |
| 403 | :path: "../node_modules/react-native-safe-area-context" | 409 | :path: "../node_modules/react-native-safe-area-context" |
| 410 | + react-native-webview: | ||
| 411 | + :path: "../node_modules/react-native-webview" | ||
| 404 | React-RCTActionSheet: | 412 | React-RCTActionSheet: |
| 405 | :path: "../node_modules/react-native/Libraries/ActionSheetIOS" | 413 | :path: "../node_modules/react-native/Libraries/ActionSheetIOS" |
| 406 | React-RCTAnimation: | 414 | React-RCTAnimation: |
| @@ -421,6 +429,8 @@ EXTERNAL SOURCES: | @@ -421,6 +429,8 @@ EXTERNAL SOURCES: | ||
| 421 | :path: "../node_modules/react-native/Libraries/Vibration" | 429 | :path: "../node_modules/react-native/Libraries/Vibration" |
| 422 | ReactCommon: | 430 | ReactCommon: |
| 423 | :path: "../node_modules/react-native/ReactCommon" | 431 | :path: "../node_modules/react-native/ReactCommon" |
| 432 | + ReactNativeART: | ||
| 433 | + :path: "../node_modules/@react-native-community/art" | ||
| 424 | ReactNativeDarkMode: | 434 | ReactNativeDarkMode: |
| 425 | :path: "../node_modules/react-native-dark-mode" | 435 | :path: "../node_modules/react-native-dark-mode" |
| 426 | RNCMaskedView: | 436 | RNCMaskedView: |
| @@ -465,6 +475,7 @@ SPEC CHECKSUMS: | @@ -465,6 +475,7 @@ SPEC CHECKSUMS: | ||
| 465 | React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157 | 475 | React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157 |
| 466 | React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5 | 476 | React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5 |
| 467 | react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392 | 477 | react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392 |
| 478 | + react-native-webview: 0633fd7861a9bd7a80bacaee7da763c3afc248fa | ||
| 468 | React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88 | 479 | React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88 |
| 469 | React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96 | 480 | React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96 |
| 470 | React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6 | 481 | React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6 |
| @@ -475,6 +486,7 @@ SPEC CHECKSUMS: | @@ -475,6 +486,7 @@ SPEC CHECKSUMS: | ||
| 475 | React-RCTText: 239e040f401505001327a109f9188a4e6dad1bd2 | 486 | React-RCTText: 239e040f401505001327a109f9188a4e6dad1bd2 |
| 476 | React-RCTVibration: 072c3b427dd29e730c2ee5bfc509cf5054741a50 | 487 | React-RCTVibration: 072c3b427dd29e730c2ee5bfc509cf5054741a50 |
| 477 | ReactCommon: 3585806280c51d5c2c0d3aa5a99014c3badb629d | 488 | ReactCommon: 3585806280c51d5c2c0d3aa5a99014c3badb629d |
| 489 | + ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab | ||
| 478 | ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949 | 490 | ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949 |
| 479 | RNCMaskedView: 76c40a1d41c3e2535df09246a2b5487f04de0814 | 491 | RNCMaskedView: 76c40a1d41c3e2535df09246a2b5487f04de0814 |
| 480 | RNDeviceInfo: 6a3d16fce033f6979c4a6a41e62244d183e8c765 | 492 | RNDeviceInfo: 6a3d16fce033f6979c4a6a41e62244d183e8c765 |
| @@ -11,7 +11,10 @@ | @@ -11,7 +11,10 @@ | ||
| 11 | "commit": "git-cz" | 11 | "commit": "git-cz" |
| 12 | }, | 12 | }, |
| 13 | "dependencies": { | 13 | "dependencies": { |
| 14 | + "@huse/boolean": "^1.0.2", | ||
| 15 | + "@huse/immer": "^1.0.2", | ||
| 14 | "@huse/previous-value": "^1.0.1", | 16 | "@huse/previous-value": "^1.0.1", |
| 17 | + "@react-native-community/art": "^1.2.0", | ||
| 15 | "@react-native-community/masked-view": "^0.1.7", | 18 | "@react-native-community/masked-view": "^0.1.7", |
| 16 | "@react-navigation/bottom-tabs": "^5.2.5", | 19 | "@react-navigation/bottom-tabs": "^5.2.5", |
| 17 | "@react-navigation/drawer": "^5.4.0", | 20 | "@react-navigation/drawer": "^5.4.0", |
| @@ -24,11 +27,14 @@ | @@ -24,11 +27,14 @@ | ||
| 24 | "react-native-device-info": "^5.5.4", | 27 | "react-native-device-info": "^5.5.4", |
| 25 | "react-native-elements": "^1.2.7", | 28 | "react-native-elements": "^1.2.7", |
| 26 | "react-native-gesture-handler": "^1.6.1", | 29 | "react-native-gesture-handler": "^1.6.1", |
| 30 | + "react-native-progress": "^4.1.2", | ||
| 27 | "react-native-reanimated": "^1.7.1", | 31 | "react-native-reanimated": "^1.7.1", |
| 28 | "react-native-safe-area-context": "^0.7.3", | 32 | "react-native-safe-area-context": "^0.7.3", |
| 29 | "react-native-screens": "^2.4.0", | 33 | "react-native-screens": "^2.4.0", |
| 30 | "react-native-tab-view": "^2.13.0", | 34 | "react-native-tab-view": "^2.13.0", |
| 31 | - "react-native-vector-icons": "^6.6.0" | 35 | + "react-native-vector-icons": "^6.6.0", |
| 36 | + "react-native-webview": "9.1.1", | ||
| 37 | + "url-parse": "^1.4.7" | ||
| 32 | }, | 38 | }, |
| 33 | "devDependencies": { | 39 | "devDependencies": { |
| 34 | "@babel/core": "^7.6.2", | 40 | "@babel/core": "^7.6.2", |
| @@ -39,6 +45,7 @@ | @@ -39,6 +45,7 @@ | ||
| 39 | "@types/jest": "^24.0.24", | 45 | "@types/jest": "^24.0.24", |
| 40 | "@types/react-native": "^0.62.0", | 46 | "@types/react-native": "^0.62.0", |
| 41 | "@types/react-test-renderer": "16.9.2", | 47 | "@types/react-test-renderer": "16.9.2", |
| 48 | + "@types/url-parse": "^1.4.3", | ||
| 42 | "@typescript-eslint/eslint-plugin": "^2.27.0", | 49 | "@typescript-eslint/eslint-plugin": "^2.27.0", |
| 43 | "@typescript-eslint/parser": "^2.27.0", | 50 | "@typescript-eslint/parser": "^2.27.0", |
| 44 | "@welldone-software/why-did-you-render": "^4.0.7", | 51 | "@welldone-software/why-did-you-render": "^4.0.7", |
| @@ -16,6 +16,8 @@ import DesignList from './DesignList'; | @@ -16,6 +16,8 @@ import DesignList from './DesignList'; | ||
| 16 | import { useDarkMode } from 'react-native-dark-mode'; | 16 | import { useDarkMode } from 'react-native-dark-mode'; |
| 17 | import { themeForNav } from '../design'; | 17 | import { themeForNav } from '../design'; |
| 18 | import RNDeviceInfoList from './RNDeviceInfo'; | 18 | import RNDeviceInfoList from './RNDeviceInfo'; |
| 19 | +import WebviewScreen from './WebviewScreen'; | ||
| 20 | +import { Platform } from 'react-native'; | ||
| 19 | 21 | ||
| 20 | const MainTab = createBottomTabNavigator<MainTabParamList>(); | 22 | const MainTab = createBottomTabNavigator<MainTabParamList>(); |
| 21 | 23 | ||
| @@ -74,7 +76,7 @@ const Home = () => { | @@ -74,7 +76,7 @@ const Home = () => { | ||
| 74 | 76 | ||
| 75 | const MainStack = createStackNavigator<MainStackParamList>(); | 77 | const MainStack = createStackNavigator<MainStackParamList>(); |
| 76 | 78 | ||
| 77 | -export default () => { | 79 | +const Container = () => { |
| 78 | const inDarkMode = useDarkMode(); | 80 | const inDarkMode = useDarkMode(); |
| 79 | return ( | 81 | return ( |
| 80 | <NavigationContainer | 82 | <NavigationContainer |
| @@ -96,7 +98,17 @@ export default () => { | @@ -96,7 +98,17 @@ export default () => { | ||
| 96 | name="RNDeviceInfoList" | 98 | name="RNDeviceInfoList" |
| 97 | component={RNDeviceInfoList} | 99 | component={RNDeviceInfoList} |
| 98 | /> | 100 | /> |
| 101 | + <MainStack.Screen | ||
| 102 | + name="WebviewScreen" | ||
| 103 | + component={WebviewScreen} | ||
| 104 | + options={({ navigation, route }) => ({ | ||
| 105 | + // FIXME: https://github.com/react-native-community/react-native-webview/issues/575#issuecomment-587267906 | ||
| 106 | + animationEnabled: Platform.OS === 'ios', | ||
| 107 | + })} | ||
| 108 | + /> | ||
| 99 | </MainStack.Navigator> | 109 | </MainStack.Navigator> |
| 100 | </NavigationContainer> | 110 | </NavigationContainer> |
| 101 | ); | 111 | ); |
| 102 | }; | 112 | }; |
| 113 | + | ||
| 114 | +export default Container; |
| @@ -48,6 +48,15 @@ const SystemInfo = ({ | @@ -48,6 +48,15 @@ const SystemInfo = ({ | ||
| 48 | onPress={() => navigation.navigate('RNDeviceInfoList')} | 48 | onPress={() => navigation.navigate('RNDeviceInfoList')} |
| 49 | chevron | 49 | chevron |
| 50 | /> | 50 | /> |
| 51 | + <Divider /> | ||
| 52 | + <ListItem | ||
| 53 | + title={'RNCWebview'} | ||
| 54 | + onPress={() => | ||
| 55 | + navigation.navigate('WebviewScreen', { | ||
| 56 | + uri: 'https://www.baidu.com', | ||
| 57 | + }) | ||
| 58 | + } | ||
| 59 | + /> | ||
| 51 | </Card> | 60 | </Card> |
| 52 | </BGScroll> | 61 | </BGScroll> |
| 53 | ); | 62 | ); |
src/screen/WebviewScreen/Body.tsx
0 → 100644
| 1 | +import React, { useCallback } from 'react'; | ||
| 2 | +import RNCWebView, { WebViewNavigation } from 'react-native-webview'; | ||
| 3 | +import { WebviewState, WebviewActions, webActions } from './reducer'; | ||
| 4 | +import { | ||
| 5 | + WebViewProgressEvent, | ||
| 6 | + WebViewErrorEvent, | ||
| 7 | + OnShouldStartLoadWithRequest, | ||
| 8 | +} from 'react-native-webview/lib/WebViewTypes'; | ||
| 9 | +import ErrorView from './ErrorView'; | ||
| 10 | +import { Linking } from 'react-native'; | ||
| 11 | + | ||
| 12 | +const Body = React.forwardRef< | ||
| 13 | + RNCWebView, | ||
| 14 | + { | ||
| 15 | + state: WebviewState; | ||
| 16 | + dispatch: React.Dispatch<WebviewActions>; | ||
| 17 | + initialUrl: string; | ||
| 18 | + } | ||
| 19 | +>(({ state, dispatch, initialUrl }, ref) => { | ||
| 20 | + const onNavigationStateChange = useCallback( | ||
| 21 | + (s: WebViewNavigation) => { | ||
| 22 | + dispatch(webActions.changeNavigationState(s)); | ||
| 23 | + }, | ||
| 24 | + [dispatch] | ||
| 25 | + ); | ||
| 26 | + | ||
| 27 | + const onLoadProgress = useCallback( | ||
| 28 | + (s: WebViewProgressEvent) => { | ||
| 29 | + dispatch(webActions.onLoadProgress(s.nativeEvent)); | ||
| 30 | + }, | ||
| 31 | + [dispatch] | ||
| 32 | + ); | ||
| 33 | + | ||
| 34 | + const onError = useCallback( | ||
| 35 | + (s: WebViewErrorEvent) => { | ||
| 36 | + dispatch(webActions.onLoadError(s.nativeEvent)); | ||
| 37 | + }, | ||
| 38 | + [dispatch] | ||
| 39 | + ); | ||
| 40 | + | ||
| 41 | + const shouldRequest: OnShouldStartLoadWithRequest = useCallback( | ||
| 42 | + (request) => { | ||
| 43 | + const { url } = request; | ||
| 44 | + if (url.startsWith('http') || url === 'about:blank') { | ||
| 45 | + return true; | ||
| 46 | + } else { | ||
| 47 | + dispatch(webActions.changeNavigationState(request)); | ||
| 48 | + Linking.canOpenURL(url) | ||
| 49 | + .then((canOpen) => { | ||
| 50 | + if (canOpen) { | ||
| 51 | + return Linking.openURL(url); | ||
| 52 | + } | ||
| 53 | + }) | ||
| 54 | + .catch(() => {}); | ||
| 55 | + return false; | ||
| 56 | + } | ||
| 57 | + }, | ||
| 58 | + [dispatch] | ||
| 59 | + ); | ||
| 60 | + | ||
| 61 | + return ( | ||
| 62 | + <RNCWebView | ||
| 63 | + ref={ref} | ||
| 64 | + source={{ uri: initialUrl }} | ||
| 65 | + onNavigationStateChange={onNavigationStateChange} | ||
| 66 | + onLoadProgress={onLoadProgress} | ||
| 67 | + onError={onError} | ||
| 68 | + onShouldStartLoadWithRequest={shouldRequest} | ||
| 69 | + renderError={(_, code, description) => ( | ||
| 70 | + <ErrorView code={code} description={description} /> | ||
| 71 | + )} | ||
| 72 | + /> | ||
| 73 | + ); | ||
| 74 | +}); | ||
| 75 | + | ||
| 76 | +export default Body; |
src/screen/WebviewScreen/ErrorView.tsx
0 → 100644
| 1 | +import React from 'react'; | ||
| 2 | +import { SafeAreaView, Text, StyleSheet } from 'react-native'; | ||
| 3 | +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; | ||
| 4 | +import { | ||
| 5 | + DynamicStyleSheet, | ||
| 6 | + useDynamicStyleSheet, | ||
| 7 | + useDynamicValue, | ||
| 8 | +} from 'react-native-dark-mode'; | ||
| 9 | +import { colorPreset } from '../../design'; | ||
| 10 | + | ||
| 11 | +const dynamicStyles = new DynamicStyleSheet({ | ||
| 12 | + background: { | ||
| 13 | + backgroundColor: colorPreset.backgroundColor.secondary, | ||
| 14 | + ...StyleSheet.absoluteFillObject, | ||
| 15 | + alignItems: 'center', | ||
| 16 | + justifyContent: 'center', | ||
| 17 | + padding: 20, | ||
| 18 | + }, | ||
| 19 | + errorCode: { | ||
| 20 | + color: colorPreset.labelColor.primary, | ||
| 21 | + }, | ||
| 22 | + description: { | ||
| 23 | + color: colorPreset.labelColor.primary, | ||
| 24 | + }, | ||
| 25 | +}); | ||
| 26 | + | ||
| 27 | +export default function ErrorView({ | ||
| 28 | + code, | ||
| 29 | + description, | ||
| 30 | +}: { | ||
| 31 | + code: number; | ||
| 32 | + description: string; | ||
| 33 | +}) { | ||
| 34 | + const styles = useDynamicStyleSheet(dynamicStyles); | ||
| 35 | + const redColor = useDynamicValue(colorPreset.rainbow.red); | ||
| 36 | + return ( | ||
| 37 | + <SafeAreaView style={styles.background}> | ||
| 38 | + <MaterialCommunityIcons | ||
| 39 | + name={'close-circle'} | ||
| 40 | + size={60} | ||
| 41 | + color={redColor} | ||
| 42 | + /> | ||
| 43 | + <Text style={styles.errorCode}>{code}</Text> | ||
| 44 | + <Text style={styles.description}>{description}</Text> | ||
| 45 | + </SafeAreaView> | ||
| 46 | + ); | ||
| 47 | +} |
src/screen/WebviewScreen/Header.tsx
0 → 100644
| 1 | +import React, { | ||
| 2 | + useState, | ||
| 3 | + useCallback, | ||
| 4 | + useEffect, | ||
| 5 | + useLayoutEffect, | ||
| 6 | +} from 'react'; | ||
| 7 | +import URL from 'url-parse'; | ||
| 8 | +import { useToggle } from '@huse/boolean'; | ||
| 9 | +import RNCWebview from 'react-native-webview'; | ||
| 10 | +import { | ||
| 11 | + DynamicStyleSheet, | ||
| 12 | + useDynamicStyleSheet, | ||
| 13 | + useDynamicValue, | ||
| 14 | +} from 'react-native-dark-mode'; | ||
| 15 | +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; | ||
| 16 | +import * as Progress from 'react-native-progress'; | ||
| 17 | +import { | ||
| 18 | + Platform, | ||
| 19 | + SafeAreaView, | ||
| 20 | + View, | ||
| 21 | + Text, | ||
| 22 | + TouchableOpacity, | ||
| 23 | + StyleSheet, | ||
| 24 | + Animated, | ||
| 25 | + TextInput, | ||
| 26 | + NativeSyntheticEvent, | ||
| 27 | + TextInputSubmitEditingEventData, | ||
| 28 | + LayoutAnimation, | ||
| 29 | +} from 'react-native'; | ||
| 30 | +import { colorPreset } from '../../design'; | ||
| 31 | +import { WebviewState, WebviewActions, webActions } from './reducer'; | ||
| 32 | + | ||
| 33 | +const dynamicStyles = new DynamicStyleSheet({ | ||
| 34 | + headerContainer: { | ||
| 35 | + backgroundColor: colorPreset.backgroundColor.primary, | ||
| 36 | + borderBottomWidth: StyleSheet.hairlineWidth, | ||
| 37 | + borderBottomColor: colorPreset.separator.opaque, | ||
| 38 | + }, | ||
| 39 | + containerStyle: { | ||
| 40 | + backgroundColor: colorPreset.backgroundColor.secondary, | ||
| 41 | + borderRadius: 10, | ||
| 42 | + margin: 10, | ||
| 43 | + ...Platform.select({ | ||
| 44 | + ios: { | ||
| 45 | + height: 36, | ||
| 46 | + marginTop: 4, | ||
| 47 | + }, | ||
| 48 | + android: { | ||
| 49 | + height: 36, | ||
| 50 | + }, | ||
| 51 | + }), | ||
| 52 | + overflow: 'hidden', | ||
| 53 | + }, | ||
| 54 | + labelContainer: { | ||
| 55 | + flexDirection: 'row', | ||
| 56 | + flex: 1, | ||
| 57 | + alignItems: 'center', | ||
| 58 | + }, | ||
| 59 | + hostLabel: { | ||
| 60 | + color: colorPreset.labelColor.primary, | ||
| 61 | + flex: 1, | ||
| 62 | + textAlign: 'center', | ||
| 63 | + fontSize: 17, | ||
| 64 | + }, | ||
| 65 | + refreshButton: { | ||
| 66 | + width: 36, | ||
| 67 | + height: 36, | ||
| 68 | + justifyContent: 'center', | ||
| 69 | + alignItems: 'center', | ||
| 70 | + }, | ||
| 71 | + inputContainerStyle: { | ||
| 72 | + backgroundColor: colorPreset.backgroundColor.secondary, | ||
| 73 | + }, | ||
| 74 | + inputStyle: { | ||
| 75 | + flex: 1, | ||
| 76 | + paddingHorizontal: 16, | ||
| 77 | + }, | ||
| 78 | + progressBar: { | ||
| 79 | + position: 'absolute', | ||
| 80 | + left: 0, | ||
| 81 | + right: 0, | ||
| 82 | + bottom: 0, | ||
| 83 | + height: 2, | ||
| 84 | + }, | ||
| 85 | +}); | ||
| 86 | + | ||
| 87 | +interface Props { | ||
| 88 | + state: WebviewState; | ||
| 89 | + dispatch: React.Dispatch<WebviewActions>; | ||
| 90 | + webview: React.RefObject<RNCWebview>; | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +// EXPERIMENT | ||
| 94 | +function WebviewHeader({ state, dispatch, webview }: Props) { | ||
| 95 | + const [uri, setUri] = useState<URL>(); | ||
| 96 | + const [focused, toggleFocused] = useToggle(false); | ||
| 97 | + const progressBarOpacity = new Animated.Value(1); | ||
| 98 | + | ||
| 99 | + const { url, progress, loading } = state; | ||
| 100 | + useEffect(() => { | ||
| 101 | + const newUrl = new URL(url); | ||
| 102 | + setUri(newUrl); | ||
| 103 | + }, [url]); | ||
| 104 | + | ||
| 105 | + useEffect(() => { | ||
| 106 | + if (progress === 1) { | ||
| 107 | + Animated.timing(progressBarOpacity, { | ||
| 108 | + toValue: 0, | ||
| 109 | + useNativeDriver: true, | ||
| 110 | + duration: 1000, | ||
| 111 | + }).start(); | ||
| 112 | + } | ||
| 113 | + }, [progress, progressBarOpacity]); | ||
| 114 | + | ||
| 115 | + const onSubmitEditing = useCallback( | ||
| 116 | + (e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => { | ||
| 117 | + toggleFocused(); | ||
| 118 | + dispatch(webActions.loadText(e.nativeEvent.text)); | ||
| 119 | + }, | ||
| 120 | + [toggleFocused, dispatch] | ||
| 121 | + ); | ||
| 122 | + | ||
| 123 | + const onPressLoading = useCallback(() => { | ||
| 124 | + if (loading) { | ||
| 125 | + webview.current?.stopLoading(); | ||
| 126 | + } else { | ||
| 127 | + webview.current?.reload(); | ||
| 128 | + } | ||
| 129 | + }, [loading, webview]); | ||
| 130 | + | ||
| 131 | + useLayoutEffect(() => { | ||
| 132 | + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); | ||
| 133 | + }, [focused]); | ||
| 134 | + | ||
| 135 | + const styles = useDynamicStyleSheet(dynamicStyles); | ||
| 136 | + const redColor = useDynamicValue(colorPreset.rainbow.red); | ||
| 137 | + const greenColor = useDynamicValue(colorPreset.rainbow.green); | ||
| 138 | + const primaryLabelColor = useDynamicValue(colorPreset.labelColor.primary); | ||
| 139 | + return ( | ||
| 140 | + <SafeAreaView style={styles.headerContainer}> | ||
| 141 | + <View style={styles.containerStyle}> | ||
| 142 | + {focused ? ( | ||
| 143 | + <View style={styles.labelContainer}> | ||
| 144 | + <TextInput | ||
| 145 | + defaultValue={url} | ||
| 146 | + onSubmitEditing={onSubmitEditing} | ||
| 147 | + style={styles.inputStyle} | ||
| 148 | + underlineColorAndroid="transparent" | ||
| 149 | + textContentType="URL" | ||
| 150 | + selectTextOnFocus | ||
| 151 | + keyboardType="url" | ||
| 152 | + returnKeyType="go" | ||
| 153 | + autoCapitalize="none" | ||
| 154 | + autoCorrect={false} | ||
| 155 | + autoFocus | ||
| 156 | + onBlur={(e) => { | ||
| 157 | + toggleFocused(); | ||
| 158 | + }} | ||
| 159 | + clearButtonMode="while-editing" | ||
| 160 | + /> | ||
| 161 | + </View> | ||
| 162 | + ) : ( | ||
| 163 | + <TouchableOpacity | ||
| 164 | + activeOpacity={1} | ||
| 165 | + onPress={toggleFocused} | ||
| 166 | + style={styles.labelContainer} | ||
| 167 | + > | ||
| 168 | + <View style={styles.refreshButton}> | ||
| 169 | + <MaterialCommunityIcons | ||
| 170 | + name={ | ||
| 171 | + uri?.protocol === 'https:' | ||
| 172 | + ? 'shield-outline' | ||
| 173 | + : 'shield-off-outline' | ||
| 174 | + } | ||
| 175 | + color={uri?.protocol === 'https:' ? greenColor : redColor} | ||
| 176 | + size={20} | ||
| 177 | + /> | ||
| 178 | + </View> | ||
| 179 | + <Text style={styles.hostLabel}> | ||
| 180 | + {uri?.hostname.replace(/^www\./, '')} | ||
| 181 | + </Text> | ||
| 182 | + <TouchableOpacity | ||
| 183 | + onPress={onPressLoading} | ||
| 184 | + style={styles.refreshButton} | ||
| 185 | + > | ||
| 186 | + <MaterialCommunityIcons | ||
| 187 | + name={loading ? 'close' : 'refresh'} | ||
| 188 | + color={primaryLabelColor} | ||
| 189 | + size={20} | ||
| 190 | + /> | ||
| 191 | + </TouchableOpacity> | ||
| 192 | + <Animated.View | ||
| 193 | + style={[{ opacity: progressBarOpacity }, styles.progressBar]} | ||
| 194 | + > | ||
| 195 | + <Progress.Bar | ||
| 196 | + progress={progress} | ||
| 197 | + borderWidth={0} | ||
| 198 | + borderRadius={0} | ||
| 199 | + width={null} | ||
| 200 | + useNativeDriver | ||
| 201 | + /> | ||
| 202 | + </Animated.View> | ||
| 203 | + </TouchableOpacity> | ||
| 204 | + )} | ||
| 205 | + </View> | ||
| 206 | + </SafeAreaView> | ||
| 207 | + ); | ||
| 208 | +} | ||
| 209 | + | ||
| 210 | +export default WebviewHeader; |
src/screen/WebviewScreen/ProgressBar.tsx
0 → 100644
| 1 | +import React, { useEffect, useMemo, useState, useRef } from 'react'; | ||
| 2 | +import { Animated, StyleSheet } from 'react-native'; | ||
| 3 | +import * as Progress from 'react-native-progress'; | ||
| 4 | + | ||
| 5 | +const styles = StyleSheet.create({ | ||
| 6 | + progressBar: { | ||
| 7 | + position: 'absolute', | ||
| 8 | + left: 0, | ||
| 9 | + right: 0, | ||
| 10 | + top: 0, | ||
| 11 | + height: 1, | ||
| 12 | + }, | ||
| 13 | +}); | ||
| 14 | + | ||
| 15 | +export default function ProgressBar({ | ||
| 16 | + progress, | ||
| 17 | + loading, | ||
| 18 | +}: { | ||
| 19 | + progress: number; | ||
| 20 | + loading: boolean; | ||
| 21 | +}) { | ||
| 22 | + const progressBarOpacity = useRef(new Animated.Value(1)).current; | ||
| 23 | + | ||
| 24 | + useEffect(() => { | ||
| 25 | + if (!loading) { | ||
| 26 | + Animated.timing(progressBarOpacity, { | ||
| 27 | + toValue: 0, | ||
| 28 | + useNativeDriver: true, | ||
| 29 | + duration: 1000, | ||
| 30 | + }).start(); | ||
| 31 | + } else { | ||
| 32 | + progressBarOpacity.setValue(1); | ||
| 33 | + } | ||
| 34 | + }, [loading, progressBarOpacity]); | ||
| 35 | + | ||
| 36 | + return ( | ||
| 37 | + <Animated.View | ||
| 38 | + style={[{ opacity: progressBarOpacity }, styles.progressBar]} | ||
| 39 | + > | ||
| 40 | + <Progress.Bar | ||
| 41 | + progress={progress} | ||
| 42 | + borderWidth={0} | ||
| 43 | + borderRadius={0} | ||
| 44 | + width={null} | ||
| 45 | + height={4} | ||
| 46 | + useNativeDriver | ||
| 47 | + /> | ||
| 48 | + </Animated.View> | ||
| 49 | + ); | ||
| 50 | +} |
src/screen/WebviewScreen/Toolbar.tsx
0 → 100644
| 1 | +import React, { useCallback } from 'react'; | ||
| 2 | +import { SafeAreaView, TouchableOpacity, StyleSheet, View } from 'react-native'; | ||
| 3 | +import RNCWebview from 'react-native-webview'; | ||
| 4 | +import EvilIcons from 'react-native-vector-icons/EvilIcons'; | ||
| 5 | +import { WebviewState, WebviewActions } from './reducer'; | ||
| 6 | +import { | ||
| 7 | + DynamicStyleSheet, | ||
| 8 | + useDynamicStyleSheet, | ||
| 9 | + useDynamicValue, | ||
| 10 | +} from 'react-native-dark-mode'; | ||
| 11 | +import { colorPreset } from '../../design'; | ||
| 12 | + | ||
| 13 | +interface Props { | ||
| 14 | + state: WebviewState; | ||
| 15 | + dispatch: React.Dispatch<WebviewActions>; | ||
| 16 | + webview: React.RefObject<RNCWebview>; | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +const dynamicStyles = new DynamicStyleSheet({ | ||
| 20 | + background: { | ||
| 21 | + backgroundColor: colorPreset.backgroundColor.primary, | ||
| 22 | + borderTopWidth: StyleSheet.hairlineWidth, | ||
| 23 | + borderTopColor: colorPreset.separator.opaque, | ||
| 24 | + }, | ||
| 25 | + container: { | ||
| 26 | + height: 44, | ||
| 27 | + flexDirection: 'row', | ||
| 28 | + justifyContent: 'space-evenly', | ||
| 29 | + }, | ||
| 30 | + button: { | ||
| 31 | + width: 44, | ||
| 32 | + height: 44, | ||
| 33 | + justifyContent: 'center', | ||
| 34 | + alignItems: 'center', | ||
| 35 | + }, | ||
| 36 | +}); | ||
| 37 | + | ||
| 38 | +const Toolbar = ({ state, webview }: Props) => { | ||
| 39 | + const { canGoBack, canGoForward, loading } = state; | ||
| 40 | + const styles = useDynamicStyleSheet(dynamicStyles); | ||
| 41 | + const primaryLabelColor = useDynamicValue(colorPreset.labelColor.primary); | ||
| 42 | + const secondaryLabelColor = useDynamicValue(colorPreset.labelColor.tertiary); | ||
| 43 | + | ||
| 44 | + const onPressLoading = useCallback(() => { | ||
| 45 | + if (loading) { | ||
| 46 | + webview.current?.stopLoading(); | ||
| 47 | + } else { | ||
| 48 | + webview.current?.reload(); | ||
| 49 | + } | ||
| 50 | + }, [loading, webview]); | ||
| 51 | + | ||
| 52 | + return ( | ||
| 53 | + <SafeAreaView style={styles.background}> | ||
| 54 | + <View style={styles.container}> | ||
| 55 | + <TouchableOpacity | ||
| 56 | + style={styles.button} | ||
| 57 | + disabled={!canGoBack} | ||
| 58 | + onPress={() => webview.current?.goBack()} | ||
| 59 | + > | ||
| 60 | + <EvilIcons | ||
| 61 | + name="chevron-left" | ||
| 62 | + size={40} | ||
| 63 | + color={canGoBack ? primaryLabelColor : secondaryLabelColor} | ||
| 64 | + /> | ||
| 65 | + </TouchableOpacity> | ||
| 66 | + <TouchableOpacity | ||
| 67 | + style={styles.button} | ||
| 68 | + disabled={!canGoForward} | ||
| 69 | + onPress={() => webview.current?.goForward()} | ||
| 70 | + > | ||
| 71 | + <EvilIcons | ||
| 72 | + name="chevron-right" | ||
| 73 | + size={40} | ||
| 74 | + color={canGoForward ? primaryLabelColor : secondaryLabelColor} | ||
| 75 | + /> | ||
| 76 | + </TouchableOpacity> | ||
| 77 | + <TouchableOpacity style={styles.button} onPress={onPressLoading}> | ||
| 78 | + <EvilIcons | ||
| 79 | + name={loading ? 'close' : 'refresh'} | ||
| 80 | + size={40} | ||
| 81 | + color={primaryLabelColor} | ||
| 82 | + /> | ||
| 83 | + </TouchableOpacity> | ||
| 84 | + </View> | ||
| 85 | + </SafeAreaView> | ||
| 86 | + ); | ||
| 87 | +}; | ||
| 88 | + | ||
| 89 | +export default Toolbar; |
src/screen/WebviewScreen/index.tsx
0 → 100644
| 1 | +import React, { useRef, useEffect, useReducer } from 'react'; | ||
| 2 | +import { useImmerReducer } from '@huse/immer'; | ||
| 3 | +import RNCWebview from 'react-native-webview'; | ||
| 4 | +import { reducer, defaultState, webActions, WebviewActions } from './reducer'; | ||
| 5 | +import { BGView } from '../../component/View'; | ||
| 6 | +import Header from './Header'; | ||
| 7 | +import Body from './Body'; | ||
| 8 | +import { MainStackScreenProps } from '../../type/Navigation'; | ||
| 9 | +import Toolbar from './Toolbar'; | ||
| 10 | +import ProgressBar from './ProgressBar'; | ||
| 11 | + | ||
| 12 | +const WebviewScreen = ({ | ||
| 13 | + navigation, | ||
| 14 | + route, | ||
| 15 | +}: MainStackScreenProps<'WebviewScreen'>) => { | ||
| 16 | + const paramUri = route.params?.uri ?? 'https://about:blank'; | ||
| 17 | + const [state, dispatch] = useImmerReducer(reducer, { | ||
| 18 | + ...defaultState, | ||
| 19 | + url: paramUri, | ||
| 20 | + }); | ||
| 21 | + | ||
| 22 | + const webview = useRef<RNCWebview>(null); | ||
| 23 | + | ||
| 24 | + return ( | ||
| 25 | + <BGView> | ||
| 26 | + <Body | ||
| 27 | + initialUrl={paramUri} | ||
| 28 | + state={state} | ||
| 29 | + dispatch={dispatch} | ||
| 30 | + ref={webview} | ||
| 31 | + /> | ||
| 32 | + <ProgressBar progress={state.progress} loading={state.loading} /> | ||
| 33 | + <Toolbar state={state} dispatch={dispatch} webview={webview} /> | ||
| 34 | + </BGView> | ||
| 35 | + ); | ||
| 36 | +}; | ||
| 37 | + | ||
| 38 | +export default WebviewScreen; |
src/screen/WebviewScreen/reducer.ts
0 → 100644
| 1 | +import React, { Reducer } from 'react'; | ||
| 2 | +import { WebViewNavigation } from 'react-native-webview'; | ||
| 3 | +import { ImmerReducer } from '@huse/immer'; | ||
| 4 | +import { | ||
| 5 | + WebViewError, | ||
| 6 | + WebViewProgressEvent, | ||
| 7 | + WebViewNativeProgressEvent, | ||
| 8 | + WebViewNativeEvent, | ||
| 9 | +} from 'react-native-webview/lib/WebViewTypes'; | ||
| 10 | + | ||
| 11 | +export type WebviewActionTypes = | ||
| 12 | + | 'ChangeNavigationState' | ||
| 13 | + | 'OnLoadError' | ||
| 14 | + | 'OnLoadProgress' | ||
| 15 | + | 'LoadText'; | ||
| 16 | + | ||
| 17 | +type PayloadAction<Type extends WebviewActionTypes, Payload> = { | ||
| 18 | + type: Type; | ||
| 19 | + payload: Payload; | ||
| 20 | +}; | ||
| 21 | + | ||
| 22 | +export type WebviewActions = | ||
| 23 | + | PayloadAction<'ChangeNavigationState', WebViewNavigation> | ||
| 24 | + | PayloadAction<'LoadText', string> | ||
| 25 | + | PayloadAction<'OnLoadError', WebViewError> | ||
| 26 | + | PayloadAction<'OnLoadProgress', WebViewNativeProgressEvent>; | ||
| 27 | + | ||
| 28 | +type PayloadOf< | ||
| 29 | + A extends { type: string; payload: any }, | ||
| 30 | + T extends string | ||
| 31 | +> = A extends { | ||
| 32 | + type: T; | ||
| 33 | + payload: infer R; | ||
| 34 | +} | ||
| 35 | + ? R | ||
| 36 | + : never; | ||
| 37 | + | ||
| 38 | +const createAction = < | ||
| 39 | + T extends WebviewActionTypes, | ||
| 40 | + P extends PayloadOf<WebviewActions, T> | ||
| 41 | +>( | ||
| 42 | + type: T | ||
| 43 | +) => (payload: P) => ({ | ||
| 44 | + type, | ||
| 45 | + payload, | ||
| 46 | +}); | ||
| 47 | + | ||
| 48 | +export const webActions = { | ||
| 49 | + changeNavigationState: createAction('ChangeNavigationState'), | ||
| 50 | + onLoadError: createAction('OnLoadError'), | ||
| 51 | + onLoadProgress: createAction('OnLoadProgress'), | ||
| 52 | + loadText: createAction('LoadText'), | ||
| 53 | +}; | ||
| 54 | + | ||
| 55 | +export type WebviewState = WebViewNativeEvent & { | ||
| 56 | + domain?: string; | ||
| 57 | + code?: number; | ||
| 58 | + description?: string; | ||
| 59 | + progress: number; | ||
| 60 | +}; | ||
| 61 | + | ||
| 62 | +export const defaultState: WebviewState = { | ||
| 63 | + canGoBack: false, | ||
| 64 | + canGoForward: false, | ||
| 65 | + url: 'https://about:blank', | ||
| 66 | + title: '', | ||
| 67 | + lockIdentifier: 0, | ||
| 68 | + loading: false, | ||
| 69 | + progress: 0, | ||
| 70 | +}; | ||
| 71 | + | ||
| 72 | +export const reducer: ImmerReducer<WebviewState, WebviewActions> = ( | ||
| 73 | + state, | ||
| 74 | + action | ||
| 75 | +) => { | ||
| 76 | + switch (action.type) { | ||
| 77 | + case 'ChangeNavigationState': | ||
| 78 | + case 'OnLoadProgress': | ||
| 79 | + case 'OnLoadError': | ||
| 80 | + return { ...state, ...action.payload }; | ||
| 81 | + case 'LoadText': | ||
| 82 | + state.url = action.payload; | ||
| 83 | + break; | ||
| 84 | + } | ||
| 85 | +}; |
| @@ -10,6 +10,7 @@ export type MainTabParamList = { | @@ -10,6 +10,7 @@ export type MainTabParamList = { | ||
| 10 | export type MainStackParamList = { | 10 | export type MainStackParamList = { |
| 11 | MainTab: undefined; | 11 | MainTab: undefined; |
| 12 | RNDeviceInfoList: undefined; | 12 | RNDeviceInfoList: undefined; |
| 13 | + WebviewScreen: { uri: string } | undefined; | ||
| 13 | }; | 14 | }; |
| 14 | 15 | ||
| 15 | export type MainTabScreenProps<RouteName extends keyof MainTabParamList> = { | 16 | export type MainTabScreenProps<RouteName extends keyof MainTabParamList> = { |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment