now with download progress bar and media playback
|
@ -11,6 +11,7 @@
|
||||||
"react": "16.2.0",
|
"react": "16.2.0",
|
||||||
"react-native": "0.52.0",
|
"react-native": "0.52.0",
|
||||||
"react-native-vector-icons": "^4.5.0",
|
"react-native-vector-icons": "^4.5.0",
|
||||||
|
"react-native-video": "2.0.0",
|
||||||
"react-navigation": "^1.0.3",
|
"react-navigation": "^1.0.3",
|
||||||
"react-navigation-redux-helpers": "^1.0.1",
|
"react-navigation-redux-helpers": "^1.0.1",
|
||||||
"react-redux": "^5.0.3",
|
"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 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 filePageStyle from '../../styles/filePage';
|
||||||
import FileItemMedia from '../../component/fileItemMedia';
|
import FileItemMedia from '../../component/fileItemMedia';
|
||||||
|
import FileDownloadButton from '../../component/fileDownloadButton';
|
||||||
|
|
||||||
class FilePage extends React.PureComponent {
|
class FilePage extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
rate: 1,
|
||||||
|
volume: 1,
|
||||||
|
muted: false,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
duration: 0.0,
|
||||||
|
currentTime: 0.0,
|
||||||
|
paused: true,
|
||||||
|
};
|
||||||
|
|
||||||
static navigationOptions = {
|
static navigationOptions = {
|
||||||
title: ''
|
title: ''
|
||||||
};
|
};
|
||||||
|
@ -28,7 +41,7 @@ class FilePage extends React.PureComponent {
|
||||||
props.fetchCostInfo(props.navigation.state.params.uri);
|
props.fetchCostInfo(props.navigation.state.params.uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
claim,
|
claim,
|
||||||
|
@ -36,9 +49,9 @@ class FilePage extends React.PureComponent {
|
||||||
metadata,
|
metadata,
|
||||||
contentType,
|
contentType,
|
||||||
tab,
|
tab,
|
||||||
uri,
|
|
||||||
rewardedContentClaimIds,
|
rewardedContentClaimIds,
|
||||||
} = this.props;
|
navigation
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (!claim || !metadata) {
|
if (!claim || !metadata) {
|
||||||
return (
|
return (
|
||||||
|
@ -48,23 +61,37 @@ class FilePage extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const completed = fileInfo && fileInfo.completed;
|
||||||
const title = metadata.title;
|
const title = metadata.title;
|
||||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||||
const description = metadata.description ? metadata.description : null;
|
const description = metadata.description ? metadata.description : null;
|
||||||
//const mediaType = lbry.getMediaType(contentType);
|
const mediaType = Lbry.getMediaType(contentType);
|
||||||
//const player = require('render-media');
|
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||||
//const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
|
||||||
/*const isPlayable =
|
|
||||||
Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio';*/
|
|
||||||
const { height, channel_name: channelName, value } = claim;
|
const { height, channel_name: channelName, value } = claim;
|
||||||
const channelClaimId =
|
const channelClaimId =
|
||||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={filePageStyle.pageContainer}>
|
<View style={filePageStyle.pageContainer}>
|
||||||
<View style={filePageStyle.mediaContainer}>
|
<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>
|
</View>
|
||||||
<ScrollView style={filePageStyle.scrollContainer}>
|
<ScrollView style={filePageStyle.scrollContainer}>
|
||||||
<Text style={filePageStyle.title}>{title}</Text>
|
<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
|
flex: 1
|
||||||
},
|
},
|
||||||
mediaContainer: {
|
mediaContainer: {
|
||||||
backgroundColor: '#000000'
|
backgroundColor: '#000000',
|
||||||
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
emptyClaimText: {
|
emptyClaimText: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
@ -49,6 +50,14 @@ const filePageStyle = StyleSheet.create({
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
width: screenWidth,
|
width: screenWidth,
|
||||||
height: 200
|
height: 200
|
||||||
|
},
|
||||||
|
downloadButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%'
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
width: screenWidth,
|
||||||
|
height: 200
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -364,7 +364,6 @@ main.py that loads it.''')
|
||||||
remove('AndroidManifest.xml')
|
remove('AndroidManifest.xml')
|
||||||
shutil.copy(join('src', 'main', 'AndroidManifest.xml'),
|
shutil.copy(join('src', 'main', 'AndroidManifest.xml'),
|
||||||
'AndroidManifest.xml')
|
'AndroidManifest.xml')
|
||||||
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
'strings.tmpl.xml',
|
'strings.tmpl.xml',
|
||||||
|
@ -397,6 +396,18 @@ main.py that loads it.''')
|
||||||
aars=aars,
|
aars=aars,
|
||||||
android_api=android_api,
|
android_api=android_api,
|
||||||
build_tools_version=build_tools_version)
|
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
|
## ant build templates
|
||||||
render(
|
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 {
|
dependencies {
|
||||||
|
compile project(':react-native-video')
|
||||||
{%- for aar in aars %}
|
{%- for aar in aars %}
|
||||||
compile(name: '{{ aar }}', ext: 'aar')
|
compile(name: '{{ aar }}', ext: 'aar')
|
||||||
{%- endfor -%}
|
{%- 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.net.Uri;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import com.brentvatne.react.ReactVideoPackage;
|
||||||
import com.facebook.react.common.LifecycleState;
|
import com.facebook.react.common.LifecycleState;
|
||||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||||
import com.facebook.react.ReactRootView;
|
import com.facebook.react.ReactRootView;
|
||||||
import com.facebook.react.ReactInstanceManager;
|
import com.facebook.react.ReactInstanceManager;
|
||||||
import com.facebook.react.shell.MainReactPackage;
|
import com.facebook.react.shell.MainReactPackage;
|
||||||
|
|
||||||
|
import io.lbry.lbrynet.reactpackages.LbryReactPackage;
|
||||||
|
|
||||||
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
|
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
|
||||||
private static final int OVERLAY_PERMISSION_REQ_CODE = 101;
|
private static final int OVERLAY_PERMISSION_REQ_CODE = 101;
|
||||||
|
|
||||||
|
@ -51,7 +54,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
||||||
.setBundleAssetName("index.android.bundle")
|
.setBundleAssetName("index.android.bundle")
|
||||||
.setJSMainModulePath("index")
|
.setJSMainModulePath("index")
|
||||||
.addPackage(new MainReactPackage())
|
.addPackage(new MainReactPackage())
|
||||||
/*.setUseDeveloperSupport(BuildConfig.DEBUG)*/
|
.addPackage(new ReactVideoPackage())
|
||||||
|
.addPackage(new LbryReactPackage())
|
||||||
|
.setUseDeveloperSupport(true)
|
||||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||||
.build();
|
.build();
|
||||||
mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
|
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;
|
||||||
|
}
|
||||||
|
}
|