now with download progress bar and media playback
|
@ -11,6 +11,7 @@
|
|||
"react": "16.2.0",
|
||||
"react-native": "0.52.0",
|
||||
"react-native-vector-icons": "^4.5.0",
|
||||
"react-native-video": "2.0.0",
|
||||
"react-navigation": "^1.0.3",
|
||||
"react-navigation-redux-helpers": "^1.0.1",
|
||||
"react-redux": "^5.0.3",
|
||||
|
|
23
app/src/component/fileDownloadButton/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectDownloadingForUri,
|
||||
makeSelectLoadingForUri,
|
||||
makeSelectCostInfoForUri
|
||||
} from 'lbry-redux';
|
||||
import { doPurchaseUri, doStartDownload } from '../../redux/actions/file';
|
||||
import FileDownloadButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
downloading: makeSelectDownloadingForUri(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
loading: makeSelectLoadingForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileDownloadButton);
|
80
app/src/component/fileDownloadButton/view.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { Text, View, TouchableOpacity } from 'react-native';
|
||||
import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
|
||||
|
||||
class FileDownloadButton extends React.PureComponent {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
//this.checkAvailability(nextProps.uri);
|
||||
this.restartDownload(nextProps);
|
||||
}
|
||||
|
||||
restartDownload(props) {
|
||||
const { downloading, fileInfo, uri, restartDownload } = props;
|
||||
|
||||
if (
|
||||
!downloading &&
|
||||
fileInfo &&
|
||||
!fileInfo.completed &&
|
||||
fileInfo.written_bytes !== false &&
|
||||
fileInfo.written_bytes < fileInfo.total_bytes
|
||||
) {
|
||||
restartDownload(uri, fileInfo.outpoint);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fileInfo,
|
||||
downloading,
|
||||
uri,
|
||||
purchaseUri,
|
||||
costInfo,
|
||||
loading,
|
||||
doPause,
|
||||
style,
|
||||
} = this.props;
|
||||
|
||||
const openFile = () => {
|
||||
//openInShell(fileInfo.download_path);
|
||||
//doPause();
|
||||
};
|
||||
|
||||
if (loading || downloading) {
|
||||
const progress =
|
||||
fileInfo && fileInfo.written_bytes ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
|
||||
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
|
||||
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<View style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}></View>
|
||||
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (fileInfo === null && !downloading) {
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<Text>Fetching cost info...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={() => {
|
||||
purchaseUri(uri);
|
||||
}}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Download</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={() => openFile()}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Open</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileDownloadButton;
|
|
@ -1,9 +1,22 @@
|
|||
import React from 'react';
|
||||
import { Text, View, ScrollView } from 'react-native';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { Text, View, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import filePageStyle from '../../styles/filePage';
|
||||
import FileItemMedia from '../../component/fileItemMedia';
|
||||
import FileDownloadButton from '../../component/fileDownloadButton';
|
||||
|
||||
class FilePage extends React.PureComponent {
|
||||
state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: 'contain',
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: true,
|
||||
};
|
||||
|
||||
static navigationOptions = {
|
||||
title: ''
|
||||
};
|
||||
|
@ -28,7 +41,7 @@ class FilePage extends React.PureComponent {
|
|||
props.fetchCostInfo(props.navigation.state.params.uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
|
@ -36,9 +49,9 @@ class FilePage extends React.PureComponent {
|
|||
metadata,
|
||||
contentType,
|
||||
tab,
|
||||
uri,
|
||||
rewardedContentClaimIds,
|
||||
} = this.props;
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
if (!claim || !metadata) {
|
||||
return (
|
||||
|
@ -48,23 +61,37 @@ class FilePage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const completed = fileInfo && fileInfo.completed;
|
||||
const title = metadata.title;
|
||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const description = metadata.description ? metadata.description : null;
|
||||
//const mediaType = lbry.getMediaType(contentType);
|
||||
//const player = require('render-media');
|
||||
//const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
/*const isPlayable =
|
||||
Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio';*/
|
||||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
|
||||
|
||||
return (
|
||||
<View style={filePageStyle.pageContainer}>
|
||||
<View style={filePageStyle.mediaContainer}>
|
||||
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />
|
||||
{(!fileInfo || !isPlayable) && <FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
|
||||
{!completed && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
|
||||
|
||||
{fileInfo && isPlayable &&
|
||||
<TouchableOpacity
|
||||
style={filePageStyle.player}
|
||||
onPress={() => this.setState({ paused: !this.state.paused })}>
|
||||
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
|
||||
resizeMode="cover"
|
||||
playInBackground={true}
|
||||
style={filePageStyle.player}
|
||||
rate={this.state.rate}
|
||||
volume={this.state.volume}
|
||||
paused={this.state.paused}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
</View>
|
||||
<ScrollView style={filePageStyle.scrollContainer}>
|
||||
<Text style={filePageStyle.title}>{title}</Text>
|
||||
|
|
222
app/src/redux/actions/file.js
Normal file
|
@ -0,0 +1,222 @@
|
|||
import {
|
||||
ACTIONS,
|
||||
Lbry,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
selectTotalDownloadProgress,
|
||||
selectDownloadingByOutpoint,
|
||||
} from 'lbry-redux';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||
|
||||
export function doUpdateLoadStatus(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
Lbry.file_list({
|
||||
outpoint,
|
||||
full_status: true,
|
||||
}).then(([fileInfo]) => {
|
||||
if (!fileInfo || fileInfo.written_bytes === 0) {
|
||||
// download hasn't started yet
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
}, DOWNLOAD_POLL_INTERVAL);
|
||||
} else if (fileInfo.completed) {
|
||||
// TODO this isn't going to get called if they reload the client before
|
||||
// the download finished
|
||||
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_COMPLETED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
NativeModules.LbryDownloadManager.updateDownload(uri, fileInfo.file_name, 100, writtenBytes, totalBytes);
|
||||
|
||||
/*const notif = new window.Notification('LBRY Download Complete', {
|
||||
body: fileInfo.metadata.stream.metadata.title,
|
||||
silent: false,
|
||||
});
|
||||
notif.onclick = () => {
|
||||
ipcRenderer.send('focusWindow', 'main');
|
||||
};*/
|
||||
} else {
|
||||
// ready to play
|
||||
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
|
||||
const progress = writtenBytes / totalBytes * 100;
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_PROGRESSED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
progress,
|
||||
},
|
||||
});
|
||||
|
||||
NativeModules.LbryDownloadManager.updateDownload(uri, fileInfo.file_name, progress, writtenBytes, totalBytes);
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
}, DOWNLOAD_POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doStartDownload(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (!outpoint) {
|
||||
throw new Error('outpoint is required to begin a download');
|
||||
}
|
||||
|
||||
const { downloadingByOutpoint = {} } = state.fileInfo;
|
||||
|
||||
if (downloadingByOutpoint[outpoint]) return;
|
||||
|
||||
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
NativeModules.LbryDownloadManager.startDownload(uri, fileInfo.file_name);
|
||||
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadFile(uri, streamInfo) {
|
||||
return dispatch => {
|
||||
dispatch(doStartDownload(uri, streamInfo.outpoint));
|
||||
|
||||
//analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
|
||||
|
||||
//dispatch(doClaimEligiblePurchaseRewards());
|
||||
};
|
||||
}
|
||||
|
||||
export function doSetPlayingUri(uri) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_PLAYING_URI,
|
||||
data: { uri },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doLoadVideo(uri) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
},
|
||||
});
|
||||
|
||||
Lbry.get({ uri })
|
||||
.then(streamInfo => {
|
||||
const timeout =
|
||||
streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout';
|
||||
|
||||
if (timeout) {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
console.log(`File timeout for uri ${uri}`);
|
||||
//dispatch(doOpenModal(MODALS.FILE_TIMEOUT, { uri }));
|
||||
} else {
|
||||
dispatch(doDownloadFile(uri, streamInfo));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
console.log(`Failed to download ${uri}`);
|
||||
/*dispatch(
|
||||
doAlertError(
|
||||
`Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.io/faq/support for support.`
|
||||
)
|
||||
);*/
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doPurchaseUri(uri, specificCostInfo) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = 0;//selectBalance(state);
|
||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||
|
||||
function attemptPlay(cost, instantPurchaseMax = null) {
|
||||
if (cost > 0 && (!instantPurchaseMax || cost > instantPurchaseMax)) {
|
||||
//dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
|
||||
console.log('Affirm purchase...');
|
||||
} else {
|
||||
dispatch(doLoadVideo(uri));
|
||||
}
|
||||
}
|
||||
|
||||
// we already fully downloaded the file.
|
||||
if (fileInfo && fileInfo.completed) {
|
||||
// If written_bytes is false that means the user has deleted/moved the
|
||||
// file manually on their file system, so we need to dispatch a
|
||||
// doLoadVideo action to reconstruct the file from the blobs
|
||||
if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri));
|
||||
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// we are already downloading the file
|
||||
if (alreadyDownloading) {
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
|
||||
const { cost } = costInfo;
|
||||
|
||||
if (cost > balance) {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
//dispatch(doOpenModal(MODALS.INSUFFICIENT_CREDITS));
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cost === 0/* || !makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state)*/) {
|
||||
attemptPlay(cost);
|
||||
}
|
||||
/*} else {
|
||||
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
|
||||
if (instantPurchaseMax.currency === 'LBC') {
|
||||
attemptPlay(cost, instantPurchaseMax.amount);
|
||||
} else {
|
||||
// Need to convert currency of instant purchase maximum before trying to play
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
||||
attemptPlay(cost, instantPurchaseMax.amount / LBC_USD);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
};
|
||||
}
|
18
app/src/styles/fileDownloadButton.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const fileDownloadButtonStyle = StyleSheet.create({
|
||||
container: {
|
||||
width: 120,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#40c0a9',
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
fontSize: 13,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default fileDownloadButtonStyle;
|
|
@ -12,7 +12,8 @@ const filePageStyle = StyleSheet.create({
|
|||
flex: 1
|
||||
},
|
||||
mediaContainer: {
|
||||
backgroundColor: '#000000'
|
||||
backgroundColor: '#000000',
|
||||
alignItems: 'center'
|
||||
},
|
||||
emptyClaimText: {
|
||||
textAlign: 'center',
|
||||
|
@ -49,6 +50,14 @@ const filePageStyle = StyleSheet.create({
|
|||
thumbnail: {
|
||||
width: screenWidth,
|
||||
height: 200
|
||||
},
|
||||
downloadButton: {
|
||||
position: 'absolute',
|
||||
top: '50%'
|
||||
},
|
||||
player: {
|
||||
width: screenWidth,
|
||||
height: 200
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -364,7 +364,6 @@ main.py that loads it.''')
|
|||
remove('AndroidManifest.xml')
|
||||
shutil.copy(join('src', 'main', 'AndroidManifest.xml'),
|
||||
'AndroidManifest.xml')
|
||||
|
||||
|
||||
render(
|
||||
'strings.tmpl.xml',
|
||||
|
@ -397,6 +396,18 @@ main.py that loads it.''')
|
|||
aars=aars,
|
||||
android_api=android_api,
|
||||
build_tools_version=build_tools_version)
|
||||
|
||||
render(
|
||||
'settings.tmpl.gradle',
|
||||
'settings.gradle'
|
||||
)
|
||||
|
||||
# copy icon drawables
|
||||
for folder in ('drawable-hdpi', 'drawable-mdpi', 'drawable-xhdpi', 'drawable-xxhdpi', 'drawable-xxxhdpi'):
|
||||
shutil.copy(
|
||||
'templates/res/{}/ic_file_download_black_24dp.png'.format(folder),
|
||||
'src/main/res/{}/ic_file_download_black_24dp.png'.format(folder)
|
||||
);
|
||||
|
||||
## ant build templates
|
||||
render(
|
||||
|
|
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 144 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 209 B |
|
@ -67,6 +67,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-video')
|
||||
{%- for aar in aars %}
|
||||
compile(name: '{{ aar }}', ext: 'aar')
|
||||
{%- endfor -%}
|
||||
|
|
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 144 B |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 209 B |
|
@ -0,0 +1,3 @@
|
|||
rootProject.name = 'lbrynet'
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android')
|
3
package-lock.json
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
|
@ -1 +1 @@
|
|||
JR['jÎÉŕ™©˛uaZ
|
||||
·ѓ*Цї<D0A6>1rЏ8Љ¤)Чт»EгЉ
|
|
@ -9,12 +9,15 @@ import android.content.Context;
|
|||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
|
||||
import io.lbry.lbrynet.reactpackages.LbryReactPackage;
|
||||
|
||||
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
|
||||
private static final int OVERLAY_PERMISSION_REQ_CODE = 101;
|
||||
|
||||
|
@ -51,7 +54,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index")
|
||||
.addPackage(new MainReactPackage())
|
||||
/*.setUseDeveloperSupport(BuildConfig.DEBUG)*/
|
||||
.addPackage(new ReactVideoPackage())
|
||||
.addPackage(new LbryReactPackage())
|
||||
.setUseDeveloperSupport(true)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
.build();
|
||||
mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package io.lbry.lbrynet.reactmodules;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import io.lbry.lbrynet.R;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Created by akinwale on 3/15/18.
|
||||
*/
|
||||
|
||||
public class LbryDownloadManagerModule extends ReactContextBaseJavaModule {
|
||||
private Context context;
|
||||
|
||||
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
|
||||
|
||||
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
|
||||
|
||||
private static final int MAX_PROGRESS = 100;
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
|
||||
|
||||
public LbryDownloadManagerModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.context = reactContext;
|
||||
}
|
||||
|
||||
private int generateNotificationId() {
|
||||
return new Random().nextInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LbryDownloadManager";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void startDownload(String id, String fileName) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
||||
builder.setContentTitle(String.format("Downloading %s...", fileName))
|
||||
.setSmallIcon(R.drawable.ic_file_download_black_24dp)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
|
||||
builder.setProgress(MAX_PROGRESS, 0, false);
|
||||
|
||||
int notificationId = generateNotificationId();
|
||||
downloadIdNotificationIdMap.put(id, notificationId);
|
||||
|
||||
builders.put(notificationId, builder);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateDownload(String id, String fileName, double progress, double writtenBytes, double totalBytes) {
|
||||
if (!downloadIdNotificationIdMap.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int notificationId = downloadIdNotificationIdMap.get(id);
|
||||
if (!builders.containsKey(notificationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = builders.get(notificationId);
|
||||
builder.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false);
|
||||
builder.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)));
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (progress == MAX_PROGRESS) {
|
||||
builder.setContentTitle(String.format("Downloaded %s.", fileName));
|
||||
downloadIdNotificationIdMap.remove(id);
|
||||
builders.remove(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatBytes(double bytes)
|
||||
{
|
||||
if (bytes < 1048576) { // < 1MB
|
||||
return String.format("%s KB", DECIMAL_FORMAT.format(bytes / 1024.0));
|
||||
}
|
||||
|
||||
if (bytes < 1073741824) { // < 1GB
|
||||
return String.format("%s MB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0)));
|
||||
}
|
||||
|
||||
return String.format("%s GB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0 * 1024.0)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.lbry.lbrynet.reactpackages;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import io.lbry.lbrynet.reactmodules.LbryDownloadManagerModule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class LbryReactPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new LbryDownloadManagerModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|