Refactor settings screen

This commit is contained in:
Bill Thornton 2020-07-01 17:10:48 -04:00
parent 7d041afc1c
commit 1958194e5e
16 changed files with 464 additions and 46 deletions

View File

@ -0,0 +1,31 @@
/**
* 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 { colors, Text } from 'react-native-elements';
import Constants from 'expo-constants';
import { getAppName } from '../utils/Device';
const AppInfoFooter = () => (
<View style={styles.container}>
<Text style={styles.text}>{`${getAppName()}`}</Text>
<Text style={styles.text}>{`${Constants.nativeAppVersion} (${Constants.nativeBuildVersion})`}</Text>
<Text style={styles.text}>{`Expo Version: ${Constants.expoVersion}`}</Text>
</View>
);
const styles = StyleSheet.create({
container: {
margin: 15
},
text: {
color: colors.grey4,
fontSize: 15
}
});
export default AppInfoFooter;

View File

@ -0,0 +1,34 @@
/**
* 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 { ListItem } from 'react-native-elements';
import PropTypes from 'prop-types';
import { openBrowser } from '../utils/WebBrowser';
const BrowserListItem = ({item, index}) => (
<ListItem
title={item.name}
leftIcon={item.icon}
topDivider={index === 0}
bottomDivider
chevron
onPress={() => {
openBrowser(item.url);
}}
/>
);
BrowserListItem.propTypes = {
item: PropTypes.shape({
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
}).isRequired,
index: PropTypes.number.isRequired
};
export default BrowserListItem;

View File

@ -0,0 +1,25 @@
/**
* 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 } from 'react-native';
import { Button } from 'react-native-elements';
import PropTypes from 'prop-types';
const ButtonListItem = ({item}) => (
<Button {...item} buttonStyle={{ ...styles.button, ...item.buttonStyle }} />
);
ButtonListItem.propTypes = {
item: PropTypes.object.isRequired
};
const styles = StyleSheet.create({
button: {
margin: 15
}
});
export default ButtonListItem;

View File

@ -0,0 +1,79 @@
/**
* 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, Platform } from 'react-native';
import { Button, Icon, ListItem, colors } from 'react-native-elements';
import PropTypes from 'prop-types';
import { getIconName } from '../utils/Icons';
const ServerListItem = ({item, index, activeServer, onDelete, onPress}) => {
let title;
let subtitle;
if (item.info) {
title = item.info.ServerName;
subtitle = `Version: ${item.info.Version}\n${item.urlString}`;
} else {
title = item.url.host;
subtitle = `Version: unknown\n${item.urlString}`;
}
return (
<ListItem
title={title}
titleStyle={styles.title}
subtitle={subtitle}
leftElement={(
index === activeServer ? (
<Icon
name={getIconName('checkmark')}
type='ionicon'
size={24}
containerStyle={styles.leftElement}
/>
) : (
<View style={styles.leftElement} />
)
)}
rightElement={(
<Button
type='clear'
icon={{
name: getIconName('trash'),
type: 'ionicon',
iconStyle: styles.deleteButton
}}
onPress={() => onDelete(index)}
/>
)}
topDivider={index === 0}
bottomDivider
onPress={() => onPress(index)}
/>
);
};
ServerListItem.propTypes = {
item: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
activeServer: PropTypes.number.isRequired,
onDelete: PropTypes.func.isRequired,
onPress: PropTypes.func.isRequired
};
const styles = StyleSheet.create({
title: {
marginBottom: 2
},
leftElement: {
width: 12
},
deleteButton: {
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error
}
});
export default ServerListItem;

View File

@ -7,6 +7,7 @@ import { Platform } from 'react-native';
export default [
{
key: 'links-website',
name: 'Jellyfin Website',
url: 'https://jellyfin.org/',
icon: {
@ -15,6 +16,7 @@ export default [
}
},
{
key: 'links-documentation',
name: 'Documentation',
url: 'https://docs.jellyfin.org',
icon: {
@ -23,6 +25,7 @@ export default [
}
},
{
key: 'links-source',
name: 'Source Code',
url: 'https://github.com/jellyfin/jellyfin-expo',
icon: {
@ -31,6 +34,7 @@ export default [
}
},
{
key: 'links-feature',
name: 'Request a Feature',
url: 'https://features.jellyfin.org/',
icon: {
@ -39,6 +43,7 @@ export default [
}
},
{
key: 'links-issue',
name: 'Report an Issue',
url: 'https://github.com/jellyfin/jellyfin-expo/issues',
icon: {

53
models/ServerModel.js Normal file
View File

@ -0,0 +1,53 @@
/**
* 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 { action, autorun, computed, observable } from 'mobx';
import { ignore } from 'mobx-sync';
import { task } from 'mobx-task';
import JellyfinValidator from '../utils/JellyfinValidator';
export default class ServerModel {
@observable
id
@observable
url
@ignore
@observable
online = false
@observable
info
constructor(id, url, info) {
this.id = id;
this.url = url;
this.info = info;
autorun(() => {
this.urlString = this.parseUrlString;
});
}
@computed
get parseUrlString() {
try {
return JellyfinValidator.getServerUrl(this);
} catch (ex) {
return '';
}
}
@task
async fetchInfo() {
return await JellyfinValidator.fetchServerInfo(this)
.then(action(info => {
this.online = true;
this.info = info;
}));
}
}

View File

@ -16,6 +16,7 @@ import Colors from '../constants/Colors';
import AddServerScreen from '../screens/AddServerScreen';
import HomeScreen from '../screens/HomeScreen';
import SettingsScreen from '../screens/SettingsScreen';
import NewSettingsScreen from '../screens/NewSettingsScreen';
// Customize theme for navigator
const theme = {
@ -57,7 +58,7 @@ function Main() {
}}
>
<Tab.Screen name='Home' component={HomeScreen} />
<Tab.Screen name='Settings' component={SettingsScreen} />
<Tab.Screen name='Settings' component={NewSettingsScreen} />
</Tab.Navigator>
);
}
@ -71,7 +72,7 @@ const AppNavigator = observer(() => {
return (
<NavigationContainer theme={theme}>
<Stack.Navigator
initialRouteName={(rootStore.serverStore.servers.length > 0) ? 'Main' : 'AddServer'}
initialRouteName={(rootStore.serverStore.servers?.length > 0) ? 'Main' : 'AddServer'}
headerMode='screen'
screenOptions={{ headerShown: false }}
>
@ -95,7 +96,7 @@ const AppNavigator = observer(() => {
name='AddServer'
component={AddServerScreen}
options={{
headerShown: rootStore.serverStore.servers.length > 0,
headerShown: rootStore.serverStore.servers?.length > 0,
title: 'Add Server'
}}
/>

View File

@ -26,6 +26,7 @@
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"mobx-sync": "^3.0.0",
"mobx-task": "^2.0.1",
"prop-types": "^15.7.2",
"react": "16.9.0",
"react-lifecycles-compat": "^3.0.4",

View File

@ -18,7 +18,6 @@ import PropTypes from 'prop-types';
import { useStores } from '../hooks/useStores';
import Colors from '../constants/Colors';
import { getAppName, getSafeDeviceName } from '../utils/Device';
import JellyfinValidator from '../utils/JellyfinValidator';
import NativeShell from '../utils/NativeShell';
import { openBrowser } from '../utils/WebBrowser';
@ -38,8 +37,6 @@ true;
@observer
class HomeScreen extends React.Component {
state = {
server: null,
serverUrl: null,
isError: false,
isFullscreen: false,
isLoading: true,
@ -52,29 +49,6 @@ class HomeScreen extends React.Component {
rootStore: PropTypes.object.isRequired
}
async bootstrapAsync() {
const servers = this.props.rootStore.serverStore.servers;
let activeServer = this.props.rootStore.settingStore.activeServer;
// If the activeServer is greater than the length of the server array, reset it to 0
if (activeServer && servers.length && activeServer > servers.length - 1) {
this.props.rootStore.settingStore.activeServer = 0;
activeServer = 0;
}
let server;
if (servers.length > 0) {
server = servers[activeServer];
}
const serverUrl = JellyfinValidator.getServerUrl(server);
this.setState({
server,
serverUrl
});
}
getErrorView() {
return (
<View style={styles.container}>
@ -124,19 +98,19 @@ class HomeScreen extends React.Component {
deactivateKeepAwake();
break;
case 'console.debug':
console.debug('[Browser Console]', data);
// console.debug('[Browser Console]', data);
break;
case 'console.error':
console.error('[Browser Console]', data);
// console.error('[Browser Console]', data);
break;
case 'console.info':
console.info('[Browser Console]', data);
// console.info('[Browser Console]', data);
break;
case 'console.log':
console.log('[Browser Console]', data);
// console.log('[Browser Console]', data);
break;
case 'console.warn':
console.warn('[Browser Console]', data);
// console.warn('[Browser Console]', data);
break;
default:
console.debug('[HomeScreen.onMessage]', event, data);
@ -183,15 +157,9 @@ class HomeScreen extends React.Component {
this.onGoHome();
}
});
// Bootstrap component state
this.bootstrapAsync();
}
componentDidUpdate(prevProps, prevState) {
if (typeof this.props.route.params?.activeServer != 'undefined' &&
prevProps.route.params?.activeServer !== this.props.route.params?.activeServer) {
this.bootstrapAsync();
}
if (prevState.isFullscreen !== this.state.isFullscreen) {
// Update the screen orientation
this.updateScreenOrientation();
@ -210,6 +178,11 @@ class HomeScreen extends React.Component {
// 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={safeAreaStyle} >
{!this.state.isFullscreen && (
@ -228,10 +201,10 @@ class HomeScreen extends React.Component {
) : null
}
>
{this.state.serverUrl && (
{server && server.urlString && (
<WebView
ref={ref => (this.webview = ref)}
source={{ uri: this.state.serverUrl }}
source={{ uri: server.urlString }}
style={webviewStyle}
// Inject javascript for NativeShell
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}

View File

@ -0,0 +1,168 @@
/**
* 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 } from 'react';
import { Alert, AsyncStorage, Platform, SectionList, StyleSheet } from 'react-native';
import { colors, Text } from 'react-native-elements';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { observer } from 'mobx-react';
import AppInfoFooter from '../components/AppInfoFooter';
import BrowserListItem from '../components/BrowserListItem';
import ButtonListItem from '../components/ButtonListItem';
import ServerListItem from '../components/ServerListItem';
import Colors from '../constants/Colors';
import Links from '../constants/Links';
import { useStores } from '../hooks/useStores';
const NewSettingsScreen = observer(() => {
const { rootStore } = useStores();
const navigation = useNavigation();
useEffect(() => {
// Fetch server info
rootStore.serverStore.fetchInfo();
}, []);
const onAddServer = () => {
navigation.navigate('AddServer');
};
const onDeleteServer = index => {
Alert.alert(
'Delete Server',
'Are you sure you want to delete this server?',
[
{ text: 'Cancel' },
{
text: 'Delete',
onPress: () => {
// Remove server and update active server
rootStore.serverStore.removeServer(index);
rootStore.settingStore.activeServer = 0;
if (rootStore.serverStore.servers.length > 0) {
// More servers exist, navigate home
navigation.navigate('Home');
} else {
// No servers are present, navigate to add server screen
navigation.replace('AddServer');
}
},
style: 'destructive'
}
]
);
};
const onSelectServer = index => {
rootStore.settingStore.activeServer = index;
navigation.navigate('Home');
};
const onResetApplication = () => {
Alert.alert(
'Reset Application',
'Are you sure you want to reset all settings?',
[
{ text: 'Cancel' },
{
text: 'Reset',
onPress: () => {
// Reset data in stores
rootStore.reset();
AsyncStorage.clear();
// Navigate to the loading screen
navigation.replace('AddServer');
},
style: 'destructive'
}
]
);
};
const AugmentedServerListItem = (props) => (
<ServerListItem
{...props}
activeServer={rootStore.settingStore.activeServer}
onDelete={onDeleteServer}
onPress={onSelectServer}
/>
);
const getSections = () => {
return [
{
title: 'Servers',
data: rootStore.serverStore.servers.slice(),
keyExtractor: (item, index) => `server-${index}`,
renderItem: AugmentedServerListItem
},
{
title: 'Add Server',
hideHeader: true,
data: [{
key: 'add-server-button',
title: 'Add Server',
onPress: onAddServer
}],
renderItem: ButtonListItem
},
{
title: 'Links',
data: Links,
renderItem: BrowserListItem
},
{
title: 'Reset Application',
hideHeader: true,
data: [{
key: 'reset-app-button',
title: 'Reset Application',
buttonStyle: {
backgroundColor: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error
},
onPress: onResetApplication
}],
renderItem: ButtonListItem
}
];
};
return (
<SafeAreaView style={{...styles.container, paddingTop: 0}} >
<SectionList
sections={getSections()}
extraData={{
activeServer: rootStore.settingStore.activeServer,
isFetching: rootStore.serverStore.fetchInfo.pending
}}
renderItem={({ item }) => <Text>{JSON.stringify(item)}</Text>}
renderSectionHeader={({ section: { title, hideHeader } }) => hideHeader ? null : <Text style={styles.header}>{title}</Text>}
ListFooterComponent={AppInfoFooter}
showsVerticalScrollIndicator={false}
/>
</SafeAreaView>
);
});
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.backgroundColor
},
header: {
backgroundColor: Colors.backgroundColor,
color: colors.grey4,
fontSize: 17,
fontWeight: '600',
paddingVertical: 8,
paddingHorizontal: 15,
marginBottom: 1
}
});
export default NewSettingsScreen;

View File

@ -11,6 +11,12 @@ import SettingStore from "./SettingStore";
export default class RootStore {
@ignore
storeLoaded = false
serverStore = new ServerStore()
settingStore = new SettingStore()
reset() {
this.serverStore.reset();
this.settingStore.reset();
}
}

View File

@ -4,18 +4,35 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { action, observable } from 'mobx';
import { format } from 'mobx-sync';
import { task } from 'mobx-task';
import ServerModel from '../models/ServerModel';
export default class ServerStore {
@format(data => data.map(value => new ServerModel(value.id, value.url, value.info)))
@observable
servers = []
@action
addServer(server) {
this.servers.push(server);
this.servers.push(new ServerModel(this.servers.length, server.url));
}
@action
removeServer(index) {
this.servers.splice(index, 1);
}
@action
reset() {
this.servers = [];
}
@task
async fetchInfo() {
await Promise.all(
this.servers.map(server => server.fetchInfo())
);
}
}

View File

@ -3,7 +3,7 @@
* 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 { observable } from 'mobx';
import { action, observable } from 'mobx';
/**
* Data store for application settings
@ -11,4 +11,9 @@ import { observable } from 'mobx';
export default class SettingStore {
@observable
activeServer = 0
@action
reset() {
this.activeServer = 0;
}
}

13
utils/Icons.js Normal file
View File

@ -0,0 +1,13 @@
/**
* 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 { Platform } from 'react-native';
export const getIconName = (name = '') => {
if (name) {
return Platform.OS === 'ios' ? `ios-${name}` : `md-${name}`;
}
return name;
};

View File

@ -35,7 +35,7 @@ export default class JellyfinValidator {
}
static async fetchServerInfo(server = {}) {
const serverUrl = this.getServerUrl(server);
const serverUrl = server.urlString || this.getServerUrl(server);
const infoUrl = `${serverUrl}system/info/public`;
console.log('info url', infoUrl);
@ -61,7 +61,7 @@ export default class JellyfinValidator {
static getServerUrl(server = {}) {
if (!server || !server.url || !server.url.href) {
throw new Error(`Cannot get server url for invalid server ${server}`);
throw new Error('Cannot get server url for invalid server', server);
}
// Strip the query string or hash if present

View File

@ -9780,6 +9780,13 @@ mobx-sync@^3.0.0:
dependencies:
tslib "^2.0.0"
mobx-task@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mobx-task/-/mobx-task-2.0.1.tgz#bd9e335fd2f2cb6738eb6d83cae222d3b41c9d29"
integrity sha512-CWAqDYfNi6fKvdaPCO/qbns1VHKVF/yX5MezysieOwld4l+B77XFCDrP+E/W6W46gclPnyVRWllJ0fDYwC7S/g==
dependencies:
tslib "^1.9.3"
mobx@^5.15.4:
version "5.15.4"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab"