now with download progress bar and media playback

This commit is contained in:
Akinwale Ariwodola 2018-03-18 15:42:16 +01:00
parent 2063e716f5
commit deb9771b68
30 changed files with 1530 additions and 67 deletions

View file

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

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

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

View file

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

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

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View file

@ -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 -%}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View file

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

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
JR['jÎÉੲua Z ·ѓ*Цї<D0A6>1rЏ 8Љ¤)Чт»EгЉ

View file

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

View file

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

View file

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