Add file downloads

This commit is contained in:
Bill Thornton 2021-11-18 14:24:50 -05:00
parent 38b6a930da
commit fd3f3401c2
11 changed files with 218 additions and 4 deletions

View File

@ -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() {

View 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;

View File

@ -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);

View File

@ -9,6 +9,7 @@ export default {
AddServerScreen: 'AddServer',
HomeScreen: 'HomeScreen',
HomeTab: 'Home',
DownloadsTab: 'Downloads',
ServerHelpScreen: 'ServerHelpScreen',
SettingsTab: 'Settings',
SettingsScreen: 'SettingsScreen',

View File

@ -8,6 +8,7 @@
"headings": {
"addServer": "Add Server",
"appearance": "Appearance",
"downloads": "Downloads",
"home": "Home",
"links": "Links",
"playback": "Playback",

View File

@ -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
View File

@ -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",

View File

@ -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
View 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
View 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
});

View File

@ -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();