mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2024-11-23 05:59:39 +00:00
Add file downloads
This commit is contained in:
parent
38b6a930da
commit
fd3f3401c2
@ -6,7 +6,7 @@
|
||||
|
||||
// List of supported features as reported in Safari
|
||||
const ExpoSupportedFeatures = [
|
||||
// 'filedownload',
|
||||
'filedownload',
|
||||
'exit',
|
||||
'plugins',
|
||||
'externallinks',
|
||||
@ -102,8 +102,8 @@ window.NativeShell = {
|
||||
}
|
||||
},
|
||||
|
||||
downloadFile: function(url) {
|
||||
postExpoEvent('downloadFile', { url: url });
|
||||
downloadFile: function(item) {
|
||||
postExpoEvent('downloadFile', { item: item });
|
||||
},
|
||||
|
||||
enableFullscreen: function() {
|
||||
|
41
components/DownloadListItem.js
Normal file
41
components/DownloadListItem.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ActivityIndicator } from 'react-native';
|
||||
import { Button, ListItem } from 'react-native-elements';
|
||||
|
||||
import { getIconName } from '../utils/Icons';
|
||||
|
||||
const DownloadListItem = ({ item, index, onShare }) => (
|
||||
<ListItem
|
||||
topDivider={index === 0}
|
||||
bottomDivider
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title>{item.title}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
{item.isComplete ?
|
||||
<Button
|
||||
type='clear'
|
||||
icon={{
|
||||
name: getIconName('share-outline'),
|
||||
type: 'ionicon'
|
||||
}}
|
||||
onPress={() => onShare(item)}
|
||||
/> : <ActivityIndicator />
|
||||
}
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
DownloadListItem.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
onShare: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DownloadListItem;
|
@ -3,6 +3,7 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import compareVersions from 'compare-versions';
|
||||
import Constants from 'expo-constants';
|
||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||
@ -87,6 +88,15 @@ true;
|
||||
case 'disableFullscreen':
|
||||
rootStore.isFullscreen = false;
|
||||
break;
|
||||
case 'downloadFile':
|
||||
console.log('Download item', data);
|
||||
const url = new URL(data.item.url); // eslint-disable-line no-case-declarations
|
||||
// console.log('url', url.searchParams.get('api_key'));
|
||||
rootStore.downloadStore.add({
|
||||
...data.item,
|
||||
apiKey: url.searchParams.get('api_key')
|
||||
});
|
||||
break;
|
||||
case 'openUrl':
|
||||
console.log('Opening browser for external url', data.url);
|
||||
openBrowser(data.url);
|
||||
|
@ -9,6 +9,7 @@ export default {
|
||||
AddServerScreen: 'AddServer',
|
||||
HomeScreen: 'HomeScreen',
|
||||
HomeTab: 'Home',
|
||||
DownloadsTab: 'Downloads',
|
||||
ServerHelpScreen: 'ServerHelpScreen',
|
||||
SettingsTab: 'Settings',
|
||||
SettingsScreen: 'SettingsScreen',
|
||||
|
@ -8,6 +8,7 @@
|
||||
"headings": {
|
||||
"addServer": "Add Server",
|
||||
"appearance": "Appearance",
|
||||
"downloads": "Downloads",
|
||||
"home": "Home",
|
||||
"links": "Links",
|
||||
"playback": "Playback",
|
||||
|
@ -13,6 +13,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import Screens from '../constants/Screens';
|
||||
import { useStores } from '../hooks/useStores';
|
||||
import DownloadScreen from '../screens/DownloadScreen';
|
||||
import { getIconName } from '../utils/Icons';
|
||||
|
||||
import HomeNavigator from './HomeNavigator';
|
||||
@ -22,8 +23,12 @@ function TabIcon(routeName, color, size) {
|
||||
let iconName = null;
|
||||
if (routeName === Screens.HomeTab) {
|
||||
iconName = getIconName('tv-outline');
|
||||
} else if (routeName === Screens.DownloadsTab) {
|
||||
iconName = 'download-outline';
|
||||
} else if (routeName === Screens.SettingsTab) {
|
||||
iconName = getIconName('cog-outline');
|
||||
} else {
|
||||
iconName = 'help-circle-outline';
|
||||
}
|
||||
|
||||
return (
|
||||
@ -66,6 +71,13 @@ const TabNavigator = observer(() => {
|
||||
title: t('headings.home')
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.DownloadsTab}
|
||||
component={DownloadScreen}
|
||||
options={{
|
||||
title: t('headings.downloads')
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.SettingsTab}
|
||||
component={SettingsNavigator}
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -9348,6 +9348,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"expo-sharing": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-10.0.3.tgz",
|
||||
"integrity": "sha512-ZiXmirRVznbrrc0ztfNj+IV9bzUaeMqubEJSrnl/lIBXPPQNSSvDehe7hmPf7c7tHH6q4ebR/hlExkNgjz93wA==",
|
||||
"requires": {
|
||||
"expo-modules-core": "~0.4.4"
|
||||
}
|
||||
},
|
||||
"expo-splash-screen": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.13.5.tgz",
|
||||
|
@ -74,7 +74,8 @@
|
||||
"react-native-screens": "~3.8.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-webview": "11.13.0",
|
||||
"uuid": "^8.3.2"
|
||||
"uuid": "^8.3.2",
|
||||
"expo-sharing": "~10.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
|
104
screens/DownloadScreen.js
Normal file
104
screens/DownloadScreen.js
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { ThemeContext } from 'react-native-elements';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
import DownloadListItem from '../components/DownloadListItem';
|
||||
import { useStores } from '../hooks/useStores';
|
||||
|
||||
const getDownloadDir = download => `${FileSystem.documentDirectory}${download.serverId}/${download.itemId}/`;
|
||||
const getDownloadUri = download => getDownloadDir(download) + encodeURI(download.filename);
|
||||
|
||||
async function ensureDirExists(dir) {
|
||||
console.log('ensure directory', dir);
|
||||
const info = await FileSystem.getInfoAsync(dir);
|
||||
console.log('directory info', info);
|
||||
if (!info.exists) {
|
||||
await FileSystem.makeDirectoryAsync(dir, { intermediates: true });
|
||||
}
|
||||
}
|
||||
|
||||
const DownloadScreen = observer(() => {
|
||||
const { rootStore } = useStores();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const [ resumables, setResumables ] = useState([]);
|
||||
|
||||
async function downloadFile(download) {
|
||||
console.log('download file', download);
|
||||
await ensureDirExists(getDownloadDir(download));
|
||||
|
||||
const url = download.url;
|
||||
const uri = getDownloadUri(download);
|
||||
console.log('url', url, uri);
|
||||
|
||||
const resumable = FileSystem.createDownloadResumable(
|
||||
url,
|
||||
uri,
|
||||
{},
|
||||
console.log
|
||||
);
|
||||
setResumables([ ...resumables, resumable ]);
|
||||
try {
|
||||
download.isDownloading = true;
|
||||
await resumable.downloadAsync();
|
||||
download.isDownloading = false;
|
||||
download.isComplete = true;
|
||||
} catch (e) {
|
||||
console.error('Download failed', e);
|
||||
download.isDownloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
rootStore.downloadStore.downloads
|
||||
.filter(download => !download.isComplete && !download.isDownloading)
|
||||
.forEach(downloadFile);
|
||||
}, [ rootStore.downloadStore.downloads ]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
...styles.container,
|
||||
backgroundColor: theme.colors.background
|
||||
}}
|
||||
edges={[ 'right', 'left' ]}
|
||||
>
|
||||
<FlatList
|
||||
data={rootStore.downloadStore.downloads}
|
||||
renderItem={({ item, index }) => (
|
||||
<DownloadListItem
|
||||
item={item}
|
||||
index={index}
|
||||
onShare={async () => {
|
||||
Sharing.shareAsync(
|
||||
await FileSystem.getContentUriAsync(getDownloadUri(item))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item, index) => `download-${index}-${item.serverId}-${item.itemId}`}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
listContainer: {
|
||||
marginTop: 1
|
||||
}
|
||||
});
|
||||
|
||||
export default DownloadScreen;
|
33
stores/DownloadStore.js
Normal file
33
stores/DownloadStore.js
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { action, decorate, observable } from 'mobx';
|
||||
|
||||
export default class DownloadStore {
|
||||
downloads = []
|
||||
|
||||
add(download) {
|
||||
// Do not allow duplicate downloads
|
||||
if (!this.downloads.find(search => search.serverId === download.serverId && search.itemId === download.itemId)) {
|
||||
this.downloads.push(download);
|
||||
}
|
||||
}
|
||||
|
||||
remove(index) {
|
||||
this.downloads.splice(index, 1);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.downloads = [];
|
||||
}
|
||||
}
|
||||
|
||||
decorate(DownloadStore, {
|
||||
downloads: [ observable ],
|
||||
add: action,
|
||||
remove: action,
|
||||
reset: action
|
||||
});
|
@ -11,6 +11,7 @@ import { action, decorate, observable } from 'mobx';
|
||||
import { ignore } from 'mobx-sync-lite';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import DownloadStore from './DownloadStore';
|
||||
import MediaStore from './MediaStore';
|
||||
import ServerStore from './ServerStore';
|
||||
import SettingStore from './SettingStore';
|
||||
@ -41,6 +42,7 @@ export default class RootStore {
|
||||
*/
|
||||
didPlayerCloseManually = true
|
||||
|
||||
downloadStore = new DownloadStore()
|
||||
mediaStore = new MediaStore()
|
||||
serverStore = new ServerStore()
|
||||
settingStore = new SettingStore()
|
||||
@ -52,6 +54,7 @@ export default class RootStore {
|
||||
this.isReloadRequired = false;
|
||||
this.didPlayerCloseManually = true;
|
||||
|
||||
this.downloadStore.reset();
|
||||
this.mediaStore.reset();
|
||||
this.serverStore.reset();
|
||||
this.settingStore.reset();
|
||||
|
Loading…
Reference in New Issue
Block a user