Skip to content

Commit

Permalink
Merge pull request #119 from thornbill/refactor-home-screen
Browse files Browse the repository at this point in the history
Refactor HomeScreen and fix pull to refresh
  • Loading branch information
anthonylavado authored Jul 24, 2020
2 parents c8320e3 + c816478 commit 18dc84a
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 271 deletions.
21 changes: 21 additions & 0 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ const App = observer(({ skipLoadingScreen }) => {
}
}, [rootStore.settingStore.isRotationEnabled]);

const updateScreenOrientation = async () => {
if (rootStore.settingStore.isRotationEnabled) {
if (rootStore.isFullscreen) {
// Lock to landscape orientation
// For some reason video apps on iPhone use LANDSCAPE_RIGHT ¯\_(ツ)_/¯
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT);
// Allow either landscape orientation after forcing initial rotation
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
} else {
// Restore portrait orientation lock
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
}
}
};

useEffect(() => {
// Update the screen orientation
updateScreenOrientation();
}, [rootStore.isFullscreen]);

const loadImagesAsync = () => {
const images = [
require('./assets/images/splash.png'),
Expand Down Expand Up @@ -114,6 +134,7 @@ const App = observer(({ skipLoadingScreen }) => {
<StatusBar
style="light"
backgroundColor={Colors.headerBackgroundColor}
hidden={rootStore.isFullscreen}
/>
<AppNavigator />
</ThemeProvider>
Expand Down
130 changes: 130 additions & 0 deletions components/NativeShellWebView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import React, { useEffect, useRef, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import Constants from 'expo-constants';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';

import { useStores } from '../hooks/useStores';
import { getAppName, getSafeDeviceName } from '../utils/Device';
import NativeShell from '../utils/NativeShell';
import { openBrowser } from '../utils/WebBrowser';
import RefreshWebView from './RefreshWebView';

const injectedJavaScript = `
window.ExpoAppInfo = {
appName: '${getAppName()}',
appVersion: '${Constants.nativeAppVersion}',
deviceId: '${Constants.deviceId}',
deviceName: '${getSafeDeviceName().replace(/'/g, '\\\'')}'
};
${NativeShell}
true;
`;

const NativeShellWebView = observer((props) => {
const { rootStore } = useStores();
const navigation = useNavigation();
const [isRefreshing, setIsRefreshing] = useState(false);
const webview = useRef(null);

const server = rootStore.serverStore.servers[rootStore.settingStore.activeServer];

useEffect(() => {
navigation.addListener('tabPress', e => {
if (navigation.isFocused()) {
// Prevent default behavior
e.preventDefault();
// Call the web router to navigate home
webview.current?.injectJavaScript('window.Emby && window.Emby.Page && typeof window.Emby.Page.goHome === "function" && window.Emby.Page.goHome();');
}
});
}, []);

const onRefresh = () => {
// Disable pull to refresh when in fullscreen
if (rootStore.isFullscreen) return;
setIsRefreshing(true);
webview.current?.reload();
setIsRefreshing(false);
};

const onMessage = action(({ nativeEvent: state }) => {
try {
const { event, data } = JSON.parse(state.data);
switch (event) {
case 'enableFullscreen':
rootStore.isFullscreen = true;
break;
case 'disableFullscreen':
rootStore.isFullscreen = false;
break;
case 'openUrl':
console.log('Opening browser for external url', data.url);
openBrowser(data.url);
break;
case 'updateMediaSession':
// Keep the screen awake when music is playing
if (rootStore.settingStore.isScreenLockEnabled) {
activateKeepAwake();
}
break;
case 'hideMediaSession':
// When music session stops disable keep awake
if (rootStore.settingStore.isScreenLockEnabled) {
deactivateKeepAwake();
}
break;
case 'console.debug':
// console.debug('[Browser Console]', data);
break;
case 'console.error':
console.error('[Browser Console]', data);
break;
case 'console.info':
// console.info('[Browser Console]', data);
break;
case 'console.log':
// console.log('[Browser Console]', data);
break;
case 'console.warn':
console.warn('[Browser Console]', data);
break;
default:
console.debug('[HomeScreen.onMessage]', event, data);
}
} catch (ex) {
console.warn('Exception handling message', state.data);
}
});

return (
<RefreshWebView
ref={webview}
source={{ uri: server.urlString }}
// Inject javascript for NativeShell
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
onMessage={onMessage}
isRefreshing={isRefreshing}
onRefresh={onRefresh}
// Pass through additional props
{...props}
// Make scrolling feel faster
decelerationRate='normal'
// Media playback options to fix video player
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
// Use WKWebView on iOS
useWebKit={true}
/>
);
});

export default NativeShellWebView;
52 changes: 52 additions & 0 deletions components/OfflineErrorView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Button, Text } from 'react-native-elements';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';

import Colors from '../constants/Colors';
import { getIconName } from '../utils/Icons';

const OfflineErrorView = ({ onRetry }) => {
const { t } = useTranslation();

return (
<View style={styles.container}>
<Text style={styles.error}>{t('home.offline')}</Text>
<Button
buttonStyle={{
marginLeft: 15,
marginRight: 15
}}
icon={{
name: getIconName('refresh'),
type: 'ionicon'
}}
title={t('home.retry')}
onPress={onRetry}
/>
</View>);
};

OfflineErrorView.propTypes = {
onRetry: PropTypes.func.isRequired
};

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.backgroundColor
},
error: {
fontSize: 17,
paddingBottom: 17,
textAlign: 'center'
}
});

export default OfflineErrorView;
62 changes: 62 additions & 0 deletions components/RefreshWebView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import React, { useState } from 'react';
import { WebView } from 'react-native-webview';
import { RefreshControl, Dimensions, StyleSheet } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import PropTypes from 'prop-types';

const RefreshWebView = React.forwardRef(
function RefreshWebView({ isRefreshing, onRefresh, refreshControlProps, ...webViewProps }, ref) {
const [height, setHeight] = useState(Dimensions.get('screen').height);
const [isEnabled, setEnabled] = useState(typeof onRefresh === 'function');

return (
<ScrollView
onLayout={(e) => setHeight(e.nativeEvent.layout.height)}
refreshControl={
<RefreshControl
onRefresh={onRefresh}
refreshing={isRefreshing}
enabled={isEnabled}
{...refreshControlProps}
/>
}
style={styles.view}>
<WebView
ref={ref}
{...webViewProps}
onScroll={(e) =>
setEnabled(
typeof onRefresh === 'function' &&
e.nativeEvent.contentOffset.y === 0
)
}
style={{
...styles.view,
height,
...webViewProps.style
}}
/>
</ScrollView>
);
}
);

RefreshWebView.propTypes = {
isRefreshing: PropTypes.bool.isRequired,
onRefresh: PropTypes.func,
refreshControlProps: PropTypes.any
};

const styles = StyleSheet.create({
view: {
flex: 1,
height: '100%'
}
});

export default RefreshWebView;
5 changes: 3 additions & 2 deletions components/ServerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';
import { ActivityIndicator, Platform, StyleSheet } from 'react-native';
import { Input, colors } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import PropTypes from 'prop-types';

Expand All @@ -33,7 +34,7 @@ const ServerInput = observer(class ServerInput extends React.Component {
validationMessage: ''
}

async onAddServer() {
onAddServer = action(async () => {
const { host } = this.state;
console.log('add server', host);
if (host) {
Expand Down Expand Up @@ -92,7 +93,7 @@ const ServerInput = observer(class ServerInput extends React.Component {
validationMessage: this.props.t('addServer.validation.empty')
});
}
}
})

render() {
return (
Expand Down
9 changes: 6 additions & 3 deletions constants/Colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@
*/
const backgroundColor = '#101010';
const textColor = '#fff';
const tintColor = '#00a4dc';
const primaryBlue = '#00a4dc';
const primaryPurple = '#AA5CC3';

export default {
primaryBlue,
primaryPurple,
textColor,
backgroundColor,
headerBackgroundColor: '#202020',
tintColor,
tintColor: primaryBlue,
headerTintColor: textColor,
tabText: '#ccc',
errorBackground: 'red',
errorText: textColor,
warningBackground: '#EAEB5E',
warningText: '#666804',
noticeBackground: tintColor,
noticeBackground: primaryBlue,
noticeText: textColor
};
8 changes: 4 additions & 4 deletions navigation/AppNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ const AppNavigator = observer(() => {
component={Main}
options={({ route }) => {
const routeName = route.state ?
// Get the currently active route name in the tab navigator
// Get the currently active route name in the tab navigator
route.state.routes[route.state.index].name :
// If state doesn't exist, we need to default to `screen` param if available, or the initial screen
// In our case, it's "Main" as that's the first screen inside the navigator
route.params?.screen || 'Main';
// If state doesn't exist, we need to default to `screen` param if available, or the initial screen
// In our case, it's "Main" as that's the first screen inside the navigator
route.params?.screen || 'Main';
return ({
headerShown: routeName === 'Settings',
title: t(`headings.${routeName.toLowerCase()}`)
Expand Down
Loading

0 comments on commit 18dc84a

Please sign in to comment.