mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2024-11-27 00:00:26 +00:00
Fix eslint errors
This commit is contained in:
parent
f3a3647c79
commit
7c4c7329be
156
App.js
156
App.js
@ -30,99 +30,99 @@ import Theme from './utils/Theme';
|
||||
import './i18n';
|
||||
|
||||
const App = observer(({ skipLoadingScreen }) => {
|
||||
const [isSplashReady, setIsSplashReady] = useState(false);
|
||||
const { rootStore } = useStores();
|
||||
const [isSplashReady, setIsSplashReady] = useState(false);
|
||||
const { rootStore } = useStores();
|
||||
|
||||
const trunk = new AsyncTrunk(rootStore, {
|
||||
storage: AsyncStorage
|
||||
});
|
||||
const trunk = new AsyncTrunk(rootStore, {
|
||||
storage: AsyncStorage
|
||||
});
|
||||
|
||||
const hydrateStores = async () => {
|
||||
// Migrate servers and settings
|
||||
// TODO: Remove this for next release
|
||||
const servers = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
|
||||
if (servers) {
|
||||
const activeServer = await CachingStorage.getInstance().getItem(StorageKeys.ActiveServer) || 0;
|
||||
const hydrateStores = async () => {
|
||||
// Migrate servers and settings
|
||||
// TODO: Remove this for next release
|
||||
const servers = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
|
||||
if (servers) {
|
||||
const activeServer = await CachingStorage.getInstance().getItem(StorageKeys.ActiveServer) || 0;
|
||||
|
||||
// Initialize the store with the existing servers and settings
|
||||
await trunk.init({
|
||||
serverStore: { servers },
|
||||
settingStore: { activeServer }
|
||||
});
|
||||
// Initialize the store with the existing servers and settings
|
||||
await trunk.init({
|
||||
serverStore: { servers },
|
||||
settingStore: { activeServer }
|
||||
});
|
||||
|
||||
// Remove old data values
|
||||
AsyncStorage.multiRemove(Object.values(StorageKeys));
|
||||
} else {
|
||||
// No servers saved in the old method, initialize normally
|
||||
await trunk.init();
|
||||
}
|
||||
// Remove old data values
|
||||
AsyncStorage.multiRemove(Object.values(StorageKeys));
|
||||
} else {
|
||||
// No servers saved in the old method, initialize normally
|
||||
await trunk.init();
|
||||
}
|
||||
|
||||
rootStore.storeLoaded = true;
|
||||
rootStore.storeLoaded = true;
|
||||
|
||||
if (typeof rootStore.settingStore.isRotationEnabled === 'undefined') {
|
||||
rootStore.settingStore.isRotationEnabled = Platform.OS === 'ios' && !Platform.isPad;
|
||||
console.info('Initializing rotation lock setting', rootStore.settingStore.isRotationEnabled);
|
||||
}
|
||||
};
|
||||
if (typeof rootStore.settingStore.isRotationEnabled === 'undefined') {
|
||||
rootStore.settingStore.isRotationEnabled = Platform.OS === 'ios' && !Platform.isPad;
|
||||
console.info('Initializing rotation lock setting', rootStore.settingStore.isRotationEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Hydrate mobx data stores
|
||||
hydrateStores();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// Hydrate mobx data stores
|
||||
hydrateStores();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.info('rotation lock setting changed!', rootStore.settingStore.isRotationEnabled);
|
||||
if (rootStore.settingStore.isRotationEnabled) {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
||||
} else {
|
||||
ScreenOrientation.unlockAsync();
|
||||
}
|
||||
}, [rootStore.settingStore.isRotationEnabled]);
|
||||
useEffect(() => {
|
||||
console.info('rotation lock setting changed!', rootStore.settingStore.isRotationEnabled);
|
||||
if (rootStore.settingStore.isRotationEnabled) {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
||||
} else {
|
||||
ScreenOrientation.unlockAsync();
|
||||
}
|
||||
}, [rootStore.settingStore.isRotationEnabled]);
|
||||
|
||||
const loadImagesAsync = () => {
|
||||
const images = [
|
||||
require('./assets/images/splash.png'),
|
||||
require('./assets/images/logowhite.png')
|
||||
];
|
||||
return images.map(image => Asset.fromModule(image).downloadAsync());
|
||||
};
|
||||
const loadImagesAsync = () => {
|
||||
const images = [
|
||||
require('./assets/images/splash.png'),
|
||||
require('./assets/images/logowhite.png')
|
||||
];
|
||||
return images.map(image => Asset.fromModule(image).downloadAsync());
|
||||
};
|
||||
|
||||
const loadResourcesAsync = async () => {
|
||||
return Promise.all([
|
||||
Font.loadAsync({
|
||||
// This is the font that we are using for our tab bar
|
||||
...Ionicons.font
|
||||
}),
|
||||
...loadImagesAsync()
|
||||
]);
|
||||
};
|
||||
const loadResourcesAsync = async () => {
|
||||
return Promise.all([
|
||||
Font.loadAsync({
|
||||
// This is the font that we are using for our tab bar
|
||||
...Ionicons.font
|
||||
}),
|
||||
...loadImagesAsync()
|
||||
]);
|
||||
};
|
||||
|
||||
if (!isSplashReady && !skipLoadingScreen) {
|
||||
return (
|
||||
<AppLoading
|
||||
startAsync={loadResourcesAsync}
|
||||
onError={console.warn}
|
||||
onFinish={() => setIsSplashReady(true)}
|
||||
autoHideSplash={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!isSplashReady && !skipLoadingScreen) {
|
||||
return (
|
||||
<AppLoading
|
||||
startAsync={loadResourcesAsync}
|
||||
onError={console.warn}
|
||||
onFinish={() => setIsSplashReady(true)}
|
||||
autoHideSplash={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
backgroundColor={Colors.headerBackgroundColor}
|
||||
/>
|
||||
<AppNavigator />
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
backgroundColor={Colors.headerBackgroundColor}
|
||||
/>
|
||||
<AppNavigator />
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
});
|
||||
|
||||
App.propTypes = {
|
||||
skipLoadingScreen: PropTypes.bool
|
||||
skipLoadingScreen: PropTypes.bool
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -4,8 +4,8 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo']
|
||||
};
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo']
|
||||
};
|
||||
};
|
||||
|
@ -12,25 +12,25 @@ import { useTranslation } from 'react-i18next';
|
||||
import { getAppName } from '../utils/Device';
|
||||
|
||||
const AppInfoFooter = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>{`${getAppName()}`}</Text>
|
||||
<Text style={styles.text}>{`${Constants.nativeAppVersion} (${Constants.nativeBuildVersion})`}</Text>
|
||||
<Text style={styles.text}>{t('settings.expoVersion', { version: Constants.expoVersion })}</Text>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>{`${getAppName()}`}</Text>
|
||||
<Text style={styles.text}>{`${Constants.nativeAppVersion} (${Constants.nativeBuildVersion})`}</Text>
|
||||
<Text style={styles.text}>{t('settings.expoVersion', { version: Constants.expoVersion })}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 15
|
||||
},
|
||||
text: {
|
||||
color: colors.grey4,
|
||||
fontSize: 15
|
||||
}
|
||||
container: {
|
||||
margin: 15
|
||||
},
|
||||
text: {
|
||||
color: colors.grey4,
|
||||
fontSize: 15
|
||||
}
|
||||
});
|
||||
|
||||
export default AppInfoFooter;
|
||||
|
@ -10,25 +10,25 @@ 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);
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
item: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default BrowserListItem;
|
||||
|
@ -9,30 +9,30 @@ import { Button, ListItem } from 'react-native-elements';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ButtonListItem = ({item, index}) => (
|
||||
<ListItem
|
||||
title={
|
||||
<Button
|
||||
{...item}
|
||||
type='clear'
|
||||
buttonStyle={{ ...styles.button, ...item.buttonStyle }}
|
||||
titleStyle={{ ...styles.title, ...item.titleStyle }}
|
||||
/>
|
||||
}
|
||||
topDivider={index === 0}
|
||||
bottomDivider
|
||||
/>
|
||||
<ListItem
|
||||
title={
|
||||
<Button
|
||||
{...item}
|
||||
type='clear'
|
||||
buttonStyle={{ ...styles.button, ...item.buttonStyle }}
|
||||
titleStyle={{ ...styles.title, ...item.titleStyle }}
|
||||
/>
|
||||
}
|
||||
topDivider={index === 0}
|
||||
bottomDivider
|
||||
/>
|
||||
);
|
||||
|
||||
ButtonListItem.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
item: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
justifyContent: 'flex-start',
|
||||
padding: 0
|
||||
}
|
||||
button: {
|
||||
justifyContent: 'flex-start',
|
||||
padding: 0
|
||||
}
|
||||
});
|
||||
|
||||
export default ButtonListItem;
|
||||
|
@ -18,127 +18,127 @@ import JellyfinValidator from '../utils/JellyfinValidator';
|
||||
const sanitizeHost = (url = '') => url.trim();
|
||||
|
||||
const ServerInput = observer(class ServerInput extends React.Component {
|
||||
static propTypes = {
|
||||
navigation: PropTypes.object.isRequired,
|
||||
rootStore: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func,
|
||||
successScreen: PropTypes.string
|
||||
}
|
||||
static propTypes = {
|
||||
navigation: PropTypes.object.isRequired,
|
||||
rootStore: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func,
|
||||
successScreen: PropTypes.string
|
||||
}
|
||||
|
||||
state = {
|
||||
host: '',
|
||||
isValidating: false,
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
}
|
||||
state = {
|
||||
host: '',
|
||||
isValidating: false,
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
}
|
||||
|
||||
async onAddServer() {
|
||||
const { host } = this.state;
|
||||
console.log('add server', host);
|
||||
if (host) {
|
||||
this.setState({
|
||||
isValidating: true,
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
});
|
||||
async onAddServer() {
|
||||
const { host } = this.state;
|
||||
console.log('add server', host);
|
||||
if (host) {
|
||||
this.setState({
|
||||
isValidating: true,
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
});
|
||||
|
||||
// Parse the entered url
|
||||
let url;
|
||||
try {
|
||||
url = JellyfinValidator.parseUrl(host);
|
||||
console.log('parsed url', url);
|
||||
} catch (err) {
|
||||
console.info(err);
|
||||
this.setState({
|
||||
isValidating: false,
|
||||
isValid: false,
|
||||
validationMessage: this.props.t('addServer.validation.invalid')
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Parse the entered url
|
||||
let url;
|
||||
try {
|
||||
url = JellyfinValidator.parseUrl(host);
|
||||
console.log('parsed url', url);
|
||||
} catch (err) {
|
||||
console.info(err);
|
||||
this.setState({
|
||||
isValidating: false,
|
||||
isValid: false,
|
||||
validationMessage: this.props.t('addServer.validation.invalid')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the server is available
|
||||
const validation = await JellyfinValidator.validate({ url });
|
||||
console.log(`Server is ${validation.isValid ? '' : 'not '}valid`);
|
||||
if (!validation.isValid) {
|
||||
const message = validation.message || 'invalid';
|
||||
this.setState({
|
||||
isValidating: false,
|
||||
isValid: validation.isValid,
|
||||
validationMessage: this.props.t([`addServer.validation.${message}`, 'addServer.validation.invalid'])
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate the server is available
|
||||
const validation = await JellyfinValidator.validate({ url });
|
||||
console.log(`Server is ${validation.isValid ? '' : 'not '}valid`);
|
||||
if (!validation.isValid) {
|
||||
const message = validation.message || 'invalid';
|
||||
this.setState({
|
||||
isValidating: false,
|
||||
isValid: validation.isValid,
|
||||
validationMessage: this.props.t([`addServer.validation.${message}`, 'addServer.validation.invalid'])
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the server details
|
||||
this.props.rootStore.serverStore.addServer({ url });
|
||||
this.props.rootStore.settingStore.activeServer = this.props.rootStore.serverStore.servers.length - 1;
|
||||
// Call the success callback if present
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess();
|
||||
}
|
||||
// Navigate to the main screen
|
||||
this.props.navigation.replace(
|
||||
'Main',
|
||||
{
|
||||
screen: this.props.successScreen || 'Home',
|
||||
params: { activeServer: this.props.rootStore.settingStore.activeServer }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({
|
||||
isValid: false,
|
||||
validationMessage: this.props.t('addServer.validation.empty')
|
||||
});
|
||||
}
|
||||
}
|
||||
// Save the server details
|
||||
this.props.rootStore.serverStore.addServer({ url });
|
||||
this.props.rootStore.settingStore.activeServer = this.props.rootStore.serverStore.servers.length - 1;
|
||||
// Call the success callback if present
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess();
|
||||
}
|
||||
// Navigate to the main screen
|
||||
this.props.navigation.replace(
|
||||
'Main',
|
||||
{
|
||||
screen: this.props.successScreen || 'Home',
|
||||
params: { activeServer: this.props.rootStore.settingStore.activeServer }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({
|
||||
isValid: false,
|
||||
validationMessage: this.props.t('addServer.validation.empty')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Input
|
||||
inputContainerStyle={styles.inputContainerStyle}
|
||||
leftIcon={{
|
||||
name: getIconName('globe'),
|
||||
type: 'ionicon'
|
||||
}}
|
||||
labelStyle={{
|
||||
color: colors.grey4
|
||||
}}
|
||||
placeholderTextColor={colors.grey3}
|
||||
rightIcon={this.state.isValidating ? <ActivityIndicator /> : null}
|
||||
selectionColor={Colors.tintColor}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
autoCompleteType='off'
|
||||
autoFocus={true}
|
||||
keyboardType={Platform.OS === 'ios' ? 'url' : 'default'}
|
||||
returnKeyType='go'
|
||||
textContentType='URL'
|
||||
editable={!this.state.isValidating}
|
||||
value={this.state.host}
|
||||
errorMessage={this.state.isValid ? null : this.state.validationMessage}
|
||||
onChangeText={text => this.setState({ host: sanitizeHost(text) })}
|
||||
onSubmitEditing={() => this.onAddServer()}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Input
|
||||
inputContainerStyle={styles.inputContainerStyle}
|
||||
leftIcon={{
|
||||
name: getIconName('globe'),
|
||||
type: 'ionicon'
|
||||
}}
|
||||
labelStyle={{
|
||||
color: colors.grey4
|
||||
}}
|
||||
placeholderTextColor={colors.grey3}
|
||||
rightIcon={this.state.isValidating ? <ActivityIndicator /> : null}
|
||||
selectionColor={Colors.tintColor}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
autoCompleteType='off'
|
||||
autoFocus={true}
|
||||
keyboardType={Platform.OS === 'ios' ? 'url' : 'default'}
|
||||
returnKeyType='go'
|
||||
textContentType='URL'
|
||||
editable={!this.state.isValidating}
|
||||
value={this.state.host}
|
||||
errorMessage={this.state.isValid ? null : this.state.validationMessage}
|
||||
onChangeText={text => this.setState({ host: sanitizeHost(text) })}
|
||||
onSubmitEditing={() => this.onAddServer()}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inputContainerStyle: {
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#292929',
|
||||
borderBottomWidth: 0
|
||||
}
|
||||
inputContainerStyle: {
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#292929',
|
||||
borderBottomWidth: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Inject the Navigation Hook as a prop to mimic the legacy behavior
|
||||
const ServerInputWithNavigation = observer((props) => {
|
||||
const stores = useStores();
|
||||
return <ServerInput {...props} navigation={useNavigation()} {...stores} />;
|
||||
const stores = useStores();
|
||||
return <ServerInput {...props} navigation={useNavigation()} {...stores} />;
|
||||
});
|
||||
|
||||
export default ServerInputWithNavigation;
|
||||
|
@ -12,65 +12,65 @@ import { useTranslation } from 'react-i18next';
|
||||
import { getIconName } from '../utils/Icons';
|
||||
|
||||
const ServerListItem = ({item, index, activeServer, onDelete, onPress}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = item?.name;
|
||||
const version = item?.info?.Version || t('common.unknown');
|
||||
const subtitle = `${t('settings.version', { version })}\n${item.urlString}`;
|
||||
const title = item?.name;
|
||||
const version = item?.info?.Version || t('common.unknown');
|
||||
const subtitle = `${t('settings.version', { version })}\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)}
|
||||
/>
|
||||
);
|
||||
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
|
||||
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
|
||||
}
|
||||
title: {
|
||||
marginBottom: 2
|
||||
},
|
||||
leftElement: {
|
||||
width: 12
|
||||
},
|
||||
deleteButton: {
|
||||
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error
|
||||
}
|
||||
});
|
||||
|
||||
export default ServerListItem;
|
||||
|
@ -9,23 +9,23 @@ import { ListItem } from 'react-native-elements';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const SwitchListItem = ({item, index}) => (
|
||||
<ListItem
|
||||
title={item.title}
|
||||
subtitle={item.subtitle}
|
||||
rightElement={
|
||||
<Switch
|
||||
value={item.value}
|
||||
onValueChange={item.onValueChange}
|
||||
/>
|
||||
}
|
||||
topDivider={index === 0}
|
||||
bottomDivider
|
||||
/>
|
||||
<ListItem
|
||||
title={item.title}
|
||||
subtitle={item.subtitle}
|
||||
rightElement={
|
||||
<Switch
|
||||
value={item.value}
|
||||
onValueChange={item.onValueChange}
|
||||
/>
|
||||
}
|
||||
topDivider={index === 0}
|
||||
bottomDivider
|
||||
/>
|
||||
);
|
||||
|
||||
SwitchListItem.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
item: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default SwitchListItem;
|
||||
|
@ -8,16 +8,16 @@ const textColor = '#fff';
|
||||
const tintColor = '#00a4dc';
|
||||
|
||||
export default {
|
||||
textColor,
|
||||
backgroundColor,
|
||||
headerBackgroundColor: '#202020',
|
||||
tintColor,
|
||||
headerTintColor: textColor,
|
||||
tabText: '#ccc',
|
||||
errorBackground: 'red',
|
||||
errorText: textColor,
|
||||
warningBackground: '#EAEB5E',
|
||||
warningText: '#666804',
|
||||
noticeBackground: tintColor,
|
||||
noticeText: textColor
|
||||
textColor,
|
||||
backgroundColor,
|
||||
headerBackgroundColor: '#202020',
|
||||
tintColor,
|
||||
headerTintColor: textColor,
|
||||
tabText: '#ccc',
|
||||
errorBackground: 'red',
|
||||
errorText: textColor,
|
||||
warningBackground: '#EAEB5E',
|
||||
warningText: '#666804',
|
||||
noticeBackground: tintColor,
|
||||
noticeText: textColor
|
||||
};
|
||||
|
@ -6,58 +6,58 @@
|
||||
import { getIconName } from '../utils/Icons';
|
||||
|
||||
export default [
|
||||
{
|
||||
key: 'links-website',
|
||||
name: 'links.website',
|
||||
url: 'https://jellyfin.org/',
|
||||
icon: {
|
||||
name: getIconName('globe'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-documentation',
|
||||
name: 'links.documentation',
|
||||
url: 'https://docs.jellyfin.org',
|
||||
icon: {
|
||||
name: getIconName('book'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-source',
|
||||
name: 'links.source',
|
||||
url: 'https://github.com/jellyfin/jellyfin-expo',
|
||||
icon: {
|
||||
name: 'logo-github',
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-translate',
|
||||
name: 'links.translate',
|
||||
url: 'https://translate.jellyfin.org/projects/jellyfin/jellyfin-expo/',
|
||||
icon: {
|
||||
name: 'translate',
|
||||
type: 'material'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-feature',
|
||||
name: 'links.feature',
|
||||
url: 'https://features.jellyfin.org/',
|
||||
icon: {
|
||||
name: getIconName('create'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-issue',
|
||||
name: 'links.issue',
|
||||
url: 'https://github.com/jellyfin/jellyfin-expo/issues',
|
||||
icon: {
|
||||
name: getIconName('bug'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
}
|
||||
{
|
||||
key: 'links-website',
|
||||
name: 'links.website',
|
||||
url: 'https://jellyfin.org/',
|
||||
icon: {
|
||||
name: getIconName('globe'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-documentation',
|
||||
name: 'links.documentation',
|
||||
url: 'https://docs.jellyfin.org',
|
||||
icon: {
|
||||
name: getIconName('book'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-source',
|
||||
name: 'links.source',
|
||||
url: 'https://github.com/jellyfin/jellyfin-expo',
|
||||
icon: {
|
||||
name: 'logo-github',
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-translate',
|
||||
name: 'links.translate',
|
||||
url: 'https://translate.jellyfin.org/projects/jellyfin/jellyfin-expo/',
|
||||
icon: {
|
||||
name: 'translate',
|
||||
type: 'material'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-feature',
|
||||
name: 'links.feature',
|
||||
url: 'https://features.jellyfin.org/',
|
||||
icon: {
|
||||
name: getIconName('create'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'links-issue',
|
||||
name: 'links.issue',
|
||||
url: 'https://github.com/jellyfin/jellyfin-expo/issues',
|
||||
icon: {
|
||||
name: getIconName('bug'),
|
||||
type: 'ionicon'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -6,6 +6,6 @@
|
||||
const prefix = 'org.jellyfin.expo';
|
||||
|
||||
export default {
|
||||
ActiveServer: `${prefix}:ActiveServer`,
|
||||
Servers: `${prefix}:Servers`
|
||||
ActiveServer: `${prefix}:ActiveServer`,
|
||||
Servers: `${prefix}:Servers`
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ import { createContext, useContext } from 'react';
|
||||
import RootStore from '../stores/RootStore';
|
||||
|
||||
export const storesContext = createContext({
|
||||
rootStore: new RootStore()
|
||||
rootStore: new RootStore()
|
||||
});
|
||||
|
||||
export const useStores = () => useContext(storesContext);
|
||||
|
50
i18n.js
50
i18n.js
@ -19,31 +19,31 @@ import zh_Hans from './langs/zh_Hans.json';
|
||||
import zh_Hant from './langs/zh_Hant.json';
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
ar: { translation: ar },
|
||||
cs: { translation: cs },
|
||||
da: { translation: da },
|
||||
de: { translation: de },
|
||||
es: { translation: es },
|
||||
'es-AR': { translation: es_AR },
|
||||
fr: { translation: fr },
|
||||
it: { translation: it },
|
||||
'nb-NO': { translation: nb_NO },
|
||||
sk: { translation: sk },
|
||||
sl: { translation: sl },
|
||||
sv: { translation: sv },
|
||||
'zh-Hans': { translation: zh_Hans },
|
||||
'zh-Hant': { translation: zh_Hant }
|
||||
en: { translation: en },
|
||||
ar: { translation: ar },
|
||||
cs: { translation: cs },
|
||||
da: { translation: da },
|
||||
de: { translation: de },
|
||||
es: { translation: es },
|
||||
'es-AR': { translation: es_AR },
|
||||
fr: { translation: fr },
|
||||
it: { translation: it },
|
||||
'nb-NO': { translation: nb_NO },
|
||||
sk: { translation: sk },
|
||||
sl: { translation: sl },
|
||||
sv: { translation: sv },
|
||||
'zh-Hans': { translation: zh_Hans },
|
||||
'zh-Hant': { translation: zh_Hant }
|
||||
};
|
||||
|
||||
i18next
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
// debug: true,
|
||||
fallbackLng: 'en',
|
||||
lng: Localization.locale,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
resources
|
||||
});
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
// debug: true,
|
||||
fallbackLng: 'en',
|
||||
lng: Localization.locale,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
resources
|
||||
});
|
||||
|
@ -10,56 +10,56 @@ import { task } from 'mobx-task';
|
||||
import JellyfinValidator from '../utils/JellyfinValidator';
|
||||
|
||||
export default class ServerModel {
|
||||
id
|
||||
id
|
||||
|
||||
url
|
||||
url
|
||||
|
||||
online = false
|
||||
online = false
|
||||
|
||||
info
|
||||
info
|
||||
|
||||
constructor(id, url, info) {
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.info = info;
|
||||
constructor(id, url, info) {
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.info = info;
|
||||
|
||||
autorun(() => {
|
||||
this.urlString = this.parseUrlString;
|
||||
});
|
||||
}
|
||||
autorun(() => {
|
||||
this.urlString = this.parseUrlString;
|
||||
});
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.info?.ServerName || this.url?.host;
|
||||
}
|
||||
get name() {
|
||||
return this.info?.ServerName || this.url?.host;
|
||||
}
|
||||
|
||||
get parseUrlString() {
|
||||
try {
|
||||
return JellyfinValidator.getServerUrl(this);
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
get parseUrlString() {
|
||||
try {
|
||||
return JellyfinValidator.getServerUrl(this);
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
fetchInfo = task(async () => {
|
||||
return await JellyfinValidator.fetchServerInfo(this)
|
||||
.then(action(info => {
|
||||
this.online = true;
|
||||
this.info = info;
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
})
|
||||
fetchInfo = task(async () => {
|
||||
return await JellyfinValidator.fetchServerInfo(this)
|
||||
.then(action(info => {
|
||||
this.online = true;
|
||||
this.info = info;
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
decorate(ServerModel, {
|
||||
id: observable,
|
||||
url: observable,
|
||||
online: [
|
||||
ignore,
|
||||
observable
|
||||
],
|
||||
info: observable,
|
||||
name: computed,
|
||||
parseUrlString: computed
|
||||
id: observable,
|
||||
url: observable,
|
||||
online: [
|
||||
ignore,
|
||||
observable
|
||||
],
|
||||
info: observable,
|
||||
name: computed,
|
||||
parseUrlString: computed
|
||||
});
|
||||
|
@ -21,104 +21,104 @@ import { getIconName } from '../utils/Icons';
|
||||
|
||||
// Customize theme for navigator
|
||||
const theme = {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: Colors.tintColor,
|
||||
background: Colors.backgroundColor,
|
||||
card: Colors.headerBackgroundColor,
|
||||
text: Colors.textColor,
|
||||
border: 'transparent'
|
||||
}
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: Colors.tintColor,
|
||||
background: Colors.backgroundColor,
|
||||
card: Colors.headerBackgroundColor,
|
||||
text: Colors.textColor,
|
||||
border: 'transparent'
|
||||
}
|
||||
};
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
function TabIcon(routeName, color, size) {
|
||||
let iconName = null;
|
||||
if (routeName === 'Home') {
|
||||
iconName = getIconName('tv');
|
||||
} else if (routeName === 'Settings') {
|
||||
iconName = getIconName('cog');
|
||||
}
|
||||
let iconName = null;
|
||||
if (routeName === 'Home') {
|
||||
iconName = getIconName('tv');
|
||||
} else if (routeName === 'Settings') {
|
||||
iconName = getIconName('cog');
|
||||
}
|
||||
|
||||
return (
|
||||
iconName ? <Ionicons name={iconName} color={color} size={size} /> : null
|
||||
);
|
||||
return (
|
||||
iconName ? <Ionicons name={iconName} color={color} size={size} /> : null
|
||||
);
|
||||
}
|
||||
|
||||
function Main() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
tabBarIcon: ({ color, size }) => TabIcon(route.name, color, size)
|
||||
})}
|
||||
tabBarOptions={{
|
||||
inactiveTintColor: Colors.tabText
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name='Home'
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: t('headings.home')
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='Settings'
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
title: t('headings.settings')
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
tabBarIcon: ({ color, size }) => TabIcon(route.name, color, size)
|
||||
})}
|
||||
tabBarOptions={{
|
||||
inactiveTintColor: Colors.tabText
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name='Home'
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: t('headings.home')
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='Settings'
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
title: t('headings.settings')
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const AppNavigator = observer(() => {
|
||||
const { rootStore } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { rootStore } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Ensure the splash screen is hidden when loading is finished
|
||||
SplashScreen.hide();
|
||||
// Ensure the splash screen is hidden when loading is finished
|
||||
SplashScreen.hide();
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={theme}>
|
||||
<Stack.Navigator
|
||||
initialRouteName={(rootStore.serverStore.servers?.length > 0) ? 'Main' : 'AddServer'}
|
||||
headerMode='screen'
|
||||
screenOptions={{ headerShown: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Main'
|
||||
component={Main}
|
||||
options={({ route }) => {
|
||||
const routeName = route.state ?
|
||||
// Get the currently active route name in the tab navigator
|
||||
route.state.routes[route.state.index].name :
|
||||
return (
|
||||
<NavigationContainer theme={theme}>
|
||||
<Stack.Navigator
|
||||
initialRouteName={(rootStore.serverStore.servers?.length > 0) ? 'Main' : 'AddServer'}
|
||||
headerMode='screen'
|
||||
screenOptions={{ headerShown: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Main'
|
||||
component={Main}
|
||||
options={({ route }) => {
|
||||
const routeName = route.state ?
|
||||
// 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';
|
||||
return ({
|
||||
headerShown: routeName === 'Settings',
|
||||
title: t(`headings.${routeName.toLowerCase()}`)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='AddServer'
|
||||
component={AddServerScreen}
|
||||
options={{
|
||||
headerShown: rootStore.serverStore.servers?.length > 0,
|
||||
title: t('headings.addServer')
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
return ({
|
||||
headerShown: routeName === 'Settings',
|
||||
title: t(`headings.${routeName.toLowerCase()}`)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='AddServer'
|
||||
component={AddServerScreen}
|
||||
options={{
|
||||
headerShown: rootStore.serverStore.servers?.length > 0,
|
||||
title: t('headings.addServer')
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
});
|
||||
|
||||
export default AppNavigator;
|
||||
|
@ -11,52 +11,52 @@ import ServerInput from '../components/ServerInput';
|
||||
import Colors from '../constants/Colors';
|
||||
|
||||
const AddServerScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.logoContainer}>
|
||||
<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`
|
||||
/>
|
||||
</View>
|
||||
<ServerInput
|
||||
containerStyle={styles.serverTextContainer}
|
||||
label={t('addServer.address')}
|
||||
placeholder='https://jellyfin.org'
|
||||
t={t}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.logoContainer}>
|
||||
<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`
|
||||
/>
|
||||
</View>
|
||||
<ServerInput
|
||||
containerStyle={styles.serverTextContainer}
|
||||
label={t('addServer.address')}
|
||||
placeholder='https://jellyfin.org'
|
||||
t={t}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
serverTextContainer: {
|
||||
flex: 1,
|
||||
alignContent: 'flex-start'
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
logoContainer: {
|
||||
marginTop: 80,
|
||||
marginBottom: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logoImage: {
|
||||
width: '90%',
|
||||
height: undefined,
|
||||
maxWidth: 481,
|
||||
maxHeight: 151,
|
||||
// Aspect ration of the logo
|
||||
aspectRatio: 3.18253
|
||||
}
|
||||
serverTextContainer: {
|
||||
flex: 1,
|
||||
alignContent: 'flex-start'
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
logoContainer: {
|
||||
marginTop: 80,
|
||||
marginBottom: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logoImage: {
|
||||
width: '90%',
|
||||
height: undefined,
|
||||
maxWidth: 481,
|
||||
maxHeight: 151,
|
||||
// Aspect ration of the logo
|
||||
aspectRatio: 3.18253
|
||||
}
|
||||
});
|
||||
|
||||
export default AddServerScreen;
|
||||
|
@ -26,10 +26,10 @@ import { openBrowser } from '../utils/WebBrowser';
|
||||
|
||||
const injectedJavaScript = `
|
||||
window.ExpoAppInfo = {
|
||||
appName: '${getAppName()}',
|
||||
appVersion: '${Constants.nativeAppVersion}',
|
||||
deviceId: '${Constants.deviceId}',
|
||||
deviceName: '${getSafeDeviceName().replace(/'/g, '\\\'')}'
|
||||
appName: '${getAppName()}',
|
||||
appVersion: '${Constants.nativeAppVersion}',
|
||||
deviceId: '${Constants.deviceId}',
|
||||
deviceName: '${getSafeDeviceName().replace(/'/g, '\\\'')}'
|
||||
};
|
||||
|
||||
${NativeShell}
|
||||
@ -38,254 +38,254 @@ true;
|
||||
`;
|
||||
|
||||
const HomeScreen = observer(class HomeScreen extends React.Component {
|
||||
state = {
|
||||
isError: false,
|
||||
isFullscreen: false,
|
||||
isLoading: true,
|
||||
isRefreshing: false
|
||||
};
|
||||
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
|
||||
}
|
||||
static propTypes = {
|
||||
navigation: PropTypes.object.isRequired,
|
||||
route: PropTypes.object.isRequired,
|
||||
rootStore: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
getErrorView() {
|
||||
const { t } = this.props;
|
||||
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>
|
||||
);
|
||||
}
|
||||
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();');
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
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 });
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
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];
|
||||
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>
|
||||
);
|
||||
}
|
||||
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'
|
||||
}
|
||||
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();
|
||||
const stores = useStores();
|
||||
const translation = useTranslation();
|
||||
|
||||
return (
|
||||
<HomeScreen
|
||||
{...props}
|
||||
navigation={useNavigation()}
|
||||
route={useRoute()}
|
||||
{...stores}
|
||||
{...translation}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<HomeScreen
|
||||
{...props}
|
||||
navigation={useNavigation()}
|
||||
route={useRoute()}
|
||||
{...stores}
|
||||
{...translation}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default HomeScreenWithNavigation;
|
||||
|
@ -10,30 +10,30 @@ import Colors from '../constants/Colors';
|
||||
import { SplashScreen } from 'expo';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
splash: {
|
||||
flex: 1,
|
||||
resizeMode: 'contain',
|
||||
width: undefined,
|
||||
height: undefined
|
||||
}
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
splash: {
|
||||
flex: 1,
|
||||
resizeMode: 'contain',
|
||||
width: undefined,
|
||||
height: undefined
|
||||
}
|
||||
});
|
||||
|
||||
function LoadingScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={styles.splash}
|
||||
source={require('../assets/images/splash.png')}
|
||||
onLoad={() => 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>
|
||||
);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={styles.splash}
|
||||
source={require('../assets/images/splash.png')}
|
||||
onLoad={() => 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>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingScreen;
|
||||
|
@ -21,181 +21,181 @@ import Links from '../constants/Links';
|
||||
import { useStores } from '../hooks/useStores';
|
||||
|
||||
const SettingsScreen = observer(() => {
|
||||
const { rootStore } = useStores();
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
const { rootStore } = useStores();
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch server info
|
||||
rootStore.serverStore.fetchInfo();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// Fetch server info
|
||||
rootStore.serverStore.fetchInfo();
|
||||
}, []);
|
||||
|
||||
const onAddServer = () => {
|
||||
navigation.navigate('AddServer');
|
||||
};
|
||||
const onAddServer = () => {
|
||||
navigation.navigate('AddServer');
|
||||
};
|
||||
|
||||
const onDeleteServer = index => {
|
||||
Alert.alert(
|
||||
t('alerts.deleteServer.title'),
|
||||
t('alerts.deleteServer.description', { serverName: rootStore.serverStore.servers[index]?.name }),
|
||||
[
|
||||
{ text: t('common.cancel') },
|
||||
{
|
||||
text: t('alerts.deleteServer.confirm'),
|
||||
onPress: () => {
|
||||
// Remove server and update active server
|
||||
rootStore.serverStore.removeServer(index);
|
||||
rootStore.settingStore.activeServer = 0;
|
||||
const onDeleteServer = index => {
|
||||
Alert.alert(
|
||||
t('alerts.deleteServer.title'),
|
||||
t('alerts.deleteServer.description', { serverName: rootStore.serverStore.servers[index]?.name }),
|
||||
[
|
||||
{ text: t('common.cancel') },
|
||||
{
|
||||
text: t('alerts.deleteServer.confirm'),
|
||||
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'
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
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 onSelectServer = index => {
|
||||
rootStore.settingStore.activeServer = index;
|
||||
navigation.navigate('Home');
|
||||
};
|
||||
|
||||
const onResetApplication = () => {
|
||||
Alert.alert(
|
||||
t('alerts.resetApplication.title'),
|
||||
t('alerts.resetApplication.description'),
|
||||
[
|
||||
{ text: t('common.cancel') },
|
||||
{
|
||||
text: t('alerts.resetApplication.confirm'),
|
||||
onPress: () => {
|
||||
// Reset data in stores
|
||||
rootStore.reset();
|
||||
AsyncStorage.clear();
|
||||
// Navigate to the loading screen
|
||||
navigation.replace('AddServer');
|
||||
},
|
||||
style: 'destructive'
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
const onResetApplication = () => {
|
||||
Alert.alert(
|
||||
t('alerts.resetApplication.title'),
|
||||
t('alerts.resetApplication.description'),
|
||||
[
|
||||
{ text: t('common.cancel') },
|
||||
{
|
||||
text: t('alerts.resetApplication.confirm'),
|
||||
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 AugmentedServerListItem = (props) => (
|
||||
<ServerListItem
|
||||
{...props}
|
||||
activeServer={rootStore.settingStore.activeServer}
|
||||
onDelete={onDeleteServer}
|
||||
onPress={onSelectServer}
|
||||
/>
|
||||
);
|
||||
|
||||
const getSections = () => {
|
||||
return [
|
||||
{
|
||||
title: t('headings.servers'),
|
||||
data: rootStore.serverStore.servers.slice(),
|
||||
keyExtractor: (item, index) => `server-${index}`,
|
||||
renderItem: AugmentedServerListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.addServer'),
|
||||
hideHeader: true,
|
||||
data: [{
|
||||
key: 'add-server-button',
|
||||
title: t('headings.addServer'),
|
||||
onPress: onAddServer
|
||||
}],
|
||||
renderItem: ButtonListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.settings'),
|
||||
data: [
|
||||
{
|
||||
key: 'keep-awake-switch',
|
||||
title: t('settings.keepAwake'),
|
||||
value: rootStore.settingStore.isScreenLockEnabled,
|
||||
onValueChange: value => rootStore.settingStore.isScreenLockEnabled = value
|
||||
},
|
||||
{
|
||||
key: 'rotation-lock-switch',
|
||||
title: t('settings.rotationLock'),
|
||||
value: rootStore.settingStore.isRotationEnabled,
|
||||
onValueChange: value => rootStore.settingStore.isRotationEnabled = value
|
||||
}
|
||||
],
|
||||
renderItem: SwitchListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.links'),
|
||||
data: Links.map(link => ({
|
||||
...link,
|
||||
name: t(link.name)
|
||||
})),
|
||||
renderItem: BrowserListItem
|
||||
},
|
||||
{
|
||||
title: t('alerts.resetApplication.title'),
|
||||
hideHeader: true,
|
||||
data: [{
|
||||
key: 'reset-app-button',
|
||||
title: t('alerts.resetApplication.title'),
|
||||
titleStyle: {
|
||||
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error
|
||||
},
|
||||
onPress: onResetApplication
|
||||
}],
|
||||
renderItem: ButtonListItem
|
||||
}
|
||||
];
|
||||
};
|
||||
const getSections = () => {
|
||||
return [
|
||||
{
|
||||
title: t('headings.servers'),
|
||||
data: rootStore.serverStore.servers.slice(),
|
||||
keyExtractor: (item, index) => `server-${index}`,
|
||||
renderItem: AugmentedServerListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.addServer'),
|
||||
hideHeader: true,
|
||||
data: [{
|
||||
key: 'add-server-button',
|
||||
title: t('headings.addServer'),
|
||||
onPress: onAddServer
|
||||
}],
|
||||
renderItem: ButtonListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.settings'),
|
||||
data: [
|
||||
{
|
||||
key: 'keep-awake-switch',
|
||||
title: t('settings.keepAwake'),
|
||||
value: rootStore.settingStore.isScreenLockEnabled,
|
||||
onValueChange: value => rootStore.settingStore.isScreenLockEnabled = value
|
||||
},
|
||||
{
|
||||
key: 'rotation-lock-switch',
|
||||
title: t('settings.rotationLock'),
|
||||
value: rootStore.settingStore.isRotationEnabled,
|
||||
onValueChange: value => rootStore.settingStore.isRotationEnabled = value
|
||||
}
|
||||
],
|
||||
renderItem: SwitchListItem
|
||||
},
|
||||
{
|
||||
title: t('headings.links'),
|
||||
data: Links.map(link => ({
|
||||
...link,
|
||||
name: t(link.name)
|
||||
})),
|
||||
renderItem: BrowserListItem
|
||||
},
|
||||
{
|
||||
title: t('alerts.resetApplication.title'),
|
||||
hideHeader: true,
|
||||
data: [{
|
||||
key: 'reset-app-button',
|
||||
title: t('alerts.resetApplication.title'),
|
||||
titleStyle: {
|
||||
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error
|
||||
},
|
||||
onPress: onResetApplication
|
||||
}],
|
||||
renderItem: ButtonListItem
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['right', 'bottom', 'left']} >
|
||||
<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 ? <View style={styles.emptyHeader} /> : <Text style={styles.header}>{title}</Text>
|
||||
)}
|
||||
renderSectionFooter={() => <View style={styles.footer} />}
|
||||
ListFooterComponent={AppInfoFooter}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['right', 'bottom', 'left']} >
|
||||
<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 ? <View style={styles.emptyHeader} /> : <Text style={styles.header}>{title}</Text>
|
||||
)}
|
||||
renderSectionFooter={() => <View style={styles.footer} />}
|
||||
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
|
||||
},
|
||||
emptyHeader: {
|
||||
marginTop: 15
|
||||
},
|
||||
footer: {
|
||||
marginBottom: 15
|
||||
}
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.backgroundColor
|
||||
},
|
||||
header: {
|
||||
backgroundColor: Colors.backgroundColor,
|
||||
color: colors.grey4,
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 15,
|
||||
marginBottom: 1
|
||||
},
|
||||
emptyHeader: {
|
||||
marginTop: 15
|
||||
},
|
||||
footer: {
|
||||
marginBottom: 15
|
||||
}
|
||||
});
|
||||
|
||||
export default SettingsScreen;
|
||||
|
@ -6,21 +6,21 @@
|
||||
import { decorate } from 'mobx';
|
||||
import { ignore } from 'mobx-sync';
|
||||
|
||||
import ServerStore from "./ServerStore";
|
||||
import SettingStore from "./SettingStore";
|
||||
import ServerStore from './ServerStore';
|
||||
import SettingStore from './SettingStore';
|
||||
|
||||
export default class RootStore {
|
||||
storeLoaded = false
|
||||
storeLoaded = false
|
||||
|
||||
serverStore = new ServerStore()
|
||||
settingStore = new SettingStore()
|
||||
serverStore = new ServerStore()
|
||||
settingStore = new SettingStore()
|
||||
|
||||
reset() {
|
||||
this.serverStore.reset();
|
||||
this.settingStore.reset();
|
||||
}
|
||||
reset() {
|
||||
this.serverStore.reset();
|
||||
this.settingStore.reset();
|
||||
}
|
||||
}
|
||||
|
||||
decorate(RootStore, {
|
||||
storeLoaded: ignore
|
||||
storeLoaded: ignore
|
||||
});
|
||||
|
@ -10,33 +10,33 @@ import { task } from 'mobx-task';
|
||||
import ServerModel from '../models/ServerModel';
|
||||
|
||||
export default class ServerStore {
|
||||
servers = []
|
||||
servers = []
|
||||
|
||||
addServer(server) {
|
||||
this.servers.push(new ServerModel(this.servers.length, server.url));
|
||||
}
|
||||
addServer(server) {
|
||||
this.servers.push(new ServerModel(this.servers.length, server.url));
|
||||
}
|
||||
|
||||
removeServer(index) {
|
||||
this.servers.splice(index, 1);
|
||||
}
|
||||
removeServer(index) {
|
||||
this.servers.splice(index, 1);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.servers = [];
|
||||
}
|
||||
reset() {
|
||||
this.servers = [];
|
||||
}
|
||||
|
||||
fetchInfo = task(async () => {
|
||||
await Promise.all(
|
||||
this.servers.map(server => server.fetchInfo())
|
||||
);
|
||||
})
|
||||
fetchInfo = task(async () => {
|
||||
await Promise.all(
|
||||
this.servers.map(server => server.fetchInfo())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
decorate(ServerStore, {
|
||||
servers: [
|
||||
format(data => data.map(value => new ServerModel(value.id, value.url, value.info))),
|
||||
observable
|
||||
],
|
||||
addServer: action,
|
||||
removeServer: action,
|
||||
reset: action
|
||||
servers: [
|
||||
format(data => data.map(value => new ServerModel(value.id, value.url, value.info))),
|
||||
observable
|
||||
],
|
||||
addServer: action,
|
||||
removeServer: action,
|
||||
reset: action
|
||||
});
|
||||
|
@ -9,31 +9,31 @@ import { action, decorate, observable } from 'mobx';
|
||||
* Data store for application settings
|
||||
*/
|
||||
export default class SettingStore {
|
||||
/**
|
||||
* The id of the currently selected server
|
||||
*/
|
||||
activeServer = 0
|
||||
/**
|
||||
* The id of the currently selected server
|
||||
*/
|
||||
activeServer = 0
|
||||
|
||||
/**
|
||||
* Is device rotation enabled
|
||||
*/
|
||||
isRotationEnabled
|
||||
/**
|
||||
* Is device rotation enabled
|
||||
*/
|
||||
isRotationEnabled
|
||||
|
||||
/**
|
||||
* Is screen lock active when media is playing
|
||||
*/
|
||||
isScreenLockEnabled = true
|
||||
/**
|
||||
* Is screen lock active when media is playing
|
||||
*/
|
||||
isScreenLockEnabled = true
|
||||
|
||||
reset() {
|
||||
this.activeServer = 0;
|
||||
this.isRotationEnabled = null;
|
||||
this.isScreenLockEnabled = true;
|
||||
}
|
||||
reset() {
|
||||
this.activeServer = 0;
|
||||
this.isRotationEnabled = null;
|
||||
this.isScreenLockEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
decorate(SettingStore, {
|
||||
activeServer: observable,
|
||||
isRotationEnabled: observable,
|
||||
isScreenLockEnabled: observable,
|
||||
reset: action
|
||||
activeServer: observable,
|
||||
isRotationEnabled: observable,
|
||||
isScreenLockEnabled: observable,
|
||||
reset: action
|
||||
});
|
||||
|
@ -10,50 +10,50 @@ import { AsyncStorage } from 'react-native';
|
||||
* @deprecated
|
||||
*/
|
||||
export default class CachingStorage {
|
||||
static instance = null;
|
||||
static instance = null;
|
||||
|
||||
cache = new Map();
|
||||
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;
|
||||
}
|
||||
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()`');
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
// 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()`');
|
||||
}
|
||||
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);
|
||||
}
|
||||
console.debug(`CachingStorage: Saving value to device storage for ${key}`);
|
||||
await AsyncStorage.setItem(key, JSON.stringify(item));
|
||||
this.cache.set(key, item);
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,20 @@ import Constants from 'expo-constants';
|
||||
import * as Device from 'expo-device';
|
||||
|
||||
export function getAppName() {
|
||||
return `Jellyfin Mobile (${Device.osName})`;
|
||||
return `Jellyfin Mobile (${Device.osName})`;
|
||||
}
|
||||
|
||||
export function getSafeDeviceName() {
|
||||
let safeName = Constants.deviceName
|
||||
// Replace non-ascii apostrophe with single quote (default on iOS)
|
||||
.replace(/’/g, '\'')
|
||||
// Remove all other non-ascii characters
|
||||
.replace(/[^\x20-\x7E]/g, '')
|
||||
// Trim whitespace
|
||||
.trim();
|
||||
if (safeName) {
|
||||
return safeName;
|
||||
}
|
||||
let safeName = Constants.deviceName
|
||||
// Replace non-ascii apostrophe with single quote (default on iOS)
|
||||
.replace(/’/g, '\'')
|
||||
// Remove all other non-ascii characters
|
||||
.replace(/[^\x20-\x7E]/g, '')
|
||||
// Trim whitespace
|
||||
.trim();
|
||||
if (safeName) {
|
||||
return safeName;
|
||||
}
|
||||
|
||||
return Device.modelName || 'Jellyfin Mobile Device';
|
||||
return Device.modelName || 'Jellyfin Mobile Device';
|
||||
}
|
||||
|
@ -6,8 +6,8 @@
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export const getIconName = (name = '') => {
|
||||
if (name) {
|
||||
return Platform.OS === 'ios' ? `ios-${name}` : `md-${name}`;
|
||||
}
|
||||
return name;
|
||||
if (name) {
|
||||
return Platform.OS === 'ios' ? `ios-${name}` : `md-${name}`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
@ -7,118 +7,118 @@
|
||||
import Url from 'url';
|
||||
|
||||
export default class JellyfinValidator {
|
||||
static TIMEOUT_DURATION = 5000 // timeout request after 5s
|
||||
static TIMEOUT_DURATION = 5000 // timeout request after 5s
|
||||
|
||||
static parseUrl(host = '', port = '') {
|
||||
if (!host) {
|
||||
throw new Error('host cannot be blank');
|
||||
}
|
||||
static parseUrl(host = '', port = '') {
|
||||
if (!host) {
|
||||
throw new Error('host cannot be blank');
|
||||
}
|
||||
|
||||
// Default the protocol to http if not present
|
||||
// Setting the protocol on the parsed url does not update the href
|
||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||
host = `http://${host}`;
|
||||
}
|
||||
// Default the protocol to http if not present
|
||||
// Setting the protocol on the parsed url does not update the href
|
||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||
host = `http://${host}`;
|
||||
}
|
||||
|
||||
// Parse the host as a url
|
||||
const url = Url.parse(host);
|
||||
// Parse the host as a url
|
||||
const url = Url.parse(host);
|
||||
|
||||
if (!url.hostname) {
|
||||
throw new Error(`Could not parse hostname for ${host}`);
|
||||
}
|
||||
if (!url.hostname) {
|
||||
throw new Error(`Could not parse hostname for ${host}`);
|
||||
}
|
||||
|
||||
// Override the port if provided
|
||||
if (port) {
|
||||
url.port = port;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
// Override the port if provided
|
||||
if (port) {
|
||||
url.port = port;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
static async fetchServerInfo(server = {}) {
|
||||
const serverUrl = server.urlString || this.getServerUrl(server);
|
||||
const infoUrl = `${serverUrl}system/info/public`;
|
||||
console.log('info url', infoUrl);
|
||||
static async fetchServerInfo(server = {}) {
|
||||
const serverUrl = server.urlString || this.getServerUrl(server);
|
||||
const infoUrl = `${serverUrl}system/info/public`;
|
||||
console.log('info url', infoUrl);
|
||||
|
||||
// Try to fetch the server's public info
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
// Try to fetch the server's public info
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const request = fetch(infoUrl, { signal });
|
||||
const request = fetch(infoUrl, { signal });
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('request timed out, aborting');
|
||||
controller.abort();
|
||||
}, this.TIMEOUT_DURATION);
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('request timed out, aborting');
|
||||
controller.abort();
|
||||
}, this.TIMEOUT_DURATION);
|
||||
|
||||
const responseJson = await request.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error response status [${response.status}] received from ${infoUrl}`);
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
console.log('response', responseJson);
|
||||
const responseJson = await request.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error response status [${response.status}] received from ${infoUrl}`);
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
console.log('response', responseJson);
|
||||
|
||||
return responseJson;
|
||||
}
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
static getServerUrl(server = {}) {
|
||||
if (!server || !server.url || !server.url.href) {
|
||||
throw new Error('Cannot get server url for invalid server', server);
|
||||
}
|
||||
static getServerUrl(server = {}) {
|
||||
if (!server || !server.url || !server.url.href) {
|
||||
throw new Error('Cannot get server url for invalid server', server);
|
||||
}
|
||||
|
||||
// Strip the query string or hash if present
|
||||
let serverUrl = server.url.href;
|
||||
if (server.url.search || server.url.hash) {
|
||||
const endUrl = server.url.search || server.url.hash;
|
||||
serverUrl = serverUrl.substring(0, serverUrl.indexOf(endUrl));
|
||||
}
|
||||
// Strip the query string or hash if present
|
||||
let serverUrl = server.url.href;
|
||||
if (server.url.search || server.url.hash) {
|
||||
const endUrl = server.url.search || server.url.hash;
|
||||
serverUrl = serverUrl.substring(0, serverUrl.indexOf(endUrl));
|
||||
}
|
||||
|
||||
// Ensure the url ends with /
|
||||
if (!serverUrl.endsWith('/')) {
|
||||
serverUrl += '/';
|
||||
}
|
||||
// Ensure the url ends with /
|
||||
if (!serverUrl.endsWith('/')) {
|
||||
serverUrl += '/';
|
||||
}
|
||||
|
||||
console.log('getServerUrl:', serverUrl);
|
||||
return serverUrl;
|
||||
}
|
||||
console.log('getServerUrl:', serverUrl);
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
static async validate(server = {}) {
|
||||
try {
|
||||
// Does the server have a valid url?
|
||||
this.getServerUrl(server);
|
||||
} catch (err) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'invalid'
|
||||
};
|
||||
}
|
||||
static async validate(server = {}) {
|
||||
try {
|
||||
// Does the server have a valid url?
|
||||
this.getServerUrl(server);
|
||||
} catch (err) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const responseJson = await this.fetchServerInfo(server);
|
||||
try {
|
||||
const responseJson = await this.fetchServerInfo(server);
|
||||
|
||||
// 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 { isValid: true };
|
||||
}
|
||||
}
|
||||
// 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 { isValid: true };
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = responseJson.ProductName === 'Jellyfin Server';
|
||||
const answer = {
|
||||
isValid
|
||||
};
|
||||
if (!isValid) {
|
||||
answer.message = 'invalidProduct';
|
||||
}
|
||||
return answer;
|
||||
} catch (err) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'noConnection'
|
||||
};
|
||||
}
|
||||
}
|
||||
const isValid = responseJson.ProductName === 'Jellyfin Server';
|
||||
const answer = {
|
||||
isValid
|
||||
};
|
||||
if (!isValid) {
|
||||
answer.message = 'invalidProduct';
|
||||
}
|
||||
return answer;
|
||||
} catch (err) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'noConnection'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
utils/Theme.js
102
utils/Theme.js
@ -9,55 +9,55 @@ import { colors } from 'react-native-elements';
|
||||
import Colors from '../constants/Colors';
|
||||
|
||||
export default {
|
||||
colors: {
|
||||
primary: Colors.tintColor
|
||||
},
|
||||
Badge: {
|
||||
badgeStyle: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
Icon: {
|
||||
iconStyle: {
|
||||
color: Colors.textColor
|
||||
}
|
||||
},
|
||||
Input: {
|
||||
inputStyle: {
|
||||
color: Colors.textColor
|
||||
},
|
||||
errorStyle: {
|
||||
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error,
|
||||
fontSize: 16
|
||||
},
|
||||
leftIconContainerStyle: {
|
||||
marginRight: 8
|
||||
},
|
||||
rightIconContainerStyle: {
|
||||
marginRight: 15
|
||||
}
|
||||
},
|
||||
ListItem: {
|
||||
containerStyle: {
|
||||
backgroundColor: Colors.headerBackgroundColor
|
||||
},
|
||||
subtitleStyle: {
|
||||
color: colors.grey4,
|
||||
lineHeight: 21
|
||||
},
|
||||
rightSubtitleStyle: {
|
||||
color: colors.grey4
|
||||
}
|
||||
},
|
||||
Overlay: {
|
||||
windowBackgroundColor: 'rgba(0, 0, 0, .85)',
|
||||
overlayStyle: {
|
||||
backgroundColor: Colors.backgroundColor
|
||||
}
|
||||
},
|
||||
Text: {
|
||||
style: {
|
||||
color: Colors.textColor
|
||||
}
|
||||
}
|
||||
colors: {
|
||||
primary: Colors.tintColor
|
||||
},
|
||||
Badge: {
|
||||
badgeStyle: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
Icon: {
|
||||
iconStyle: {
|
||||
color: Colors.textColor
|
||||
}
|
||||
},
|
||||
Input: {
|
||||
inputStyle: {
|
||||
color: Colors.textColor
|
||||
},
|
||||
errorStyle: {
|
||||
color: Platform.OS === 'ios' ? colors.platform.ios.error : colors.platform.android.error,
|
||||
fontSize: 16
|
||||
},
|
||||
leftIconContainerStyle: {
|
||||
marginRight: 8
|
||||
},
|
||||
rightIconContainerStyle: {
|
||||
marginRight: 15
|
||||
}
|
||||
},
|
||||
ListItem: {
|
||||
containerStyle: {
|
||||
backgroundColor: Colors.headerBackgroundColor
|
||||
},
|
||||
subtitleStyle: {
|
||||
color: colors.grey4,
|
||||
lineHeight: 21
|
||||
},
|
||||
rightSubtitleStyle: {
|
||||
color: colors.grey4
|
||||
}
|
||||
},
|
||||
Overlay: {
|
||||
windowBackgroundColor: 'rgba(0, 0, 0, .85)',
|
||||
overlayStyle: {
|
||||
backgroundColor: Colors.backgroundColor
|
||||
}
|
||||
},
|
||||
Text: {
|
||||
style: {
|
||||
color: Colors.textColor
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -3,30 +3,30 @@
|
||||
* 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 * as WebBrowser from "expo-web-browser";
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
|
||||
import Colors from '../constants/Colors';
|
||||
|
||||
export async function openBrowser(url, options) {
|
||||
const finalOptions = Object.assign({
|
||||
toolbarColor: Colors.backgroundColor,
|
||||
controlsColor: Colors.tintColor
|
||||
}, options);
|
||||
const finalOptions = Object.assign({
|
||||
toolbarColor: Colors.backgroundColor,
|
||||
controlsColor: Colors.tintColor
|
||||
}, options);
|
||||
|
||||
try {
|
||||
await WebBrowser.openBrowserAsync(url, finalOptions);
|
||||
} catch (err) {
|
||||
// Workaround issue where swiping browser closed does not dismiss it.
|
||||
// https://github.com/expo/expo/issues/6918
|
||||
if (err.message === 'Another WebBrowser is already being presented.') {
|
||||
try {
|
||||
await WebBrowser.dismissBrowser();
|
||||
return WebBrowser.openBrowserAsync(url, finalOptions);
|
||||
} catch (retryErr) {
|
||||
console.warn('Could not dismiss and reopen browser', retryErr);
|
||||
}
|
||||
} else {
|
||||
console.warn('Could not open browser', err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await WebBrowser.openBrowserAsync(url, finalOptions);
|
||||
} catch (err) {
|
||||
// Workaround issue where swiping browser closed does not dismiss it.
|
||||
// https://github.com/expo/expo/issues/6918
|
||||
if (err.message === 'Another WebBrowser is already being presented.') {
|
||||
try {
|
||||
await WebBrowser.dismissBrowser();
|
||||
return WebBrowser.openBrowserAsync(url, finalOptions);
|
||||
} catch (retryErr) {
|
||||
console.warn('Could not dismiss and reopen browser', retryErr);
|
||||
}
|
||||
} else {
|
||||
console.warn('Could not open browser', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user