mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2024-11-27 00:00:26 +00:00
Initial commit
This commit is contained in:
commit
d2edb05b08
32
.eslintrc.json
Normal file
32
.eslintrc.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"eslint.enable": true
|
||||
}
|
1
.watchmanconfig
Normal file
1
.watchmanconfig
Normal file
@ -0,0 +1 @@
|
||||
{}
|
73
App.js
Normal file
73
App.js
Normal file
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { Platform, StatusBar, StyleSheet, View } from 'react-native';
|
||||
import { AppLoading } from 'expo';
|
||||
import { Asset } from 'expo-asset';
|
||||
import Constants from 'expo-constants';
|
||||
import * as Font from 'expo-font';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
import AppNavigator from './navigation/AppNavigator';
|
||||
import Colors from './constants/Colors'
|
||||
|
||||
export default class App extends React.Component {
|
||||
state = {
|
||||
isLoadingComplete: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
|
||||
return (
|
||||
<AppLoading
|
||||
startAsync={this._loadResourcesAsync}
|
||||
onError={this._handleLoadingError}
|
||||
onFinish={this._handleFinishLoading}
|
||||
autoHideSplash={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{Platform.OS === 'ios' && <StatusBar barStyle="light-content" />}
|
||||
<AppNavigator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_loadImagesAsync = () => {
|
||||
const images = [
|
||||
require('./assets/images/splash.png'),
|
||||
require('./assets/images/logowhite.png')
|
||||
];
|
||||
return images.map(image => Asset.fromModule(image).downloadAsync());
|
||||
}
|
||||
|
||||
_loadResourcesAsync = async () => {
|
||||
return Promise.all([
|
||||
Font.loadAsync({
|
||||
// This is the font that we are using for our tab bar
|
||||
...Ionicons.font
|
||||
}),
|
||||
...this._loadImagesAsync()
|
||||
]);
|
||||
};
|
||||
|
||||
_handleLoadingError = error => {
|
||||
// In this case, you might want to report the error to your error
|
||||
// reporting service, for example Sentry
|
||||
console.warn(error);
|
||||
};
|
||||
|
||||
_handleFinishLoading = () => {
|
||||
this.setState({ isLoadingComplete: true });
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor,
|
||||
// Padding for the StatusBar
|
||||
paddingTop: Constants.statusBarHeight || 0
|
||||
}
|
||||
});
|
22
__tests__/App-test.js
Normal file
22
__tests__/App-test.js
Normal file
@ -0,0 +1,22 @@
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import App from '../App';
|
||||
import renderer from 'react-test-renderer';
|
||||
import NavigationTestUtils from 'react-navigation/NavigationTestUtils';
|
||||
|
||||
describe('App snapshot', () => {
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
NavigationTestUtils.resetInternalState();
|
||||
});
|
||||
|
||||
it('renders the loading screen', async () => {
|
||||
const tree = renderer.create(<App />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the root without loading screen', async () => {
|
||||
const tree = renderer.create(<App skipLoadingScreen />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
30
app.json
Normal file
30
app.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Jellyfin",
|
||||
"slug": "jellyfin-expo",
|
||||
"privacy": "unlisted",
|
||||
"sdkVersion": "34.0.0",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"primaryColor": "#00a4dc",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#101010"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
}
|
||||
}
|
||||
}
|
BIN
assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
assets/fonts/SpaceMono-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/images/icon.png
Normal file
BIN
assets/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/images/logowhite.png
Normal file
BIN
assets/images/logowhite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
assets/images/splash.png
Executable file
BIN
assets/images/splash.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
6
babel.config.js
Normal file
6
babel.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
17
components/TabBarIcon.js
Normal file
17
components/TabBarIcon.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
|
||||
export default class TabBarIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Ionicons
|
||||
name={this.props.name}
|
||||
size={26}
|
||||
style={{ marginBottom: -3 }}
|
||||
color={this.props.focused ? Colors.tabIconSelected : Colors.tabIconDefault}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
18
constants/Colors.js
Normal file
18
constants/Colors.js
Normal file
@ -0,0 +1,18 @@
|
||||
const backgroundColor = '#101010';
|
||||
const headerTintColor = '#fff';
|
||||
const tintColor = '#00a4dc';
|
||||
|
||||
export default {
|
||||
backgroundColor,
|
||||
tintColor,
|
||||
headerTintColor,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColor,
|
||||
tabBar: '#fefefe',
|
||||
errorBackground: 'red',
|
||||
errorText: '#fff',
|
||||
warningBackground: '#EAEB5E',
|
||||
warningText: '#666804',
|
||||
noticeBackground: tintColor,
|
||||
noticeText: '#fff',
|
||||
};
|
12
constants/Layout.js
Normal file
12
constants/Layout.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { Dimensions } from 'react-native';
|
||||
|
||||
const width = Dimensions.get('window').width;
|
||||
const height = Dimensions.get('window').height;
|
||||
|
||||
export default {
|
||||
window: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
isSmallDevice: width < 375,
|
||||
};
|
5
constants/Storage.js
Normal file
5
constants/Storage.js
Normal file
@ -0,0 +1,5 @@
|
||||
const prefix = 'org.jellyfin.expo';
|
||||
|
||||
export default {
|
||||
Servers: `${prefix}:Servers`
|
||||
}
|
14
navigation/AppNavigator.js
Normal file
14
navigation/AppNavigator.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
|
||||
|
||||
import MainTabNavigator from './MainTabNavigator';
|
||||
import AddServerScreen from '../screens/AddServerScreen';
|
||||
import ServerLoadingScreen from '../screens/ServerLoadingScreen';
|
||||
|
||||
export default createAppContainer(createSwitchNavigator({
|
||||
ServerLoading: ServerLoadingScreen,
|
||||
AddServer: AddServerScreen,
|
||||
Main: MainTabNavigator
|
||||
}, {
|
||||
initialRouteName: 'ServerLoading'
|
||||
}));
|
72
navigation/MainTabNavigator.js
Normal file
72
navigation/MainTabNavigator.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { createStackNavigator, createBottomTabNavigator } from 'react-navigation';
|
||||
|
||||
import Colors from '../constants/Colors'
|
||||
import TabBarIcon from '../components/TabBarIcon';
|
||||
import HomeScreen from '../screens/HomeScreen';
|
||||
import SettingsScreen from '../screens/SettingsScreen';
|
||||
|
||||
const defaultNavigationOptions = {
|
||||
headerStyle: {
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
headerTintColor: Colors.headerTintColor
|
||||
}
|
||||
|
||||
const HomeStack = createStackNavigator({
|
||||
Home: HomeScreen,
|
||||
}, {
|
||||
defaultNavigationOptions
|
||||
});
|
||||
|
||||
HomeStack.navigationOptions = {
|
||||
tabBarLabel: 'Home',
|
||||
// eslint-disable-next-line react/display-name
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarIcon
|
||||
focused={focused}
|
||||
name={
|
||||
Platform.OS === 'ios'
|
||||
? 'ios-tv'
|
||||
: 'md-tv'
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const SettingsStack = createStackNavigator({
|
||||
Settings: SettingsScreen,
|
||||
}, {
|
||||
defaultNavigationOptions
|
||||
});
|
||||
|
||||
SettingsStack.navigationOptions = {
|
||||
tabBarLabel: 'Settings',
|
||||
// eslint-disable-next-line react/display-name
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarIcon
|
||||
focused={focused}
|
||||
name={
|
||||
Platform.OS === 'ios'
|
||||
? 'ios-options'
|
||||
: 'md-options'
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export default createBottomTabNavigator({
|
||||
HomeStack,
|
||||
SettingsStack,
|
||||
}, {
|
||||
tabBarOptions: {
|
||||
activeTintColor: Colors.tabIconSelected,
|
||||
inactiveTintColor: Colors.tabIconDefault,
|
||||
style: {
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
// Force toolbar label to be under the icon
|
||||
adaptive: false
|
||||
}
|
||||
});
|
21210
package-lock.json
generated
Normal file
21210
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"eject": "expo eject",
|
||||
"test": "node ./node_modules/jest/bin/jest.js --watchAll"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^34.0.1",
|
||||
"expo-asset": "^6.0.0",
|
||||
"expo-constants": "^6.0.0",
|
||||
"expo-font": "^6.0.1",
|
||||
"react": "16.8.3",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-34.0.0.tar.gz",
|
||||
"react-native-gesture-handler": "^1.3.0",
|
||||
"react-native-reanimated": "^1.1.0",
|
||||
"react-navigation": "^3.0.9",
|
||||
"url-parse": "^1.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-preset-expo": "^5.0.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-plugin-react": "^7.14.3",
|
||||
"expo-cli": "^3.0.9",
|
||||
"jest-expo": "^32.0.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
119
screens/AddServerScreen.js
Normal file
119
screens/AddServerScreen.js
Normal file
@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Button, Image, Platform, StyleSheet, TextInput, View } from 'react-native';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
import StorageKeys from '../constants/Storage';
|
||||
import CachingStorage from '../utils/CachingStorage';
|
||||
import JellyfinValidator from '../utils/JellyfinValidator';
|
||||
|
||||
const DEFAULT_PORT = 8096;
|
||||
|
||||
const sanitizeHost = (url = '') => url.trim();
|
||||
const sanitizePort = (port = '') => {
|
||||
if (port === '') {
|
||||
return '';
|
||||
}
|
||||
port = Number.parseInt(port, 10);
|
||||
if (Number.isNaN(port)) {
|
||||
return DEFAULT_PORT;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
export default class AddServerScreen extends React.Component {
|
||||
state = {
|
||||
host: '',
|
||||
port: `${DEFAULT_PORT}`,
|
||||
isValidating: false
|
||||
}
|
||||
|
||||
async onAddServer() {
|
||||
const { host, port } = this.state;
|
||||
console.log('add server', host, port);
|
||||
if (host && port) {
|
||||
this.setState({ isValidating: true });
|
||||
|
||||
// Parse the entered url
|
||||
const url = JellyfinValidator.parseUrl(host, port);
|
||||
console.log('parsed url', url);
|
||||
|
||||
// Validate the server is available
|
||||
const isServerValid = await JellyfinValidator.validate({ url });
|
||||
console.log(`Server is ${isServerValid ? '' : 'not '}valid`);
|
||||
if (!isServerValid) {
|
||||
this.setState({ isValidating: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the server details to app storage
|
||||
await CachingStorage.getInstance().setItem(StorageKeys.Servers, [{
|
||||
url
|
||||
}]);
|
||||
// Navigate to the main screen
|
||||
this.props.navigation.navigate('Main');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={styles.logoImage}
|
||||
source={require('../assets/images/logowhite.png')}
|
||||
fadeDuration={0} // we need to adjust Android devices (https://facebook.github.io/react-native/docs/image#fadeduration) fadeDuration prop to `0` as it's default value is `300`
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.serverTextInput}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
autoCompleteType='off'
|
||||
autoFocus={true}
|
||||
keyboardType={Platform.OS === 'ios' ? 'url' : 'default'}
|
||||
textContentType='URL'
|
||||
value={this.state.host}
|
||||
onChangeText={text => this.setState({ host: sanitizeHost(text) })}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.serverTextInput}
|
||||
keyboardType='number-pad'
|
||||
maxLength={5}
|
||||
value={this.state.port}
|
||||
onChangeText={text => this.setState({ port: `${sanitizePort(text)}` })}
|
||||
/>
|
||||
<Button
|
||||
title='Add Server'
|
||||
color={Colors.tintColor}
|
||||
onPress={() => this.onAddServer()}
|
||||
/>
|
||||
{this.state.isValidating ? (<ActivityIndicator />) : null}
|
||||
<View style={styles.spacer} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
logoImage: {
|
||||
flex: 1,
|
||||
width: '80%',
|
||||
height: null,
|
||||
resizeMode: 'contain'
|
||||
},
|
||||
serverTextInput: {
|
||||
fontSize: 20,
|
||||
margin: 4,
|
||||
width: '100%',
|
||||
borderRadius: 2,
|
||||
padding: 2,
|
||||
backgroundColor: '#292929',
|
||||
color: '#fff'
|
||||
},
|
||||
spacer: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
62
screens/HomeScreen.js
Normal file
62
screens/HomeScreen.js
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, WebView } from 'react-native';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
import StorageKeys from '../constants/Storage';
|
||||
import CachingStorage from '../utils/CachingStorage';
|
||||
import JellyfinValidator from '../utils/JellyfinValidator';
|
||||
|
||||
// Loading component rendered in webview to avoid flash of white
|
||||
const loading = () => (
|
||||
<View style={styles.container} />
|
||||
);
|
||||
|
||||
export default class HomeScreen extends React.Component {
|
||||
state = {
|
||||
server: null
|
||||
};
|
||||
|
||||
static navigationOptions = {
|
||||
header: null
|
||||
};
|
||||
|
||||
async bootstrapAsync() {
|
||||
let server = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
|
||||
if (server.length > 0) {
|
||||
server = server[0];
|
||||
}
|
||||
this.setState({ server });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.bootstrapAsync();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.server || !this.state.server.url) {
|
||||
return loading();
|
||||
}
|
||||
|
||||
return (
|
||||
<WebView
|
||||
source={{ uri: JellyfinValidator.getServerUrl(this.state.server) }}
|
||||
style={styles.container}
|
||||
// Display loading indicator
|
||||
startInLoadingState={true}
|
||||
renderLoading={loading}
|
||||
// Media playback options to fix video player
|
||||
allowsInlineMediaPlayback={true}
|
||||
mediaPlaybackRequiresUserAction={false}
|
||||
// Use WKWebView on iOS
|
||||
useWebKit={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
}
|
||||
});
|
72
screens/ServerLoadingScreen.js
Normal file
72
screens/ServerLoadingScreen.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Image, StyleSheet, View } from 'react-native';
|
||||
import { SplashScreen } from 'expo';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
import StorageKeys from '../constants/Storage';
|
||||
import CachingStorage from '../utils/CachingStorage';
|
||||
import JellyfinValidator from '../utils/JellyfinValidator';
|
||||
|
||||
export default class ServerLoadingScreen extends React.Component {
|
||||
state = { areResourcesReady: false };
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
SplashScreen.preventAutoHide();
|
||||
}
|
||||
|
||||
async getServers() {
|
||||
return await CachingStorage.getInstance().getItem(StorageKeys.Servers);
|
||||
}
|
||||
|
||||
async bootstrapAsync() {
|
||||
const servers = await this.getServers();
|
||||
const hasServer = !!servers && servers.length > 0;
|
||||
console.info('servers', servers, hasServer);
|
||||
|
||||
if (hasServer) {
|
||||
const activeServer = servers[0];
|
||||
// Validate the server is online and is a Jellyfin server
|
||||
const isServerValid = await JellyfinValidator.validate(activeServer);
|
||||
console.log('active server', activeServer, isServerValid);
|
||||
// TODO: Handle invalid server here
|
||||
}
|
||||
|
||||
// Ensure the splash screen is hidden
|
||||
SplashScreen.hide();
|
||||
// Navigate to the appropriate screen
|
||||
this.props.navigation.navigate(hasServer ? 'Main' : 'AddServer');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.bootstrapAsync();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.areResourcesReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={{ flex: 1, resizeMode: 'contain', width: undefined, height: undefined }}
|
||||
source={require('../assets/images/splash.png')}
|
||||
onLoadEnd={() => {
|
||||
// wait for image's content to fully load [`Image#onLoadEnd`] (https://facebook.github.io/react-native/docs/image#onloadend)
|
||||
SplashScreen.hide();
|
||||
}}
|
||||
fadeDuration={0} // we need to adjust Android devices (https://facebook.github.io/react-native/docs/image#fadeduration) fadeDuration prop to `0` as it's default value is `300`
|
||||
/>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
}
|
||||
});
|
39
screens/SettingsScreen.js
Normal file
39
screens/SettingsScreen.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { AsyncStorage, Button, StyleSheet, View } from 'react-native';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
import StorageKeys from '../constants/Storage';
|
||||
import CachingStorage from '../utils/CachingStorage';
|
||||
|
||||
export default class SettingsScreen extends React.Component {
|
||||
static navigationOptions = {
|
||||
title: 'Settings',
|
||||
};
|
||||
|
||||
async clearStorage() {
|
||||
// Remove all storage items used in the app
|
||||
await AsyncStorage.multiRemove(Object.values(StorageKeys));
|
||||
// Reset the storage cache
|
||||
CachingStorage.instance = null;
|
||||
// Navigate to the loading screen
|
||||
this.props.navigation.navigate('ServerLoading');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button
|
||||
title='Clear Storage'
|
||||
onPress={() => this.clearStorage()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
}
|
||||
});
|
50
utils/CachingStorage.js
Normal file
50
utils/CachingStorage.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { AsyncStorage } from 'react-native';
|
||||
|
||||
export default class CachingStorage {
|
||||
static instance = null;
|
||||
|
||||
cache = new Map();
|
||||
|
||||
static getInstance() {
|
||||
if (this.instance === null) {
|
||||
console.debug('CachingStorage: Initializing new instance');
|
||||
this.instance = new CachingStorage();
|
||||
} else {
|
||||
console.debug('CachingStorage: Using existing instance');
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
async getItem(key) {
|
||||
// Throw an error if no key is provided
|
||||
if (!key) {
|
||||
throw new Error('No key specified for `getItem()`');
|
||||
}
|
||||
|
||||
// Return cached value if present
|
||||
if (this.cache.has(key)) {
|
||||
console.debug(`CachingStorage: Returning value from cache for ${key}`);
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
// Get the item from device storage
|
||||
console.debug(`CachingStorage: Loading value from device storage for ${key}`);
|
||||
let item = await AsyncStorage.getItem(key);
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
this.cache.set(key, item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async setItem(key, item = '') {
|
||||
// Throw an error if no key is provided
|
||||
if (!key) {
|
||||
throw new Error('No key specified for `setItem()`');
|
||||
}
|
||||
|
||||
console.debug(`CachingStorage: Saving value to device storage for ${key}`);
|
||||
await AsyncStorage.setItem(key, JSON.stringify(item));
|
||||
this.cache.set(key, item);
|
||||
}
|
||||
}
|
58
utils/JellyfinValidator.js
Normal file
58
utils/JellyfinValidator.js
Normal file
@ -0,0 +1,58 @@
|
||||
/* globals fetch */
|
||||
import Url from 'url-parse';
|
||||
|
||||
export default class JellyfinValidator {
|
||||
static parseUrl(host = '', port = '') {
|
||||
if (!host) {
|
||||
throw new Error('host cannot be blank')
|
||||
}
|
||||
// Parsing seems to fail if a protocol is not set
|
||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||
host = `http://${host}`;
|
||||
}
|
||||
// Parse the host as a url
|
||||
const url = new Url(host);
|
||||
// Override the port if provided
|
||||
if (port) {
|
||||
url.set('port', port);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
static getServerUrl(server = {}) {
|
||||
if (!server || !server.url || !server.url.origin) {
|
||||
throw new Error(`Cannot get server url for invalid server ${server}`)
|
||||
}
|
||||
return `${server.url.origin}${server.url.pathname}`;
|
||||
}
|
||||
|
||||
static async validate(server = {}) {
|
||||
// Does the server have a valid url?
|
||||
if (!server.url || !server.url.origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const serverUrl = this.getServerUrl(server);
|
||||
const infoUrl = `${serverUrl}/system/info/public`;
|
||||
console.log('info url', infoUrl);
|
||||
// Try to fetch the server's public info
|
||||
try {
|
||||
const response = await fetch(infoUrl);
|
||||
const responseJson = await response.json();
|
||||
console.log('response', responseJson);
|
||||
|
||||
// Versions prior to 10.3.x do not include ProductName so return true if response includes Version < 10.3.x
|
||||
if (responseJson.Version) {
|
||||
const versionNumber = responseJson.Version.split('.').map(num => Number.parseInt(num, 10));
|
||||
if (versionNumber.length === 3 && versionNumber[0] === 10 && versionNumber[1] < 3) {
|
||||
console.log('Is valid old version');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return responseJson.ProductName === 'Jellyfin Server';
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user