mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2025-01-05 20:58:27 +00:00
292 lines
9.1 KiB
JavaScript
292 lines
9.1 KiB
JavaScript
/**
|
|
* 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 { Platform, RefreshControl, StyleSheet, ScrollView, View } from 'react-native';
|
|
import { Button, Text } from 'react-native-elements';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { WebView } from 'react-native-webview';
|
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
import { observer } from 'mobx-react';
|
|
import Constants from 'expo-constants';
|
|
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
|
import * as ScreenOrientation from 'expo-screen-orientation';
|
|
import { setStatusBarHidden } from 'expo-status-bar';
|
|
import PropTypes from 'prop-types';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useStores } from '../hooks/useStores';
|
|
import Colors from '../constants/Colors';
|
|
import { getAppName, getSafeDeviceName } from '../utils/Device';
|
|
import { getIconName } from '../utils/Icons';
|
|
import NativeShell from '../utils/NativeShell';
|
|
import { openBrowser } from '../utils/WebBrowser';
|
|
|
|
const injectedJavaScript = `
|
|
window.ExpoAppInfo = {
|
|
appName: '${getAppName()}',
|
|
appVersion: '${Constants.nativeAppVersion}',
|
|
deviceId: '${Constants.deviceId}',
|
|
deviceName: '${getSafeDeviceName().replace(/'/g, '\\\'')}'
|
|
};
|
|
|
|
${NativeShell}
|
|
|
|
true;
|
|
`;
|
|
|
|
const HomeScreen = observer(class HomeScreen extends React.Component {
|
|
state = {
|
|
isError: false,
|
|
isFullscreen: false,
|
|
isLoading: true,
|
|
isRefreshing: false
|
|
};
|
|
|
|
static propTypes = {
|
|
navigation: PropTypes.object.isRequired,
|
|
route: PropTypes.object.isRequired,
|
|
rootStore: PropTypes.object.isRequired,
|
|
t: PropTypes.func.isRequired
|
|
}
|
|
|
|
getErrorView() {
|
|
const { t } = this.props;
|
|
|
|
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={() => this.onRefresh()}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
onGoHome() {
|
|
this.webview.injectJavaScript('window.Emby && window.Emby.Page && typeof window.Emby.Page.goHome === "function" && window.Emby.Page.goHome();');
|
|
}
|
|
|
|
async onMessage({ nativeEvent: state }) {
|
|
try {
|
|
const { event, data } = JSON.parse(state.data);
|
|
switch (event) {
|
|
case 'enableFullscreen':
|
|
this.setState({ isFullscreen: true });
|
|
break;
|
|
case 'disableFullscreen':
|
|
this.setState({ 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 (this.props.rootStore.settingStore.isScreenLockEnabled) {
|
|
activateKeepAwake();
|
|
}
|
|
break;
|
|
case 'hideMediaSession':
|
|
// When music session stops disable keep awake
|
|
if (this.props.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);
|
|
}
|
|
}
|
|
|
|
onRefresh() {
|
|
// Disable pull to refresh when in fullscreen
|
|
if (this.state.isFullscreen) return;
|
|
|
|
this.setState({
|
|
isLoading: true,
|
|
isRefreshing: true
|
|
});
|
|
this.webview.reload();
|
|
this.setState({ isRefreshing: false });
|
|
}
|
|
|
|
async updateScreenOrientation() {
|
|
if (this.props.rootStore.settingStore.isRotationEnabled) {
|
|
if (this.state.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
// Override the default tab press behavior so a second press sends the webview home
|
|
this.props.navigation.addListener('tabPress', e => {
|
|
if (this.props.navigation.isFocused()) {
|
|
// Prevent default behavior
|
|
e.preventDefault();
|
|
|
|
this.onGoHome();
|
|
}
|
|
});
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
if (prevState.isFullscreen !== this.state.isFullscreen) {
|
|
// Update the screen orientation
|
|
this.updateScreenOrientation();
|
|
// Show/hide the bottom tab bar
|
|
this.props.navigation.setOptions({
|
|
tabBarVisible: !this.state.isFullscreen
|
|
});
|
|
// Show/hide the status bar
|
|
setStatusBarHidden(this.state.isFullscreen);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
// When not in fullscreen, the top adjustment is handled by the spacer View for iOS
|
|
const safeAreaEdges = ['right', 'bottom', 'left'];
|
|
if (Platform.OS !== 'ios' || this.state.isFullscreen) {
|
|
safeAreaEdges.push('top');
|
|
}
|
|
// Hide webview until loaded
|
|
const webviewStyle = (this.state.isError || this.state.isLoading) ? styles.loading : styles.container;
|
|
|
|
if (!this.props.rootStore.serverStore.servers || this.props.rootStore.serverStore.servers.length === 0) {
|
|
return null;
|
|
}
|
|
const server = this.props.rootStore.serverStore.servers[this.props.rootStore.settingStore.activeServer];
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={safeAreaEdges} >
|
|
{Platform.OS === 'ios' && !this.state.isFullscreen && (
|
|
<View style={styles.statusBarSpacer} />
|
|
)}
|
|
<ScrollView
|
|
style={styles.container}
|
|
contentContainerStyle={{flexGrow: 1}}
|
|
refreshControl={
|
|
Platform.OS === 'ios' ? (
|
|
<RefreshControl
|
|
refreshing={this.state.isRefreshing}
|
|
onRefresh={() => this.onRefresh()}
|
|
tintColor={Colors.tabText}
|
|
/>
|
|
) : null
|
|
}
|
|
>
|
|
{server && server.urlString && (
|
|
<WebView
|
|
ref={ref => (this.webview = ref)}
|
|
source={{ uri: server.urlString }}
|
|
style={webviewStyle}
|
|
// Inject javascript for NativeShell
|
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
// Handle messages from NativeShell
|
|
onMessage={this.onMessage.bind(this)}
|
|
// Make scrolling feel faster
|
|
decelerationRate='normal'
|
|
// Error screen is displayed if loading fails
|
|
renderError={() => this.getErrorView()}
|
|
// Loading screen is displayed when refreshing
|
|
renderLoading={() => <View style={styles.container} />}
|
|
// Update state on loading error
|
|
onError={({ nativeEvent: state }) => {
|
|
console.warn('Error', state);
|
|
this.setState({ isError: true });
|
|
}}
|
|
// Update state when loading is complete
|
|
onLoad={() => {
|
|
this.setState({
|
|
isError: false,
|
|
isLoading: false
|
|
});
|
|
}}
|
|
// Media playback options to fix video player
|
|
allowsInlineMediaPlayback={true}
|
|
mediaPlaybackRequiresUserAction={false}
|
|
// Use WKWebView on iOS
|
|
useWebKit={true}
|
|
/>
|
|
)}
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
});
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: Colors.backgroundColor
|
|
},
|
|
loading: {
|
|
flex: 1,
|
|
backgroundColor: Colors.backgroundColor,
|
|
opacity: 0
|
|
},
|
|
statusBarSpacer: {
|
|
backgroundColor: Colors.headerBackgroundColor,
|
|
height: Constants.statusBarHeight
|
|
},
|
|
error: {
|
|
fontSize: 17,
|
|
paddingBottom: 17,
|
|
textAlign: 'center'
|
|
}
|
|
});
|
|
|
|
// Inject the Navigation Hook as a prop to mimic the legacy behavior
|
|
const HomeScreenWithNavigation = observer((props) => {
|
|
const stores = useStores();
|
|
const translation = useTranslation();
|
|
|
|
return (
|
|
<HomeScreen
|
|
{...props}
|
|
navigation={useNavigation()}
|
|
route={useRoute()}
|
|
{...stores}
|
|
{...translation}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export default HomeScreenWithNavigation;
|