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